backend/(FastAPI + Python)와 frontend/(React + Vite)의 실제 소스코드를 파일 단위로 읽고 정리한 것이다. 별점 수 같은 인기 지표는 다루지 않는다 — 코드가 실제로 어떻게 짜여 있는지에만 집중한다.
abi/screenshot-to-code · 제작 Abi Raja · 라이선스 MIT · 최신 커밋 b0d6ab8(2026-07-27) · 백엔드=FastAPI 0.115 + Python(Poetry) · 프론트엔드=React 18 + Vite 6 + TypeScript · 지원 스택 6종 · 백엔드 테스트 함수 약 260개)
screenshot-to-code를 한 문장으로 줄이면 이렇다: 사용자가 스크린샷(또는 화면 녹화 영상, 또는 텍스트 설명)을 올리면, 여러 개의 AI 모델이 동시에 후보 코드를 만들어 실시간으로 화면에 스트리밍해 주는 웹 앱이다. 프론트엔드는 React로 만든 채팅형 UI, 백엔드는 FastAPI로 만든 WebSocket 서버다.
다만 "AI가 이미지를 보고 코드를 뱉는다"는 설명은 절반만 맞다. 실제로는 모델이 파일 생성·수정·이미지 편집·자체 스크린샷 촬영 같은 도구(tool)를 스스로 호출하며 여러 턴에 걸쳐 작업하는 에이전트다. 한 번의 프롬프트로 끝나는 게 아니라, 만들고 → 스스로 찍어보고 → 어색하면 고치는 루프를 돈다.
단순한 이미지-투-텍스트 변환기라면 사진을 한 번 보고 붓질을 끝낸다. screenshot-to-code의 에이전트는 그림을 그린 뒤(create_file) 자기가 그린 그림을 실제로 렌더링해서 다시 눈으로 확인하고(screenshot_preview 도구 — 데스크탑·모바일 화면을 헤드리스 브라우저로 직접 찍어 본다), 어색한 부분이 있으면 부분만 고친다(edit_file).
게다가 원본 사진에 있던 로고나 아이콘은 대충 다시 그리지 않고, 사진에서 그 부분만 오려내(extract_assets) 재사용한다. "보고 베끼는" 수준을 넘어 "확인하고 고치는" 단계까지 들어간 게 이 프로젝트의 정체다.
backend/agent/ 폴더 전체가 이 루프를 구현한다.2023년 말 처음 공개됐을 때만 해도 이 프로젝트는 "GPT-4 Vision으로 스크린샷을 코드로 바꿔본다"는 단순한 실험에 가까웠다. 지금(2026년) 다시 주목받는 이유는 크게 세 가지로 나뉜다.
v0·Lovable·Bolt 같은 상용 서비스가 "말만 하면 앱이 나온다"는 경험을 대중화하면서, 디자인 이미지 한 장을 코드로 바꾸는 일은 더 이상 특이한 요청이 아니라 프론트엔드 개발의 표준적인 시작점이 됐다. screenshot-to-code는 이 흐름에서 오픈소스로 직접 돌릴 수 있는 몇 안 되는 선택지다.
Gemini 계열 모델의 멀티모달 능력이 좋아지면서, 이 저장소는 정지 이미지뿐 아니라 화면 녹화 영상을 그대로 앱으로 재현하는 기능과, 스크린샷 속 실제 로고·아이콘을 잘라내어 재사용하는 자산 추출 기능까지 갖췄다. "그럴듯하게 새로 그리기"가 아니라 "원본 그대로 재사용하기"가 품질 차이를 만든다.
backend/routes/model_choice_sets.py의 커밋 이력을 보면, 어떤 모델 조합을 기본값으로 쓸지가 "8개 브리프 × 7개 모델을 채점한 평가 세션" 같은 구체적인 근거로 몇 주 단위로 갱신된다. 개인 프로젝트 수준의 오픈소스치고는 드물게, 모델 선택 자체가 데이터로 검증되는 파이프라인(backend/evals/, backend/run_evals.py)을 갖추고 있다.
v0·Bolt 같은 서비스는 편하지만 남의 서버, 남의 모델에 묶인다. screenshot-to-code는 자신의 API 키(BYOK, bring-your-own-key)로 로컬에서 직접 돌릴 수 있어 코드와 이미지가 외부로 나가는 경로를 스스로 통제할 수 있다. 대신 인프라(스크린샷 렌더링용 브라우저 설치 등)를 직접 챙겨야 한다는 트레이드오프가 있다.
프론트엔드 개발에서 가장 지루하면서도 없앨 수 없는 단계가 "디자인 이미지를 보면서 픽셀 단위로 마크업을 손으로 옮겨 적는 일"이다. screenshot-to-code는 이 단계를 다음 세 가지 축으로 자동화한다.
custom_types.py의 InputMode가 정의하듯, 입력은 이미지(스크린샷·피그마 내보내기)뿐 아니라 텍스트만으로 새 화면을 생성하거나, 화면 녹화 영상을 그대로 재현하는 것까지 지원한다. "디자인 파일이 없어도, 동작하는 화면을 녹화한 영상만 있어도" 프로토타입을 만들 수 있게 문턱을 낮췄다.
frontend/src/lib/stacks.ts / backend/prompts/prompt_types.py에 정의된 Stack은 HTML+CSS, HTML+Tailwind, React+Tailwind, Vue+Tailwind, Bootstrap, Ionic+Tailwind 6종이다. 같은 디자인 한 장을 팀이 실제로 쓰는 프레임워크에 맞춰 바로 뽑아낼 수 있다.
생성은 한 번으로 끝나지 않는다. generation_type이 create(새로 생성)와 update(기존 코드 수정) 두 갈래로 나뉘고, 사용자가 미리보기 화면에서 요소 하나를 클릭해 선택하면 그 요소에 해당하는 코드 부분만 정확히 골라 고치는 "타겟 요소 편집"까지 지원한다(자세한 동작은 s5).
이 프로젝트의 뼈대는 하나의 WebSocket 연결 위에서, 여러 개의 AI 에이전트가 동시에 각자의 후보 코드를 만들어 스트리밍하는 구조다. 프론트엔드는 그 결과를 "변형(variant)" 탭으로 나란히 보여주고 사용자가 마음에 드는 것을 고른다.
이 구조에서 눈여겨볼 점은 "파이프라인은 순서(미들웨어 체인)를 책임지고, 에이전트는 각자의 turn(대화 순번)을 책임진다"는 역할 분리다. variant 하나가 실패해도(asyncio.gather(..., return_exceptions=True)) 나머지 variant는 그대로 진행된다 — 4개 중 1개가 API 오류를 내도 사용자는 나머지 3개 결과를 받아볼 수 있다.
이 절이 이 문서에서 가장 두꺼운 이유는, screenshot-to-code의 진짜 가치가 "어떤 모델을 쓰는가"가 아니라 "모델을 어떻게 감싸서 신뢰할 수 있는 코드를 뽑아내는가"에 있기 때문이다. 아래 순서대로 실제 파일과 함수를 따라가 본다.
backend/routes/generate_code.py는 체인 오브 리스판서빌리티(chain of responsibility) 패턴을 직접 구현한다. Middleware 추상 클래스(process(context, next_func))를 6개 이어 붙이고, Pipeline._wrap_middleware가 뒤에서부터 감싸 하나의 호출 체인을 만든다. 각 미들웨어는 PipelineContext라는 공유 상태 객체(dataclass)에 자기 결과를 채워 다음 단계로 넘긴다 — 예를 들어 ParameterExtractionMiddleware가 채운 context.extracted_params를 PromptCreationMiddleware가 읽는다.
실제 코드 생성은 backend/agent/engine.py의 AgentEngine._run_with_session이 담당한다. for _ in range(max_steps)(max_steps = 30) 루프를 돌며, 매 스텝마다 모델에게 스트리밍 응답을 받고(session.stream_turn(on_event)), 모델이 도구를 호출했으면 AgentToolRuntime.execute로 실제 실행한 뒤 그 결과를 다시 대화에 붙여(session.append_tool_results) 다음 스텝으로 넘어간다. 도구 호출이 없으면(=모델이 "이제 끝났다"고 판단하면) 루프를 종료하고 최종 코드를 반환한다.
여기에 두 가지 안전장치가 붙는다. 하나는 예산 한도다. config.py의 GENERATION_MAX_COST_USD = 3.0을 넘기면 BudgetExceededError를 던져 그 variant를 중단시킨다(다만 이미 답을 낸 턴은 "돈을 이미 썼으니" 막지 않는다 — 코드 주석에 그 의도가 명시돼 있다). 다른 하나는 빈 결과 방지다. 일부 모델(주석에 gemini-3.6-flash가 관찰 사례로 적혀 있다)이 이미지 도구만 쓰고 create_file은 호출하지 않은 채 끝나버리는 경우가 있는데, 이를 조용히 "성공"으로 넘기면 평가(evals)가 오염되므로 EmptyOutputError로 명시적 실패 처리한다.
"재료를 하나씩 갖다주는 조수를 둔 요리사"라고 생각하면 쉽다. 요리사(모델)는 "달걀 좀 가져와"라고 말하고(도구 호출), 조수(AgentToolRuntime)가 실제로 냉장고에서 꺼내 온다(도구 실행). 요리사는 달걀을 받아본 다음에야 다음 지시를 내린다. 이 왕복이 최대 30번까지 반복되고, 요리사가 "다 됐다"고 하면 멈춘다.
backend/prompts/pipeline.py의 build_prompt_messages는 곧바로 프롬프트를 만들지 않는다. 먼저 prompts/plan.py의 derive_prompt_construction_plan이 현재 상황(새 생성인지 수정인지, 히스토리가 있는지, 파일 스냅샷이 있는지)을 보고 세 가지 구성 전략 중 하나를 고른다.
| 전략 | 언제 | 담당 모듈 |
|---|---|---|
| create_from_input | 완전히 새로운 생성 (이미지/텍스트/영상) | prompts/create/{image,text,video}.py |
| update_from_history | 이전 대화 기록을 바탕으로 이어서 수정 | prompts/update/from_history.py |
| update_from_file_snapshot | 프론트가 현재 파일 내용을 스냅샷으로 함께 보낸 경우 | prompts/update/from_file_snapshot.py |
공통 시스템 프롬프트는 prompts/system_prompt.py의 SYSTEM_PROMPT 하나로 관리된다. 이 안에 스택별 분기가 그대로 들어 있다 — 예를 들어 React를 고르면 "react@18.0.0" UMD 스크립트와 babel.min.js를, Vue를 고르면 vue.global.js를 CDN으로 넣으라는 지시가 텍스트로 박혀 있다. 여기에 선택적으로 prompts/design_system.py가 사용자가 지정한 "디자인 시스템" 텍스트 블록을 덧붙인다("충돌하면 디자인 시스템을 우선하라"는 지시와 함께).
모델이 create_file로 HTML 전체를 만드는 동안, 사용자는 그 결과가 다 끝날 때까지 기다리지 않는다. AgentEngine._stream_code_preview는 이미 만들어진 내용을 최대 18조각(max_chunks = 18), 최소 200자 단위(min_step = 200)로 잘라 setCode 이벤트를 반복 전송한다. 도구 호출이 스트리밍 중(delta)일 때도 _handle_streamed_tool_delta가 40자 이상 늘어날 때마다 미리 화면을 갱신한다 — "다 만들고 나서 보여주기"가 아니라 "만드는 과정 자체를 보여주기"다.
backend/agent/providers/ 아래 base.py(ProviderSession, StreamEvent 공통 타입)와 factory.py(create_provider_session)가 세 회사의 서로 다른 스트리밍·도구 호출 API를 하나의 세션 인터페이스 뒤로 감춘다. AgentEngine 입장에서는 session.stream_turn(on_event)·session.append_tool_results(...)만 알면 되고, 실제로 어느 회사의 API를 부르는지는 anthropic/·openai.py·gemini.py 각 구현이 숨긴다. 새 모델 프로바이더를 추가할 때 에이전트 루프 코드를 건드릴 필요가 없는 구조다.
backend/agent/tools/screenshot_preview.py의 run_screenshot_preview는 Playwright로 띄운 헤드리스 Chromium에서 지금까지 만든 HTML을 데스크탑·모바일 두 뷰포트로 실제 렌더링해 스크린샷을 찍고, 그 이미지를 모델에게 멀티모달 입력으로 다시 보여준다. 코드 주석에는 "이 스크린샷은 보기 위한 것이지 저장하기 위한 것이 아니다 — 모델이 자기 작업을 확인하는 용도일 뿐, 결과물에 끼워 넣는 자산으로 저장하지 않는다"고 명시돼 있다. 시스템 프롬프트도 "레이아웃이 깨지거나 요소가 겹치면 edit_file로 고치라"고 이 도구 사용을 직접 지시한다.
텍스트만 보고 HTML을 생성하는 모델은 자기가 만든 CSS가 실제로 어떻게 보이는지 알 방법이 없다. 렌더링 결과를 다시 이미지로 넣어주는 이 한 수가, "그럴듯한 마크업"과 "실제로 안 깨지는 화면" 사이의 차이를 크게 좁힌다. Playwright/Chromium이 없는 환경에서는 이 도구 자체가 조용히 비활성화된다(preview_screenshot/registry.py의 probe_screenshot_preview가 가용성을 한 번 확인해 캐시한다).
backend/asset_extraction.py는 gemini-3.6-flash에 구조화 출력(Pydantic 스키마)을 강제해 쓴다. AssetDetection 모델은 요청 ID, 원본 이미지 중 몇 번째인지, box_2d(0~1000으로 정규화된 좌표), 라벨을 반환하도록 스키마로 못박는다. 이렇게 받은 좌표로 원본 스크린샷에서 그 영역만 실제로 잘라내(crop) 코드에 삽입한다 — 모델이 "로고 비슷한 이미지를 새로 생성"하는 게 아니라 "원본에 있던 그 로고 그대로"를 쓰게 만드는 장치다.
자산 자체를 새로 만들어야 할 때(원본에 없거나 배경에 가려진 경우)는 image_generation/generation.py의 process_tasks가 담당하며, 프롬프트를 최대 20개씩 묶어(REPLICATE_BATCH_SIZE = 20) asyncio.gather로 동시에 Replicate API(기본 이미지 모델, 코드 내 flux_2_klein 분기 포함)를 호출한다. 투명 배경이 필요하면 별도의 remove_background 도구가, 저해상도 확대가 필요하면 edit_image(CSS로 늘리지 말라고 시스템 프롬프트가 명시적으로 금지한다)가 처리한다.
prompts/create/video.py의 build_video_prompt_messages를 보면 흥미로운 점이 있다. 영상을 미리 프레임으로 분해하거나 편집하는 별도 처리 없이, 영상 데이터 URL을 그대로 image_url 타입의 메시지 파트에 담아 Gemini에 통째로 넘긴다. "영상 속 사용자 상호작용과 화면 전환을 그대로 관찰해서, 같은 상호작용에 같은 결과가 나오도록 기능까지 동작하게 만들라"는 지시가 프롬프트에 그대로 박혀 있다. 영상 모드는 Gemini API 키가 반드시 있어야 하고(routes/model_choice_sets.py의 VIDEO_VARIANT_MODELS — 두 개의 Gemini 모델만), variant 수도 2개로 고정된다(NUM_VARIANTS_VIDEO = 2).
routes/model_choice_sets.py는 겉보기엔 "어떤 상황에 어떤 모델을 쓸지" 정해둔 평범한 상수 목록이다. 그런데 각 목록 위 주석을 읽으면 "2026-07-27, 8개 브리프 × 7개 모델을 채점한 'Text evals v1' 세션 기준으로 교체" 같은 구체적 근거가 남아 있다. backend/evals/(core.py·runner.py·sessions.py·sets.py)와 run_evals.py가 이 평가를 실제로 돌리는 하니스다. API 키 조합(OpenAI/Anthropic/Gemini 중 몇 개를 가졌는지)과 입력 모드·생성 타입에 따라 ModelSelectionStage._get_variant_models가 적절한 모델 튜플을 골라, 모자라면 [A, B] 같은 목록을 num_variants만큼 순환(cycle)시켜 채운다.
정상적으로는 모델이 create_file 도구를 호출해 코드를 만들지만, 혹시 도구 대신 채팅 텍스트 안에 코드를 그대로 써버리는 경우를 대비해 codegen/utils.py의 extract_html_content가 <html>...</html> 태그를 텍스트에서 직접 찾아내는 폴백을 제공한다. AgentEngine._finalize_response가 이 폴백을 최후 수단으로 사용한다.
스택 이름만 나열하면 흔한 "FastAPI + React" 조합처럼 보이지만, 버전을 어디서 확정하는지가 실제로 중요하다. 이 저장소에서 버전의 진실원(source of truth)은 항상 README의 설명문이 아니라 lock 파일이다 — backend/poetry.lock + backend/pyproject.toml, frontend/pnpm-lock.yaml + frontend/package.json.
| 영역 | 패키지 / 버전 | 역할 |
|---|---|---|
| 웹서버 | FastAPI 0.115.6 · uvicorn 0.25 · websockets 14.1 | HTTP 라우트 + WebSocket 스트리밍 |
| LLM SDK | openai 2.16.0 · anthropic 0.84 · google-genai 1.16.1 | 세 프로바이더 각각의 공식 SDK |
| 렌더링 | playwright 1.61 | 자체 스크린샷 검증용 헤드리스 Chromium |
| 이미지 | pillow 10.3 + pillow-heif 0.18 | 이미지 처리, HEIC/HEIF(아이폰 사진) 디코딩 |
| 기타 명시 의존성 | moviepy 1.0.3 · langfuse 3.0.2 | pyproject.toml에 선언은 있으나 실제 소스에서 import된 곳이 없음(s9 참고) |
| 패키지 매니저 | Poetry (Python ^3.10) | poetry install / poetry run |
| 영역 | 패키지 / 버전 | 역할 |
|---|---|---|
| 프레임워크 | React 18.2 · Vite 6.4 · TypeScript 5 | UI 렌더링 + 빌드 |
| 상태관리 | Zustand 4.5 | store/app-store.ts, store/project-store.ts |
| 에디터·코드뷰 | CodeMirror 6 계열 · react-syntax-highlighter | 생성된 코드 표시/편집 |
| UI 컴포넌트 | Radix UI(Accordion·Dialog·Tabs 등) · TailwindCSS 3.3 | 디자인 시스템 프리미티브 |
| 테스트 | Jest 29 | src/tests/qa.test.ts 포함 9개 테스트 파일 |
| 패키지 매니저 | pnpm 10.32.1 (packageManager 필드로 고정) | 루트 package.json이 frontend·backend를 workspaces로 묶음 |
package.json·pyproject.toml)과, 실제로 설치될 정확한 버전 조합을 고정해 기록한 파일(pnpm-lock.yaml·poetry.lock). 같은 선언이라도 lock 파일이 없으면 설치 시점마다 하위 버전이 달라질 수 있어, "버전의 진실"은 항상 lock 파일 쪽이다.| 항목 | 내용 |
|---|---|
| 필수 키 | OPENAI_API_KEY · ANTHROPIC_API_KEY · GEMINI_API_KEY 중 최소 1개. 셋 다 있으면 모델을 더 잘 섞어 쓴다 |
| 권장 키 | GEMINI_API_KEY(자산 추출·영상 모드에 필요) · REPLICATE_API_KEY(이미지 생성·편집·배경 제거) |
| 백엔드 실행 | cd backend && poetry install && poetry run playwright install chromium && poetry run uvicorn main:app --reload --port 7001 |
| 프론트엔드 실행 | cd frontend && pnpm install && pnpm dev → http://localhost:5173 |
| Docker | 루트에서 docker-compose up -d --build(백엔드 7001 · 프론트엔드 5173). 파일 변경 시 자동 재빌드는 안 됨 |
| 하드웨어 | GPU 불필요 — 추론은 전부 외부 API 호출. Chromium 렌더링용 메모리만 여유 있으면 됨 |
Playwright의 playwright install chromium 단계를 건너뛰면 screenshot_preview 도구 자체가 조용히 비활성화된다(에러가 나지 않는다). 설정 다이얼로그(Settings)에 "Screenshot preview" 사용 가능 여부가 표시되니, 이걸로 확인하는 게 가장 빠르다.
또한 REPLICATE_API_KEY는 오직 backend/.env 파일로만 설정할 수 있다 — 다른 세 개 키와 달리 프론트엔드 설정 다이얼로그에서는 넣을 수 없다(README에 명시된 제약).
스크린샷 이미지, 화면 녹화 영상, 또는 텍스트 설명 중 하나를 넣고, 원하는 출력 스택(예: React+Tailwind)을 고른다. 기존 코드가 있으면 "이어서 수정" 모드로 들어간다.
프론트엔드가 ws.send(JSON.stringify(params))로 파라미터를 보내면, 서버는 variantCount 이벤트로 몇 개의 후보(보통 4개, 수정은 2개, 영상은 2개)를 만들지 먼저 알려주고, 그만큼의 에이전트를 동시에 돌린다.
thinking(모델의 사고 과정)·assistant(짧은 채팅 응답)·toolStart/toolResult(도구 호출 시작/결과)·setCode(코드 미리보기 갱신) 이벤트가 순서대로 흘러온다. 사용자는 4개 탭을 오가며 "지금 어느 모델이 뭘 하고 있는지"를 볼 수 있다.
variant 4개가 각각 variantComplete를 받으면 미리보기 화면에서 나란히 비교할 수 있다. 마음에 드는 걸 고른 뒤, 그 위에서 요소를 직접 클릭해 선택하면 선택된 요소의 실제 렌더링 결과(outerHTML)를 함께 보내 "이 버튼만 빨간색으로 바꿔줘" 같은 국소 수정을 요청할 수 있다 — 시스템 프롬프트가 "선택된 요소는 라이브 DOM에서 캡처된 것이라 소스 코드와 다를 수 있으니(JSX는 className을 쓰고, Vue는 디렉티브를 쓴다), 태그·클래스·텍스트 내용으로 매칭해 그 부분만 고치라"고 구체적으로 지시한다.
저장소 전체의 진실은 pnpm이다 — 루트 package.json의 workspaces·packageManager: "pnpm@10.32.1", frontend/pnpm-lock.yaml, README·CLAUDE.md 모두 pnpm install/pnpm dev를 안내한다. 그런데 frontend/Dockerfile은 여전히 COPY package.json yarn.lock /app/ + RUN yarn install로 되어 있다 — yarn.lock 파일 자체가 저장소에 없는데도 이 명령을 그대로 실행하려 한다.
즉 docker-compose up으로 프론트엔드를 빌드하면 로컬 개발(pnpm)과 다른 경로를 타게 된다. Docker로 처음 시도했는데 의존성이 안 맞는다면, 이 불일치를 의심해볼 것.
backend/pyproject.toml에는 moviepy와 langfuse가 선언돼 있다. 하지만 실제 백엔드 소스 어디에도 import moviepy나 import langfuse는 없다 — moviepy는 딱 한 곳, 테스트 파일(tests/test_prompts.py)에서 sys.modules에 미리 mock을 심어두는 방어 코드로만 등장한다. 실제 영상 처리는 moviepy로 프레임을 자르는 게 아니라, 영상 데이터를 통째로 Gemini에 넘기는 방식이다(s5-⑧). langfuse(LLM 트레이싱 도구)도 선언만 있을 뿐 실제로 트레이싱이 연결된 흔적이 없다 — "관측(observability)이 이미 켜져 있겠지"라고 가정하면 안 된다.
pyproject.toml은 python = "^3.10"이라고 적혀 있지만, 저장소의 CLAUDE.md는 "Poetry 가상환경이 실제로는 3.12로 잡힌다(3.10이 만족되는 범위 안)"고 명시적으로 짚어둔다. 로컬에 정확히 3.10만 깔려 있어야 한다고 오해하지 말 것 — poetry run으로 실행하면 Poetry가 알아서 맞는 인터프리터를 찾는다.
.github/ 폴더에는 이슈 템플릿과 후원 설정(FUNDING.yml)만 있고 GitHub Actions 워크플로 파일은 없다. CLAUDE.md가 "모든 변경 후 pytest·pyright를 반드시 돌리라"고 규정하지만, 이는 PR 단계에서 자동으로 강제되는 게 아니라 기여자(사람이든 코딩 에이전트든)가 스스로 지켜야 하는 사내 규칙에 가깝다. "테스트가 260개 넘게 있으니 CI도 있겠지"라고 넘겨짚지 말 것.
routes/model_choice_sets.py의 모델 조합은 며칠~몇 주 단위로 평가 세션 결과에 따라 바뀐다(커밋 메시지에 그 근거가 남아 있다). 이 문서가 인용한 특정 모델명·버전은 2026-07-27 커밋 시점의 스냅샷일 뿐이므로, 실제로 코드를 볼 때는 반드시 저장소의 최신 model_choice_sets.py를 다시 확인해야 한다.
"이미지/텍스트를 코드로 바꿔주는" 도구는 이제 하나의 장르다. screenshot-to-code가 어디에 서 있는지 보려면 대표적인 이웃들과 나란히 놓아봐야 한다.
| 프로젝트 | 공개 방식 | 핵심 차이점 |
|---|---|---|
| screenshot-to-code | 오픈소스(MIT) + BYOK 로컬 실행 | 이미지·영상·텍스트 3종 입력, 6개 스택, 도구 호출 에이전트 + 자체 스크린샷 검증, 평가 기반 모델 큐레이션 |
| openui | 오픈소스 | 프롬프트 → HTML/Tailwind 단일 스트리밍 생성 위주. 도구 호출형 에이전트 루프나 자체 렌더 검증은 없음 |
| v0 (Vercel) | 비공개 SaaS | Next.js/React·shadcn/ui 생태계에 깊게 통합, 클라우드 배포까지 원스톱. 모델·인프라를 직접 고를 수 없음 |
| bolt.new (StackBlitz) | 비공개 SaaS(일부 OSS 코어) | 브라우저 안 WebContainer에서 풀스택(npm 설치 포함)까지 실행 — "이미지→정적 코드"보다 "대화형으로 앱 전체를 브라우저에서 굴리기"에 가까움 |
| fragments (e2b) | 오픈소스 템플릿 | 코드 실행 샌드박스(Next·Streamlit·Vue 등)가 중심 — 이미지 한 장을 정적 화면으로 재현하기보다 "대화하며 실행 가능한 코드 데모"에 특화 |
v0·bolt는 편하지만 닫혀 있고, openui는 열려 있지만 단순한 스트리밍 생성에 머문다. screenshot-to-code는 소스가 완전히 공개돼 있으면서도, 이미지뿐 아니라 영상까지 입력으로 받고, 도구 호출 에이전트가 자기 결과를 스스로 스크린샷으로 확인하는 구조까지 갖췄다 — "공부하기 좋은 오픈소스 에이전트 코드베이스" 관점에서도 드문 조합이다.
피그마 내보내기나 스크린샷 한 장으로 프로토타입 마크업을 빠르게 뽑아 시작점으로 삼고 싶을 때 유용하다. 6개 스택 중 팀이 실제로 쓰는 프레임워크를 바로 고를 수 있다.
backend/evals/와 run_evals.py는 그 자체로 "여러 모델에게 같은 과제를 시키고 채점해 비교하는" 하니스의 실물 예제다. 비용 추적(costs/pricing.py)까지 포함돼 있어 참고하기 좋다.
미들웨어 파이프라인(s5-①), 30스텝 에이전트 루프(s5-②), 세 회사 API를 한 인터페이스 뒤로 감추는 패턴(s5-⑤)은 그 자체로 "실무 규모의 에이전트를 어떻게 구조화하는가"를 보여주는 좋은 사례다.
README가 안내하는 호스팅 제품(screenshottocode.com)은 이 저장소와 다른 hosted 브랜치에서, 별도의 비공개 SaaS 백엔드(CLAUDE.md에 ../screenshot-to-code-saas로 명시)와 연결돼 동작한다. 이 저장소를 직접 클론해 로컬로 돌리는 것과, 유료 호스팅판을 쓰는 것은 같은 코어 로직을 공유하되 별개의 배포라는 점을 알아두면 혼동이 없다.