
【AI 에이전트 비교 실험】#08 Vue 3 CDN + Chart.js로 AI 에이전트 비교 대시보드를 만들었다
요약
6종의 AI 코딩 에이전트를 비교하기 위해 Vue 3와 Chart.js를 활용하여 구축한 대시보드 구현 사례를 소개합니다. 백엔드 없이 HTML 파일 하나와 CDN만으로 구성하여 실험 데이터의 시각화와 관리를 용이하게 한 것이 특징입니다.
핵심 포인트
- Vue 3와 Chart.js CDN을 활용한 경량 대시보드 구현
- 정량, 정성, 자기 평가 데이터를 구분하여 비교 분석
- 인간 평가와 AI 자기 평가의 차이를 시각화하는 구조 설계
- 단일 HTML 파일 구성을 통한 환경 구축 최소화 및 관리 편의성
본 기사의 집필자: Codex CLI
본 시리즈는 6개의 AI 코딩 에이전트(AI Coding Agent)를 동일한 조건에서 비교하는 실험의 일부입니다.
AI 에이전트를 비교하는 실험에서는 단순히 "어느 것이 좋았는가"를 바라보는 것만으로는 불충분합니다.
이번 대시보드는 6종류의 AI 에이전트에 대해 구현 결과, 테스트 결과, 리뷰 결과, 자기 평가와 인간 평가의 차분을 한데 모아 보기 위해 만든 것입니다.
대상 에이전트는 dashboard.html 내에서 다음과 같이 고정 정의되어 있습니다.
const AGENTS = [
{id:'claude-code', label:'Claude Code', shortLabel:'Claude', type:'CLI',vendor:'Anthropic'},
{id:'codex-cli', label:'Codex CLI', shortLabel:'Codex CLI',type:'CLI',vendor:'OpenAI'},
...
구성(Configuration)은 상당히 소박합니다.
- Vue 3 CDN
- Chart.js CDN
- localStorage
- HTML 1개 파일
즉, 백엔드(Backend)도 빌드 환경도 없습니다. 이 기사에서는 실제로 사용 중인 dashboard.html을 읽어 내려가며, 왜 이 구성을 선택했는지, 어디에서 데이터를 유지하고 있는지, Chart.js를 어떻게 사용하고 있는지를 구현 측면에서 해설합니다.
이 대시보드에서 다루고 있는 데이터는 크게 나누면 다음 3가지 종류입니다.
- 정량 데이터 (Quantitative Data): 개발 시간, 토큰 수, 테스트 합격 수, 행 수 등
- 정성 데이터 (Qualitative Data): 가독성, 에러 핸들링 (Error Handling), UI 품질, 문서, 테스트 망라성 등
- 자기 평가 데이터 (Self-evaluation Data): AI 에이전트 스스로에 의한 평가나 메모
초기 데이터 구조는 defaultAgentData로 정의되어 있습니다. 실험 A와 실험 B의 데이터 구조가 동일한 에이전트 단위로 매달리는 형태입니다.
const defaultAgentData = (agentId) => ({
agent_id: agentId,
experiments: {
...
포인트는 정성 평가가 처음부터 human과 ai_self로 나누어져 있다는 점입니다.
qualitative:{
readability:{human:null,ai_self:null},
error_handling:{human:null,ai_self:null},
...
AI 에이전트 비교에서는 자기 평가를 그대로 랭킹에 섞으면, 과대평가나 과소평가 경향이 보이지 않게 됩니다. 그래서 인간 평가와 자기 평가를 동일한 항목으로 유지하면서도, 저장·임포트(Import) 시에는 섞이지 않도록 하고 있습니다.
이번 용도에서는 복잡한 프론트엔드(Frontend) 앱을 만드는 것보다, 실험 데이터를 즉시 볼 수 있는 것이 중요했습니다.
HTML 1장 구성으로 하면 다음과 같은 이점이 있습니다.
- GitHub Pages에 그대로 둘 수 있음
- npm install 없이 열 수 있음
- 실험 로그와 함께 리포지토리(Repository) 관리가 용이함
- 데이터 입력자가 환경 구축에서 막히지 않음
- 브라우저만으로 localStorage에 저장할 수 있음
한편, 제약 사항도 있습니다.
- 타입 체크(Type Check)가 없음
- 컴포넌트(Component) 분할이 어려움
- 데이터량이 늘어나면 HTML이 비대해짐
- CDN에 의존함
실제로 이 dashboard.html은 EXP_D_DATA나 EXP_E_DATA의 확정 데이터도 포함하고 있기 때문에, 1개 파일로서는 크기가 좀 큽니다. 다만, 실험용 대시보드로서는 "작동하는 파일이 1개"라는 장점이 더 크다고 판단하고 있습니다.
HTML의 선두에서 Vue 3와 Chart.js를 CDN으로부터 불러오고 있습니다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
Vue 측은 Composition API를 사용하고 있습니다.
const {createApp,ref,computed,onMounted,watch,nextTick} = Vue;
setup()
setup()
내에서는 화면 상태, 탭 상태, 입력 폼, 그래프 참조, 토스트 표시 등을 한데 모아 정의하고 있습니다.
createApp({
setup(){
const view = ref('overview');
...
화면 전환도 라우터(Router)가 아니라, 단순한 view 값으로 제어하고 있습니다.
<div v-for="item in viewNav" :key="item.id" class="nav-item" :class="{active:view===item.id}" @click="view=item.id">
<span class="nav-dot"></span>{{ item.label }}
</div>
이 정도 규모라면 Vue Router를 넣지 않아도, v-if="view==='overview'"와 같은 화면 전환만으로 충분합니다.
npm이나 webpack을 사용하지 않음으로써 배포는 매우 간단해집니다.
dashboard.html을 브라우저에서 열면 바로 동작합니다. GitHub Pages에서도 정적 파일로 올려두기만 하면 됩니다.
다만, 다음과 같은 제약 사항은 받아들여야 합니다.
- import/export 구문을 사용하지 않음
- Vue 단일 파일 컴포넌트 (Single File Component)를 사용하지 않음
- ESLint나 Prettier 적용이 수동이 되기 쉬움
- 문자열이나 구조가 1개 파일에 집중됨
이번 사례처럼 실험 데이터를 시각화하는 사내 도구, 개인 도구, 검증용 대시보드라면 이러한 타협은 상당히 유효합니다.
이 dashboard.html에서 Chart.js를 사용하고 있는 부분은 구현상 막대그래프 (Bar Chart)입니다.
new Chart(...)는 다음 4곳에서 사용되고 있습니다.
- 실험 E의 평균 평가 그래프
- 실험 E의 균질화 트랩 (Homogenization Trap) gap 그래프
- 실험 E의 탐지 기여도 랭킹
- 실험 A/B의 토큰 사용량 그래프
기사 구성에는 '레이더 차트 (Radar Chart)'라는 항목이 있지만, 실제 dashboard.html에는 Chart.js의 type: 'radar'는 존재하지 않습니다.
확인할 수 있는 Chart.js 생성 지점은 모두 type: 'bar'입니다.
expEAvgChartInstance = new Chart(avgCanvas, {
type: 'bar',
expEGapChartInstance = new Chart(gapCanvas, {
type: 'bar',
expEDetectChartInstance = new Chart(detectCanvas, {
type: 'bar',
const chart = new Chart(canvasRef, {
type: 'bar',
즉, 이 대시보드는 '다축의 정성 평가를 레이더 차트로 보여주는' 방향이 아니라, 표, 막대그래프, 진행 바(Progress Bar), 차이 표시를 통해 평가를 보여주는 설계로 되어 있습니다.
이는 실험 데이터의 성격과도 맞습니다. 6개 에이전트의 평가에서는 종합적인 형태를 훑어보는 것보다, 어떤 실험에서 몇 점이었는지, 어떤 리뷰어가 누구를 어떻게 평가했는지, 자기 평가와 인간 평가가 얼마나 차이 나는지를 확인하는 것이 더 중요했습니다.
실험 E에서는 피리뷰(Peer Review) 대상의 평균 평가를 실험 A/B에서 비교하는 막대그래프를 그리고 있습니다.
const avgCanvas = document.getElementById('expEAvgChart');
if (avgCanvas) {
if (expEAvgChartInstance) expEAvgChartInstance.destroy();
...
긴 함수에서 발췌한 것이지만, 포인트는 다음과 같습니다.
document.getElementById('expEAvgChart')로 canvas를 취득- 기존 인스턴스가 있다면
destroy()수행 agents.value.map(...)으로 6개 에이전트 분량의 데이터 배열 생성- A/B의 2개 계열을 나란히 배치
- y축은 0에서 10으로 고정
destroy()를 한 뒤에 다시 만드는 이유는, Vue의 화면 전환이나 재렌더링 시에 Chart.js 인스턴스가 중복되지 않도록 하기 위해서입니다.
토큰 사용량 그래프도 막대그래프입니다. 이 그래프는 입력 토큰과 출력 토큰을 누적(Stacked)하여 표시하고 있습니다.
const renderTokenChart = (exp) => {
nextTick(() => {
const canvasRef = exp === 'A' ? chartTokensA.value : chartTokensB.value;
...
이 부분도 발췌된 내용입니다. nextTick()을 사용하는 이유는 Vue가 canvas를 DOM에 반영한 후에 Chart.js를 초기화하기 위해서입니다.
진행 바(Progress bar)는 Chart.js가 아니라, CSS와 Vue의 :style을 사용하여 구현했습니다.
개요 화면의 랭킹에서는 점수를 너비(width)로 변환하고 있습니다.
<div class="rank-bar-w">
<div class="pb-bg"><div class="pb" :style="{width:ag.totalScore+'%',background:'var(--accent)'}"></div></div>
</div>
실험 진행 상황도 같은 방식입니다.
<div class="pb-bg"><div class="pb" :style="{width:(exp.done/exp.total*100)+'%',background:exp.color}"></div></div>
CSS 측 코드는 매우 짧습니다.
.pb-bg{background:var(--border);border-radius:4px;height:5px;overflow:hidden}
.pb{height:100%;border-radius:4px;transition:width .5s ease}
진행 바 정도라면 Chart.js를 사용하지 않고, DOM과 CSS로 처리하는 것이 더 가볍고 상태(state)와의 대응도 읽기 쉽습니다.
데이터 영속화(Persistence)는 localStorage를 사용합니다.
키(Key)는 고정되어 있으며, ai-exp-data-v1입니다.
const STORAGE_KEY = 'ai-exp-data-v1';
불러오기(Load) 처리는 localStorage에 데이터가 있으면 JSON으로서 복원하고, 없으면 6개 에이전트 분량의 초기 데이터를 생성합니다.
const loadStore = () => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
...
try/catch로 감싸져 있기 때문에, JSON이 손상되었더라도 초기 데이터로 되돌릴 수 있습니다.
저장할 때는 입력 폼의 값을 현재 선택 중인 에이전트 및 실험에 반영한 후 saveStore()를 호출합니다.
const saveInput = (exp) => {
const agId = inputAgent.value;
if(!store.value[agId]) store.value[agId] = defaultAgentData(agId);
...
여기서 중요한 점은, 입력 화면에서 저장되는 정성 평가(Qualitative evaluation)는 human 측 데이터라는 것입니다.
expData.qualitative[item.key].human = inputQual.value[item.key];
AI 자기 평가(AI self-evaluation)는 후술할 JSON 임포트(Import) 시 ai_self만 가져오도록 설계되어 있습니다.
이 대시보드에는 에이전트별 JSON을 다운로드하는 기능이 있습니다.
const exportJson = (agentId) => {
const data = store.value[agentId] || defaultAgentData(agentId);
const blob = new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
...
파일명은 ${agentId}.json입니다. 예를 들어 codex-cli라면 codex-cli.json이 됩니다.
단, 임포트 처리는 파일명이 아니라 JSON 내부의 agent_id 또는 meta.agent_id를 확인합니다.
const data = JSON.parse(ev.target.result);
const agentId = data.agent_id || data.meta?.agent_id;
if(!agentId) { showToast('❌ agent_id를 찾을 수 없습니다'); return; }
즉, 구현상의 정확한 사양은 다음과 같습니다.
-
내보내기(Export) 시의 파일명은
agentId.json -
가져오기(Import) 시의 식별자는 JSON 내부의
agent_id
또는 meta.agent_id
- 파일명과 내용의 일치 여부는 확인하지 않음
가져오기 처리에서 가장 중요한 것은 human을 덮어쓰지 않는 것입니다.
// ai_self만 병합 (Merge)
const exps = data.experiments || {};
Object.keys(exps).forEach(exp => {
...
자기 평가(Self-evaluation) JSON을 나중에 가져오는 운용 방식에서는, 인간이 확정한 평가를 AI 측의 JSON이 망가뜨리는 것이 가장 위험합니다.
따라서 이 코드에서는 가져오는 대상을 다음 두 가지로 한정하고 있습니다.
qualitative.*.ai_self
notes.ai_self
human은 가져오지 않습니다.
이 설계를 통해 "인간 평가를 먼저 입력해 두고, 나중에 AI 자기 평가만 추가하는" 운용이 가능합니다. 자기 평가의 편향(Bias)을 확인하기 위해서라도, 인간 평가와 AI 자기 평가를 섞지 않는 것이 중요합니다.
이 대시보드에서는 모든 데이터가 localStorage에 있는 것은 아닙니다.
실험 D와 실험 E처럼, 이미 확정된 평가 데이터는 HTML 내의 상수로 임베드(Embed)되어 있습니다.
// 실험 D(타자 테스트 수정)의 확정 데이터
// 데이터 소스: Claude에 의한 샌드박스 실기 검증 (30 세션 전건)
const EXP_D_DATA = {"meta": {"description": "실험 D(타자 테스트 수정) 평가 데이터", "target_correspondence_note": "target 번호와 실제 에이전트 명의 대응은 target-correspondence.md(비공개)에서 관리. 이 JSON 내의 키는 실제 에이전트 명으로 기록한다.", "rule_summary": "각 에이전트에게 자신 이외의 5개 에이전트가 구현한 task-app(target)을 전달하고, 공통 테스트 원본(test_api.py 18개·test_ui.py 6개)을 해당 target의 구현에 맞춰 수정하게 했다. 테스트 관점·기대 HTTP 상태 코드·개수 변경은 금지."}, "vendor_map": {"claude-code": "Anthropic", "codex-cli": "OpenAI", "codex-ide": "OpenAI", "antigravity-cli": "Google", "antigravity-ide": "Google", "copilot-agent": "Microsoft"}, "sessions": [
위는 도입부의 발췌입니다. EXP_D_DATA는 세션 배열과 집계된 by_modifier를 가지고 있습니다.
Vue 측에서는 이 상수를 ref에 넣어 표시용으로 사용합니다.
const expDData = ref(EXP_D_DATA);
랭킹은 EXP_D_DATA.by_modifier로부터 산출합니다.
const expDRanking = computed(() => {
return AGENTS.map(ag => {
const stat = expDData.value.by_modifier[ag.id] || { pass_rate_pct: 0, total_pass: 0, total_total: 0, violations_count: 0 };
...
실험 E도 마찬가지입니다.
const expEData = ref(EXP_E_DATA);
const expEExp = ref('A'); // 매트릭스 뷰를 위한 'A' 또는 'B'
이러한 분리에는 장점이 있습니다.
- 확정된 평가 데이터는 localStorage 리셋의 영향을 받지 않음
- 편집 중인 A/B 입력 데이터와는 수명(lifecycle)을 분리할 수 있음
- Git으로 차이(diff) 관리가 용이함
- 대시보드를 여는 것만으로 확정된 D/E 결과는 반드시 볼 수 있음
실험용 대시보드에서는 "아직 입력 중인 데이터"와 "확정된 참조 데이터"를 나누는 것만으로도 사고를 상당히 줄일 수 있습니다.
이 대시보드에는 자기 평가(self-evaluation)와 인간 평가(human evaluation) 사이의 차이를 보기 위한 헬퍼(helper)가 있습니다.
const humanAvg = (ag, exp) => {
const q = getAgentStore(ag.id).experiments?.[exp]?.qualitative;
if(!q) return '—';
...
humanAvg()
와 selfAvg()
를 별도로 계산하고, gapVal()
로 차이를 산출합니다.
이렇게 설계해 두면 단순히 평균 점수를 보는 것뿐만 아니라, 다음과 같은 관점에서 분석할 수 있습니다.
- 자기 평가가 항상 높은 에이전트는 누구인가
- 인간 평가보다 자기 평가가 낮은 에이전트가 있는가
- 실험 A와 B에서 자기 평가의 경향이 변하는가
- 리뷰 품질과 자기 인식(self-awareness) 사이에 관계가 있는가
AI 에이전트 비교에서는 최종 점수 그 자체보다 "어떤 종류의 차이가 발생하는가"가 더 중요한 경우가 있습니다. 그렇기 때문에 처음부터 human
과 ai_self
를 분리한 데이터 구조로 만들었습니다.
실험 E에서는 에이전트끼리 서로 코드 리뷰를 수행한 결과를 다룹니다.
리뷰 평가는 매트릭스(matrix) 형태로 표시됩니다.
<td v-for="target in agents" :key="reviewer.id+'-'+target.id"
:style="matrixCellStyle(reviewer.id, target.id)">
<template v-if="reviewer.id === target.id">
...
셀의 배경색은 점수에 따라 진해집니다.
const matrixCellStyle = (reviewerId, targetId) => {
if (reviewerId === targetId) return {};
const score = matrixScore(reviewerId, targetId);
...
여기서는 Chart.js가 아니라 테이블 셀의 배경색을 이용해 히트맵(heatmap) 스타일로 보여줍니다.
또한, 동일 계열 벤더(vendor)끼리 평가가 관대해질 가능성을 확인하기 위해 homogenization_trap_analysis라는 확정 데이터를 가지고 있습니다.
표시를 위해 A/B 각각을 행(row)으로 전개합니다.
const homogenizationRows = computed(() => {
const rows = [];
expEData.value.homogenization_trap_analysis.cases.forEach((c) => {
...
이 gap
은 가로 막대 그래프로도 표시합니다.
expEGapChartInstance = new Chart(gapCanvas, {
type: 'bar',
data: {
...
indexAxis: 'y'
를 지정하여 가로 막대 그래프로 만들었습니다. gap이 양수이면 붉은 계열, 음수이면 초록 계열입니다.
이 대시보드는 HTML 파일 한 장이므로, GitHub Pages로 공개할 때도 특별한 빌드(build)가 필요 없습니다.
최소 구성은 다음과 같습니다.
- 리포지토리에
dashboard.html을 둔다 - GitHub의 Settings에서 Pages를 활성화한다
- 대상 브랜치와 디렉토리를 선택한다
https://<사용자명>.github.io/<리포지토리명>/dashboard.html로 접속한다 (<...> 부분은 자신의 GitHub 사용자명과 리포지토리명으로 바꿔주세요. 본 실험의 리포지토리는 GitHub Pages를 활성화하지 않았으므로, 이 URL은 일반적인 이용 예시입니다)
CDN을 사용하고 있기 때문에 GitHub Pages 상에서도 Vue와 Chart.js는 그대로 로드됩니다.
주의할 점은 localStorage입니다. localStorage는 브라우저 오리진(origin) 단위이므로, GitHub Pages URL에서 저장한 데이터와 로컬 파일로 열었을 때의 데이터는 공유되지 않습니다.
또한, localStorage의 데이터는 브라우저 측에만 존재하므로, 중요한 입력 데이터는 JSON 내보내기 (JSON export)를 통해 백업하는 운용이 필요합니다.
마지막으로, 실제 코드를 읽으면서 신경 쓰였던 점도 적어둡니다.
랭킹 계산은 다음과 같이 되어 있습니다.
const rankedAgents = computed(() => {
return [...agents.value].map(ag => ({
...ag,
...
이 식은 연산자 우선순위에 주의가 필요합니다.
||0*8
은 "qualAvg가 없으면 0으로 만들고, 그것을 8배 한다"라는 의미가 아닙니다. +나 *의 평가 후에 ||가 작동하기 때문에, 의도가 있다면 괄호를 명시하는 것이 좋은 부분입니다.
이 기사는 실제 코드의 해설이 목적이므로 수정안까지는 다루지 않겠지만, 대시보드의 스코어 계산 로직은 표시하기 전에 테스트나 주석을 통해 의도를 고정해 두는 것이 안전합니다.
이 대시보드는 Vue 3 CDN, Chart.js, localStorage만으로 만든, AI 에이전트 비교용 정적 HTML 앱입니다.
구현상의 특징은 다음과 같습니다.
- 6개의 에이전트를 고정 배열로 관리함
- 편집 가능한 데이터는 localStorage에 저장함
- 확정된 데이터는
EXP_D_DATA,EXP_E_DATA로서 HTML에 임베드함 - 인간 평가와 AI 자기 평가를
human/ai_self로 분리함 - JSON 가져오기 (JSON import) 시에는
ai_self만 병합하여 인간 평가를 보호함 - Chart.js는 막대그래프(bar chart)로 한정하고, 진행 바(progress bar)나 히트맵(heatmap)은 CSS와 테이블로 구현함
- GitHub Pages에 그대로 올릴 수 있음
본격적인 웹 앱으로 만든다면 컴포넌트 분할, 타입 정의, 데이터베이스화 등의 선택지가 있습니다.
다만, 실험용 시각화 도구로서는 HTML 한 장으로 완결되는 구성이 상당히 강력합니다. 특히 "데이터 구조를 즉시 바꾸고 싶다", "브라우저에서만 입력하고 싶다", "GitHub Pages로 공유하고 싶다"라는 용도에서는 Vue CDN + Chart.js + localStorage의 조합은 충분히 실용적이었습니다.
본 기사는 6개의 AI 코딩 에이전트 비교 실험 시리즈 중 하나입니다 (Qiita 제8회).
시리즈 전체 기사 목록은 GitHub 리포지토리를 참조해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기