frontbench.app | benchmark-kit GitHub
GitHub - jhste102lab/frontbench-benchmark-kit: Prompt contracts, scenarios, rubric, and validation tools for evaluating AI-gener
Prompt contracts, scenarios, rubric, and validation tools for evaluating AI-generated frontend product screens. - jhste102lab/frontbench-benchmark-kit
github.com
FrontBench - AI Frontend Comparison Benchmark
Which AI buildsthe best frontend? Compare frontends generated from the same prompt and vote.Your evaluation decides the best frontend. View Latest Benchmark›See Methodology
frontbench.app
목차
만든 이유
AI 모델 성능을 비교하는 지표는 이미 넘쳐난다. LMSYS Chatbot Arena, HumanEval, SWE-bench, 각종 MMLU 리더보드.
그런데 이것들이 공통적으로 보여주는 건 결국 점수와 순위다. "이 모델이 저 모델보다 코딩을 X% 잘한다"는 식의 숫자.
그 숫자가 틀렸다는 게 아니다. 다만 내가 알고 싶었던 건 그게 아니었다.
실제로 AI한테 UI를 만들어달라고 할 사람이 가장 먼저 궁금한 건 "어떻게 생겼는데"다. GPT-5가 만든 SaaS 대시보드랑 Claude가 만든 대시보드가 눈으로 봤을 때 어떻게 다른지, 레이아웃이 제대로 잡혀 있는지, 버튼이 실제로 작동하는지, 모바일에서 깨지진 않는지.
이걸 직접 보여주는 곳이 없었다. 비교하려면 각각 API를 써보거나 서비스마다 직접 프롬프트를 넣어봐야 했다.
동일한 프롬프트를 주요 모델들에게 던지고, 각 모델이 만든 결과물을 그대로 한 곳에 모아서 보여주면 된다는 생각에서 시작했다. 숫자 대신 결과물을 직접 놓고 비교하는 것.
주요 기능
결과물 직접 보기
각 모델이 프롬프트를 받고 생성한 HTML 파일을 iframe으로 렌더링해서 보여준다.
실제 브라우저에서 돌아가는 결과물이기 때문에 레이아웃이 어떻게 생겼는지, 반응형이 제대로 되는지, 버튼을 누르면 뭔가 일어나는지 직접 확인할 수 있다.
시나리오는 4가지다. SaaS 대시보드(InvoicePulse), 쇼핑몰(Northstar Goods), 커뮤니티(MakerCircle), 브랜드 사이트(Auralite). AI로 UI를 만들 때 가장 흔히 요청되는 유형이라고 판단했다.
각 시나리오에는 구체적인 앱 이름과 포함해야 할 기능 목록이 명시되어 있다. 막연하게 "대시보드 만들어"라고 하면 모델마다 해석이 너무 달라지기 때문이다.
현재 Anthropic, OpenAI, Google, DeepSeek, xAI, Qwen, MiniMax 등 주요 제공사의 모델 55개 이상이 등록되어 있다.
투표와 순위
각 결과물에 좋아요/싫어요를 남길 수 있고, 투표 결과가 순위에 반영된다.
순위 산정 방식은 (좋아요 - 싫어요) 스코어 기준이고, 동점이면 절대적인 좋아요 수가 높은 쪽이 앞선다. 투표가 하나도 없는 모델은 순위에서 제외되고 별도로 표시된다.
투표는 변경하거나 취소할 수 있다. 같은 결과물에 다시 투표하면 이전 선택이 덮어쓰인다.
익명 처리 방식
투표에 로그인이 없어서 중복 방지와 프라이버시를 어떻게 처리할지가 문제였다.
중복 방지는 IP 주소와 User-Agent를 서버 측 salt 값과 함께 SHA-256으로 해싱해서 voter_hash를 만드는 방식으로 처리했다. 같은 환경에서 같은 결과물에 투표하면 UPSERT로 처리되기 때문에 중복 카운트가 되지 않는다.
voter_hash 자체가 개인을 특정하지는 않지만, 그래도 영구적으로 DB에 남겨두고 싶지 않았다.
그래서 투표 시 voter_hash와 별개로 완전히 랜덤한 public_id를 생성해서 같이 저장한다. 이 public_id는 투표 내역 화면에서 "AB-12345678" 같은 형식으로 표시되는 익명 식별자다. voter_hash에서 파생되지 않고 독립적으로 생성되기 때문에, 나중에 voter_hash가 삭제되어도 "어떤 사람이 투표했다"는 표시 자체와는 연결되지 않는다.
voter_hash와 public_id는 투표 후 15분이 지나면 DB에서 null로 덮어써진다. 집계 수치(like, dislike 카운트)는 유지되지만, 누가 투표했는지는 15분 후에 영구적으로 사라진다.
별도 배치 작업 없이 투표 API가 호출될 때마다 만료된 레코드를 정리하는 방식으로 처리했다.
필터 구조
결과물이 55개 이상이다 보니 원하는 걸 찾으려면 필터가 필요했다. 시나리오, 제공사, 모델 세 가지 축으로 필터링할 수 있고, 텍스트 검색도 지원한다.
필터 구조에서 신경 쓴 부분은 각 필터 항목 옆에 표시되는 카운트다. 다른 축에서 이미 선택된 항목을 고려해서 실제로 필터링됐을 때 남는 결과 수를 보여준다.
예를 들어 "Google" 제공사를 선택한 상태에서 시나리오 필터를 보면, 각 시나리오별로 Google 모델이 몇 개 있는지가 표시된다. 자신을 제외한 나머지 선택 조건을 모두 반영한 카운트다.
구현 구조
아키텍처 — 서버 없이 정적으로
사이트에 서버가 없다. 벤치마크 러너가 GitHub Actions 위에서 OpenRouter를 통해 각 모델에 프롬프트를 전송하고, 받아온 HTML 결과물과 메타데이터를 JSON 파일로 만들어 레포에 커밋한다. 사이트는 그 데이터를 읽어서 Next.js로 정적 빌드하고 Cloudflare Pages에 업로드한다.
[benchmark-kit / GitHub Actions]
│ OpenRouter API 호출
│ 결과 JSON 커밋
▼
[frontbench 레포]
│ Next.js 정적 빌드 (next export)
▼
[Cloudflare Pages] — 서버 없음, 투표 API만 Worker로
좋아요/싫어요 투표만 상태가 필요하기 때문에, 그 부분만 Cloudflare Workers + D1(SQLite)로 처리한다. 나머지는 전부 정적이다.
이렇게 구성한 이유는 단순하다. 트래픽이 많을 것 같지 않고, 유지 비용을 최소화하고 싶었다. Cloudflare Pages 정적 호스팅은 무료고, 벤치마크 러너는 메인 레포와 별도로 공개 레포로 분리했다.
초기 지출은 도메인 15달러, API 비용 20달러. 총 35달러. 모델 한 세트를 돌리는 데 10~20분이 걸린다.
프롬프트 설계
모델 간 비교를 의미 있게 만들려면 모든 모델이 동일한 조건에서 결과물을 만들어야 한다. 기준이 없으면 어떤 모델은 마케팅 랜딩 페이지를, 어떤 모델은 실제 앱 화면을 만들어도 같은 프롬프트에 대한 응답이 된다.
그래서 시나리오 설명과 함께 공통 요구사항을 명시했다.
- 파일 하나(
index.html)로. 외부 스크립트, 폰트, 이미지, iframe 없음 - 첫 화면이 실제 사용 가능한 프로덕트 화면이어야 함. 마케팅 페이지 금지
- 필터, 탭, 정렬, 검색 같은 인터랙티브 컨트롤 최소 3개, 실제로 UI가 업데이트되어야 함
- 데스크탑과 390px 모바일 너비 모두 지원, 가로 스크롤 없음
- lorem ipsum 없음. 도메인에 맞는 실제 데이터
- 응답은
<!doctype html>로 시작하는 HTML 문서만. Markdown 펜스 금지
이 요구사항이 있어야 비교가 된다. 없으면 각 모델이 "잘 만든 것"의 기준 자체를 다르게 잡는다.
자동 품질 검사
눈으로 보는 것 외에, 모든 결과물에 자동 체크를 돌린다. Playwright로 headless Chromium을 띄워서 실제 브라우저 환경에서 다섯 가지를 확인한다.
- 첫 뷰포트가 비어 있지 않은가 — 아무것도 안 보이는 빈 화면인지 확인
- 모바일(390px)에서 가로 스크롤이 없는가 — 반응형 대응 여부
- 인터랙티브 컨트롤이 3개 이상 보이는가 — 버튼, 필터, 탭 등 실제로 화면에 보이는 것 기준
- 컨트롤을 클릭하면 UI가 실제로 바뀌는가 — JavaScript가 제대로 작동하는지
- 런타임 에러가 없는가 — 콘솔 에러 포함
이 검사들을 통과하지 못한 결과물에는 표시가 붙는다.
아예 빈 화면을 돌려주거나, HTML은 있는데 JavaScript가 동작하지 않는 케이스도 있어서, 이 정보가 단순 시각 비교보다 객관적인 지표가 된다.
벤치마크 파이프라인 흐름
러너가 한 번 돌 때 진행 순서는 대략 이렇다.
- 대상 모델 목록과 시나리오를 읽어서 실행 계획 수립
- 이미 완료된 항목은 audit 파일을 보고 건너뜀 (재실행 안전)
- OpenRouter API 호출 → generation ID를 즉시 audit 파일에 기록
- 응답에서 HTML 추출, Playwright로 자동 검사 실행
- HTML 파일과 메타데이터(비용, 토큰 수, 검사 결과 등) 저장
- benchmark-results.json 업데이트 → 레포 커밋
러너가 중간에 죽어도 audit 파일이 남아 있기 때문에 어느 시점에서 멈췄는지 파악하고 이어서 실행할 수 있다.
예상하지 못한 것들
1. API 비용은 나갔는데 결과물이 없다
가장 황당했던 케이스. OpenRouter 호출 성공, 크레딧 차감, 그런데 로컬에 HTML 파일이 없다.
초기 파이프라인은 모델을 순서대로 처리하면서 결과를 그때그때 파일로 쓰는 구조였다. 문제는 네트워크 오류나 프로세스 중단이 생기면 그 시점 이후의 결과물이 전부 날아간다는 거였다.
더 나쁜 건, "API 호출이 완료됐는지"와 "파일이 저장됐는지"가 완전히 별개였다는 것이다. 돈은 나갔는데 손에 쥔 게 없는 상황.
Claude Fable 5가 정확히 이 케이스였다. 4개 시나리오 모두 OpenRouter 쪽에는 호출 기록이 있었는데, 로컬 파일이 하나도 없었다.
OpenRouter는 각 생성 요청에 generation ID를 부여한다. 이 ID로 /api/v1/generation?id=... 엔드포인트를 치면 나중에도 결과물을 다시 가져올 수 있다. 그래서 두 가지 방향으로 파이프라인을 수정했다.
첫 번째는 audit 로그다. API 호출이 완료되면 파일 저장 여부와 무관하게 generation ID를 즉시 별도 파일에 기록한다. 이후에 뭔가 날아가도 어떤 호출이 완료됐는지 파악할 수 있다.
두 번째는 복구 스크립트다. audit 파일에서 generation ID 목록을 읽어서, 로컬에 파일이 없는 항목만 골라 OpenRouter에서 HTML을 재수집하고 저장한다. Fable 출력은 이 스크립트로 전부 복구했다.
2. CI 워크플로우가 조용히 멈춘다
GitHub Actions에서 벤치마크를 돌리는데 워크플로우가 실행 중에 멈췄다. 오류 메시지 없음, 타임아웃 없음, 그냥 정지.
특정 모델 응답이 너무 오래 걸리는 건가 싶어서 타임아웃 설정을 먼저 봤다. 아니었다. 특정 스텝 이전까지는 정상적으로 진행되다가 그 이후에서 멈추는 패턴이 있었다. 캐시 쪽을 의심했다.
원인은 이전 실행에서 남은 node_modules 캐시였다. 워크플로우에서 캐시를 활성화해두고 있었는데, 오래된 캐시가 러너 실행에 간섭하고 있었다.
캐시 설정 두 줄을 제거했더니 이후로는 재현되지 않았다. 원인 추적에 시간이 걸렸고 해결은 두 줄 삭제였다.
3. 한국어 메일이 서버에서 사라진다
컨택트 폼에서 메일을 보내면 받은편지함에 아무것도 오지 않았다. 발송 API는 성공 응답을 반환하고 있었다.
Cloudflare Workers에서 네이버 SMTP로 메일을 보내는 구조였는데, SMTP 서버도 250 OK를 반환했다. 스팸 폴더에도 없었다. 수신 자체가 없었다.
쓰고 있던 라이브러리(worker-mailer)를 파봤다. 이 라이브러리는 메일 본문을 quoted-printable 방식으로 인코딩한다. RFC 표준을 따르는 방식이지만, 네이버 SMTP가 멀티바이트 UTF-8 문자를 quoted-printable로 받으면 250 OK는 응답하면서 메시지를 조용히 버리고 있었다.
단독 진단 스크립트로 인코딩 방식을 바꿔가며 직접 테스트했다. Base64로 본문을 인코딩하면 정상 도착한다는 걸 확인했다.
Cloudflare Workers 환경에서 Base64 인코딩을 제대로 지원하는 SMTP 라이브러리가 마땅치 않아서, 결국 Workers의 cloudflare:sockets API로 TCP 소켓을 직접 여는 SMTP 클라이언트를 새로 만드는 방향으로 해결했다.
STARTTLS 핸드셰이크, 인증, Base64 본문 전송, 한국어 제목과 발신자 이름을 위한 RFC 2047 헤더 인코딩까지 전부 들어가야 했다. 라이브러리 없이 SMTP 레벨까지 내려가게 될 거라고는 생각하지 못했다.
그 이후로 한국어 메일은 정상 도착한다.
4. 모델이 늘어날수록 번들이 커진다
모델 수가 늘어나면서 페이지 로딩 성능이 무거워졌다. 원인을 찾아보니 클라이언트 컴포넌트에 BenchmarkModel 객체를 통째로 직렬화해서 내려보내고 있었다.
이 객체에는 각 모델에 사용된 프롬프트 전문(全文)이 포함되어 있었고, 그게 모델 수 × 시나리오 수만큼 번들에 중복으로 들어가고 있었다.
해결 방향은 두 가지였다. 화면별로 실제로 필요한 필드만 담은 슬림 타입을 분리하고, 프롬프트 전문처럼 자주 보지 않는 데이터는 별도 API 라우트(/model-details/[modelKey])로 분리해서 필요할 때만 가져오도록 했다.
이 작업을 하면서 성능 게이트도 함께 추가했다. 클라이언트 JS 청크는 500KB, 샘플 HTML 파일은 250KB를 넘으면 빌드가 실패한다. 이 게이트가 없으면 조용히 번들이 불어나는 걸 나중에야 발견하게 된다.
마무리
"어떤 AI가 프론트엔드를 더 잘 만드나"라는 질문에서 출발했다.
지금은 55개 이상 모델의 결과물을 동일한 기준에서 비교할 수 있는 사이트가 됐다. 초기 지출은 35달러.
시나리오를 더 늘리거나 자동 평가 지표를 더 붙이는 것도 생각하고 있다. 지금은 직접 눈으로 보고 투표하는 것이 전부지만, 그게 처음에 원했던 것이기도 하다.
FrontBench - AI Frontend Comparison Benchmark
Which AI buildsthe best frontend? Compare frontends generated from the same prompt and vote.Your evaluation decides the best frontend. View Latest Benchmark›See Methodology
frontbench.app
github.com/jhste102lab/frontbench-benchmark-kit
GitHub - jhste102lab/frontbench-benchmark-kit: Prompt contracts, scenarios, rubric, and validation tools for evaluating AI-gener
Prompt contracts, scenarios, rubric, and validation tools for evaluating AI-generated frontend product screens. - jhste102lab/frontbench-benchmark-kit
github.com