TrendShift 딥다이브 · 2026-07-17 · Daily #7

tandpfun/wardrobe 딥다이브
옷 사진 한 장을 gpt-image로
카탈로그 컷아웃으로 바꾸는 로컬 옷장

wardrobe(제품명 Open Wardrobe)는 "내 옷을 gpt-image로 오려내고 정리하는" 로컬 퍼스트(local-first) AI 옷장 갤러리다. 내가 옷을 입은 사진(또는 옷만 찍은 사진)을 끌어다 놓으면, ① 비전 모델이 사진 속 옷을 하나하나 찾아내고 ② 이미지 모델이 그 옷만 깔끔한 투명 배경 카탈로그 컷아웃(catalog cutout)으로 재구성하며 ③ 선택적으로 "내가 그 옷을 입은" 에디토리얼 사진까지 만들어, ④ 전부 내 디스크의 JSON + PNG 파일로만 저장한다. 클라우드도, 로그인도, DB도 없다. 가장 놀라운 사실부터 — 이건 흔한 Next.js AI SaaS처럼 보이지만 Next.js가 전혀 아니다. Vite 6 + React 19 단일 페이지 앱이고, "백엔드"의 정체는 Vite 개발 서버에 얹힌 미들웨어 플러그인 두 개다. 이 문서는 저장소를 소스까지 클론해 "여기서 무엇을 배울 수 있는가"를 한국어로 정리한다. (저장소: tandpfun/wardrobe · 라이선스 MIT · v1.0.0 · 최신 커밋 f44006c, 2026-07-15 · Vite 6 + React 19 SPA · 파일 30개뿐 · 한국어 README 없음 · TrendShift Daily #7 · #gpt-image · #로컬 퍼스트)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 — "서버 없는 서버"와 옷 오려내기 파이프라인
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 시스템 · 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

정체부터 명확히 — 이건 "옷 사진 → 쇼핑몰 상품컷"을 내 컴퓨터 안에서만 처리하는 개인용 도구다

wardrobe사진 속 옷을 AI로 오려내(extract) 투명 배경 상품 이미지로 만들고, 그것들을 갤러리처럼 정리해 주는 1인용 로컬 앱이다. 핵심 흐름은 세 문장으로 끝난다. 사진을 끌어다 놓는다 → 사진 속 각 옷이 깔끔한 카탈로그 컷아웃으로 바뀐다 → 원하면 "내가 그 옷을 입은" 사진까지 만들어 준다. 이 모든 결과물(원본·작업파일·완성 PNG·JSON DB)은 클라우드로 나가지 않고 data/ 폴더 안에만 쌓인다. 제작자는 tandpfun(Discord 봇 생태계로 유명한 개발자)이고, 원 게시물은 X(@cdngdev)에 올라온 데모다.

한 문장 비유

"옷장을 통째로 쇼핑몰 상품 페이지로 만들어 주는 자동 스튜디오 — 단, 스튜디오가 통째로 내 노트북 안에 있다"

온라인 쇼핑몰의 옷 사진은 배경이 새하얗고 옷만 딱 떠 있다. 그런 "상품컷"을 찍으려면 보통 조명·배경지·포토샵 누끼(배경 제거)가 필요하다. wardrobe는 그 스튜디오 전체를 AI로 대신한다 — 게다가 사진이 클라우드로 올라가지 않고 내 컴퓨터 안에서만 처리된다. "내 옷들을 디지털 카탈로그로 아카이빙하고 싶다"는 욕구를 정확히 겨냥한, 개인 소장용 프로젝트다.

핵심 용어
로컬 퍼스트(local-first) · "내 데이터가 내 기기에 먼저·주로 존재"
보통의 웹 앱은 내 사진을 회사 서버로 올려 처리하고 저장한다. 로컬 퍼스트는 반대로 데이터의 원본과 저장소가 내 기기에 있고, 외부 서비스는 필요한 계산(여기선 OpenAI 이미지 생성)만 잠깐 빌려 쓴다. wardrobe는 원본 사진·중간 작업물·완성 컷아웃·목록(JSON)을 전부 data/ 폴더에만 두고, 그 폴더는 .gitignore로 저장소에서 아예 제외된다. 그래서 클론하면 갤러리가 텅 비어 있는 게 정상이다.
핵심 용어
컷아웃(cutout) / 누끼 · 배경을 지운 투명 PNG
사진에서 대상만 남기고 배경을 투명하게 지운 이미지를 컷아웃(한국 현장 용어로는 "누끼")이라 한다. 쇼핑몰 상품 이미지가 대표적. wardrobe의 목표 산출물이 바로 이 옷 컷아웃(투명 배경 PNG)이고, 흥미롭게도 투명하게 만드는 일을 AI가 하지 않는다 — AI에겐 "단색 배경에 옷을 그려라"라고 시키고, 배경을 투명으로 파내는 건 앱이 직접 픽셀 계산으로 처리한다(§4에서 자세히).
가장 먼저 · 큰 오해 방지
이건 Next.js 앱이 아니다 — Vite SPA + "미들웨어가 곧 백엔드"

업로드·작업 큐·리뷰 승인·이미지 서빙까지 있어서 "Next.js 풀스택 SaaS"처럼 읽히지만, 실제로는 Vite 6 단일 페이지 앱이다. 모든 API(/api/import/*)는 별도 서버 프로세스가 아니라 Vite 서버에 apply: "serve"로 얹힌 미들웨어 플러그인으로 존재한다. 즉 이 앱의 백엔드는 vite dev / vite preview가 돌 때만 살아 있다. vite build로 뽑은 정적 번들엔 /api/import/*없다 — GitHub Pages 같은 정적 호스팅엔 "가져오기(import)" 기능을 올릴 수 없다는 뜻이다. 이 한 가지만 이해하면 나머지 구조가 전부 풀린다.

2왜 지금 주목받는가

"gpt-image로 누끼" 데모의 시각적 임팩트 + 로컬 퍼스트 + 영리한 엔지니어링 트릭

wardrobe가 TrendShift Daily 상위에 오른 이유는 셋으로 요약된다. 첫째, 결과물이 눈에 확 들어온다 — "내 옷 사진 → 쇼핑몰 상품컷 + 내가 입은 화보"라는 변환은 데모 영상 한 편으로 설득이 끝난다. 둘째, 로컬 퍼스트라는 안심 포인트 — 개인 사진을 다루는 앱인데 클라우드로 안 나간다. 셋째, 코드를 열어 보면 "AI에 의존하지 않고 직접 푼" 영리한 트릭들이 촘촘하다. 특히 "투명 배경을 모델에 맡기지 않고 내가 크로마키로 파낸다"는 결정이 백미다.

주목 포인트 1 — 투명은 모델이 아니라 앱이 만든다

gpt-image에 "투명 PNG 줘"라고 안 하는 이유

이미지 모델에게 "배경 투명하게 해 줘"라고 시키면 가장자리에 잔털·반투명 얼룩이 남기 쉽다. wardrobe는 대신 모델에게 "완벽하게 균일한 단색 크로마(초록/마젠타/시안) 배경에 옷만 그려라"라고 시키고, 돌아온 이미지를 앱이 직접 크로마키(초록화면 합성)로 파낸다. 게다가 옷 색과 가장 먼 크로마색을 골라 "옷까지 파여 나가는" 사고를 막는다. 이 한 끗이 결과 품질을 가른다.

주목 포인트 2 — "서버 없는 서버"

Express도, 별도 프로세스도 없이 REST API 한 벌

업로드→작업 생성→리뷰 승인→에셋 서빙까지 완전한 REST API가 있는데, 그게 전부 Vite 플러그인의 configureServer 미들웨어 안에 산다. 프레임워크(Express/Hono)도, 배포할 서버도 없다. "프론트 개발 서버가 곧 백엔드"라는 이 구조는, 로컬 전용 도구를 만들 때 배포·운영 부담을 통째로 없애는 흥미로운 선택지다.

주목 포인트 3 — 앱과 똑같은 걸 에이전트 스킬로도

Codex 스킬로 "GUI 없이" 같은 파이프라인 실행

저장소엔 .agents/skills/ 아래 Codex 스킬 두 개(import-clothes, generate-outfits)가 들어 있어, 앱을 켜지 않고도 에이전트가 같은 옷-오려내기·화보 생성 파이프라인을 돌린다. 심지어 재-임포트 시 중복이 안 생기게 이미지 바이트의 해시로 안정적 UUID를 만든다. "앱 로직을 그대로 에이전트가 조작 가능하게 노출"하는 좋은 본보기다.

경쟁 맥락

비슷한 결을 가진 서비스로는 유료 "가상 피팅/누끼" SaaS(Photoroom·remove.bg 류)나 옷장 관리 앱(Whering·Acloset 등)이 있다. wardrobe의 차별점은 완전 오픈소스 + 완전 로컬 + gpt-image 기반이라는 삼박자다. 대신 트레이드오프도 분명하다 — OpenAI API 키가 필요하고(생성 비용 발생), 배포형 서비스가 아니라 내가 직접 클론해 돌리는 개발자용 도구에 가깝다. "예쁜 제품"이라기보단 "잘 만든 레퍼런스 구현"으로 보는 게 정확하다.

3기술 스택 전체 지도

놀랄 만큼 얇다 — 파일 30개, devDependencies 0개, 프레임워크는 Vite 하나뿐

wardrobe의 의존성 목록은 "AI 이미지 앱치고 이렇게 얇다고?" 싶을 만큼 단출하다. 런타임 의존성이 9개뿐이고 devDependencies는 아예 없다. TypeScript도, Tailwind도, ORM도, 상태관리 라이브러리도 없다. 무거운 일(이미지 픽셀 연산)은 sharp 하나가 다 하고, 지능(옷 인식·생성)은 OpenAI API가 다 한다. 나머지는 순수 React + 손으로 쓴 CSS다.

프론트엔드 (src/ — React SPA)

영역기술 · 버전역할
UI 프레임워크React 19.2.0 + react-dom갤러리·임포트 UI. 상태는 순수 훅(useState/useEffect/useMemo)만 — Redux·Zustand·Router 없음
빌드/개발Vite 6.4.3 (@vitejs/plugin-react 5.0.4)번들러이자 백엔드 호스트. 미들웨어 플러그인이 여기 얹힌다
이미지 표시@unpic/react 1.0.2반응형 <Image> 컴포넌트. srcset으로 알맞은 해상도 로드
아이콘@phosphor-icons/react 2.1.10UI 아이콘 세트
폰트@fontsource-variable/instrument-sans 5.2.8자가 호스팅 가변 폰트(외부 CDN 의존 없음)
스타일손으로 쓴 CSS (styles.css 914줄 · import-flow.css 320줄)Tailwind·CSS 프레임워크 없음. 종이질감 "에디토리얼" 디자인 토큰

백엔드 (scripts/ — Vite 미들웨어)

영역기술 · 버전역할
서버Vite 미들웨어 (import-job-api.mjs 711줄)업로드·작업·리뷰·에셋 서빙 전부. Express/Hono 없음
이미지 처리sharp 0.34.5 (libvips)정규화·크롭·크로마키 제거·despill·프레이밍·검증. 이 앱의 진짜 엔진
반응형 리사이즈ipx 3.1.1/_ipx/* 엔드포인트에서 즉석 리사이즈/화질 조정
OpenAI 호출네이티브 fetch + FormData/BlobOpenAI SDK조차 안 씀 — 순수 fetch로 /responses·/images/edits 호출
저장Node fs/promises + crypto로컬 JSON DB + PNG 파일. DB 엔진·ORM 없음

AI · 인프라

영역기술역할
옷 인식(비전)OpenAI Responses API · 기본 gpt-5.4-mini사진 속 옷을 찾아 이름·카테고리·색·바운딩박스를 구조화 JSON으로 반환
이미지 생성OpenAI Images edits API · 기본 gpt-image-2옷 컷아웃(단색 배경) + "내가 입은" 화보 생성
PWAmanifest.webmanifest + sw.js오프라인 앱 셸 캐시 + 이미지 stale-while-revalidate (프로덕션에서만 등록)
에이전트 층Codex 스킬 (.agents/skills/)앱 없이 같은 파이프라인 실행 (openai.yaml 인터페이스)
주의 · 미래형 모델명
gpt-5.4-mini / gpt-image-2 는 코드에 박힌 "미래 날짜" 이름

기본 모델명 gpt-5.4-mini(비전)·gpt-image-2(이미지)는 2026년 시점의 미래형 이름으로 코드와 .env.example에 하드코딩돼 있다. 지금 실제 API로 돌리려면 OPENAI_VISION_MODEL·OPENAI_IMAGE_MODEL(또는 단계별 OPENAI_GARMENT_MODEL·OPENAI_MODELED_MODEL) 환경변수로 실존하는 모델명으로 덮어써야 한다. 클론만 하고 그대로 돌리면 "그런 모델 없음" 오류가 날 수 있다.

4아키텍처 심화 분석

"서버 없는 서버" 구조 + 옷 한 벌이 컷아웃이 되기까지의 3단계 파이프라인

4-1. 전체 그림 — 브라우저 ↔ Vite 미들웨어 ↔ OpenAI

클라이언트(React SPA)와 "서버"(Vite 미들웨어)는 같은 프로세스 안에 있다. 브라우저는 /api/import/*로 요청하고, Vite 서버에 얹힌 wardrobeImportApi 핸들러가 받아 sharp로 이미지를 주무르고 OpenAI를 호출한 뒤 파일로 저장한다. 별도 백엔드 서버는 존재하지 않는다.

브라우저 (React SPA) Vite 개발/프리뷰 서버 (미들웨어) OpenAI ──────────────────── ────────────────────────────── ────── 사진 drag/drop/paste │ FileReader → dataURL ▼ POST /api/import/jobs ───────▶ wardrobeImportApi.handler sharp 정규화 (rotate·srgb·png) openAIAnalyze ──────────────────────────▶ /responses ◀── items[]{name,part,color,bbox,tags} (gpt-5.4-mini) 아이템별 job 폴더 생성 json_schema · 최대 8개 original.png + crop.png(bbox+여백) 저장 ◀── 202 { jobs[] } crop.status = "review" 크롭 미리보기 → 사용자 승인 │ POST …/stages/crop/approve ▼ startGarment → chooseChromaKey(옷색) buildGarmentPrompt("단색 크로마 배경에 옷만") openAIEdit(crop, 1024²) ─────────────────▶ /images/edits ◀── 크로마 배경 위 옷 PNG (투명 아님!) (gpt-image-2) removeChromaBackground ← sharp, 모델 아님 키잉(tol 46)+despill+feather 80+88% 프레이밍+검증 ◀ 900ms마다 GET …/jobs/id garment.status = "review" 옷 메타 편집 → 승인 │ PATCH …/metadata; approve ▼ persistImported → data/imported/ 복사 library.json 원자적 기록(tmp+rename) startModeled → generate(job,"modeled") openAIEdit([나 사진, 옷], 1536×1024) ─────▶ /images/edits ◀── 내가 그 옷을 입은 에디토리얼 사진 ◀ modeled.status="review"; 승인 → job "complete", data/jobs/id 삭제(GC) 갤러리에 새 옷 표시 (낙관적 추가 + localStorage 덮어쓰기)
핵심 용어
Vite 미들웨어 플러그인 · apply: "serve" · configureServer
Vite 플러그인은 configureServer(server) 훅에서 개발 서버의 요청 처리 파이프라인(connect 미들웨어)에 직접 함수를 끼워 넣을 수 있다. wardrobe는 이걸로 /api/import/* 요청을 가로채 처리한다. apply: "serve"는 "이 플러그인은 개발/프리뷰 서빙 때만 작동하고 빌드 땐 빠진다"는 표시. 그래서 이 앱의 API는 vite dev·vite preview에서만 살아 있다.

4-2. 심장부 — 옷 한 벌이 컷아웃이 되는 3단계

모든 옷은 crop → garment → modeled라는 3단계 상태 기계를 거친다. 각 단계는 "생성 → 리뷰(사용자 승인/거절/재생성)"를 반복한다. 순서대로 뜯어보자.

① 인식 — Responses API로 "옷 목록 + 위치" 뽑기

업로드된 사진을 sharp로 정규화한 뒤, OpenAI /responses엄격한 json_schema(이름 wardrobe_items, 최대 8개)로 질의한다. 모델은 옷마다 이름·카테고리·주색(hex)·부색·태그와 함께 1000×1000으로 정규화한 정수 바운딩박스를 돌려준다. 카테고리는 딱 5종으로 제한: upperbody(상의) · wholebody_up(원피스류) · lowerbody(하의) · accessories_up(액세서리) · shoes(신발). 앱은 이 박스를 픽셀로 환산하고 max(12px, 8%) 여백을 더해 crop을 떠낸다.

② 컷아웃 — "크로마 배경에 그려라" + 앱이 파낸다

여기가 핵심 트릭이다. 앱은 먼저 옷의 주색과 가장 먼 크로마색을 고른다(chooseChromaKey: 초록 #00ff00·마젠타 #ff00ff·시안 #00ffff 중 RGB 거리 최대). 그 색을 프롬프트에 넣어 gpt-image에 "그 단색 배경 위에 옷만" 그리게 한다. 실제 프롬프트(buildGarmentPrompt)의 배경·금지 조항이 특히 촘촘하다:

# buildGarmentPrompt — 핵심 조항 (요약 발췌)
Primary request: Reconstruct ONLY the complete empty ${name} (${category})
  as a clean, front-facing ecommerce catalog product photograph.
  If a wearer is present, remove them. ...no person, body, mannequin, or hanger.
Background: Perfectly flat, absolutely uniform solid ${chromaKey} chroma-key
  color, edge-to-edge. No shadows, gradient, texture, vignette, reflection...
Critical: Use no ${chromaKey} anywhere in the garment.
  Produce exactly one complete garment with a crisp, separable outer silhouette.

실제 API 호출은 /images/edits멀티파트 폼으로 나간다. OpenAI SDK 없이 순수 fetch+FormData다:

// openAIEdit — /images/edits 멀티파트 호출
const form = new FormData();
form.set("model", model);        // gpt-image-2 (또는 OPENAI_GARMENT_MODEL)
form.set("prompt", prompt);
form.set("size", size);          // 옷 컷아웃: "1024x1024"
form.set("quality", quality || "high");
form.set("output_format", "png");
for (const [i, image] of images.entries()) {
  const normalized = await normalizeImage(image.data);
  form.append("image[]", new Blob([normalized], { type: "image/png" }), ...);
}
const response = await fetch(`${baseUrl}/images/edits`, {
  method: "POST", headers: { Authorization: `Bearer ${key}` }, body: form,
});
// result.data[0].b64_json → Buffer 로 디코드

모델이 준 이미지는 투명이 아니라 크로마색 배경이다. 진짜 투명화는 앱이 sharp로 직접 한다(removeChromaBackground) — 이 과정엔 AI가 전혀 없다:

removeChromaBackground (순수 sharp 픽셀 연산) ───────────────────────────────────────────── 1. 픽셀별 크로마색과의 유클리드 거리 계산 · 거리 ≤ tolerance(기본 46, 조절 18~110) → 완전 투명(alpha 0) · 거리 ≤ tolerance + feather(80) → 부분 투명(가장자리 부드럽게) 2. despill(스필 제거): 옷 가장자리에 밴 초록/마젠타 기운을 중화 spill = keyedLevel − neutralLevel → 여러 패스 반복 3. frameTransparentGarment: 옷 alpha 경계로 자르고 1024×1024 캔버스의 88% 크기로 중앙 정렬 4. verifyNoChromaSpill: 오염 픽셀(스필>1.5) 개수 검사 · 엄격 모드에서 >1개 남으면 throw → 단계 "failed" · 원본 크로마 이미지를 수동 cleanup UI로 노출(tolerance 슬라이더)
설계의 묘
"AI를 못 믿는 부분은 손으로" — 크로마키 + 동적 키색 선택

이미지 모델은 "투명 배경"을 못 미덥게 처리하지만, "완벽한 단색 배경"은 잘 그린다. 그 강점만 쓰고, 신뢰가 필요한 누끼는 결정적(deterministic) 픽셀 연산으로 앱이 직접 한다. 여기에 "옷 색과 가장 먼 크로마를 고른다"는 동적 키색 선택까지 더해 "초록 옷을 초록화면으로 파다가 옷이 사라지는" 사고를 막는다. 튜토리얼들이 잘 안 알려주는 실전 트릭이다.

③ 화보 — "나 + 옷" 두 장으로 입은 모습 합성

옷 컷아웃이 승인되면(선택) /images/edits를 한 번 더 부른다. 이번엔 이미지를 순서대로 두 장 넣는다 — data/model-reference.png(나) 먼저, 옷 PNG 나중, 크기 1536x1024(3:2). 프롬프트는 "Image 1의 사람이 Image 2의 옷을 입은 프로페셔널 에디토리얼 사진을 만들되 얼굴·정체성·나이·비율을 보존하고 옷의 색·소재·핏·로고를 그대로 유지"하라고 지시한다. 재생성 시엔 사용자의 지시문(User regeneration direction: ...)이 뒤에 덧붙는다.

핵심 용어
다중 참조 이미지 편집 · 정체성 보존 가상 피팅
/images/editsimage[]여러 장을 순서 있게 넣으면, 모델이 "1번 사람 + 2번 옷"처럼 참조를 조합한다. wardrobe는 이걸로 "내 얼굴은 그대로, 옷만 이 옷으로" 하는 가상 피팅(try-on)을 구현한다. 순서가 의미를 갖는다는 점(정체성 원본이 먼저)이 포인트다.

4-3. 저장 — DB가 없다, JSON 파일이 곧 DB

ORM도 SQL도 없다. 두 개의 JSON "테이블"과 작업별 상태 파일이 전부다. 완성된 옷은 data/library.json(평면 배열)에, 진행 중 작업은 data/jobs/<uuid>/job.json에 담긴다. 기록은 항상 원자적 쓰기(임시 파일 → rename, 실패 시 복사 폴백)로 안전하게 이뤄지고, 서버 재시작 시 중단된 작업을 스스로 정리·재개한다.

// data/library.json — 옷 한 벌의 레코드 모양
{
  "id": "import-<uuid>",              // Codex 경로에선 바이트 해시로 안정 UUID
  "name": "Navy Fair Isle Cardigan",
  "part": "wholebody_up",           // 5종 카테고리 enum
  "color": "#172033",
  "secondaryColor": "#f2efe6",       // hex 또는 null
  "palette": ["#172033", "#f2efe6"],
  "tags": ["knit", "fair isle", "zip"],
  "image": "/api/import/library/import-<uuid>-garment.png",
  "modeledImage": "/api/import/library/import-<uuid>-modeled.png", // 또는 null
  "importJobId": "<uuid>"
}

사용자의 이름 변경·색 조정·삭제 같은 편집은 서버 레코드 위에 브라우저 localStorage(open-wardrobe-edits-v1·open-wardrobe-deleted-v1)로 얹힌다. 덕분에 모든 필드마다 쓰기 API를 만들지 않고도 UI가 즉각 반응하고 편집이 유지된다("낙관적 UI + 로컬 오버레이" 패턴).

5디렉토리 구조 해부

추적 파일 30개뿐 — src/는 프론트, scripts/는 "서버", .agents/는 에이전트 스킬
wardrobe/ ├─ index.html # SPA 진입점, #root 마운트, 타이틀 "Open Wardrobe" ├─ vite.config.mjs # react() + 미들웨어 플러그인 2개 배선 ├─ package.json / package-lock.json / .env.example / .npmrc / .gitignore ├─ LICENSE (MIT) / README.md / CONTRIBUTING.md ├─ public/ │ ├─ manifest.webmanifest # PWA 매니페스트 (standalone, #f4f0e8) │ ├─ sw.js # 서비스워커 (오프라인 셸 + 이미지 캐시) │ └─ icon.svg ├─ docs/screenshots/ # gallery.png, editor.png (README 이미지) ├─ src/ # ── 프론트엔드 (React SPA) ── │ ├─ main.jsx # React 루트 + SW 등록(프로덕션만) │ ├─ App.jsx (650줄) # 갤러리·필터·뷰어/편집기·팔레트 추출 │ ├─ import-flow.jsx (294줄) # drag/drop/paste 임포트 + 리뷰 상태기계 │ ├─ OptimizedImage.jsx # @unpic/react → /_ipx 반응형 이미지 │ └─ styles.css / import-flow.css ├─ scripts/ # ── "백엔드" (Vite 미들웨어) ── │ ├─ import-job-api.mjs (711줄) # ★ 심장부: 인식+추출+화보+저장 │ └─ responsive-image-api.mjs (34줄) # ipx 이미지 서버 (/_ipx) └─ .agents/skills/ # ── Codex 에이전트 스킬 (대체 경로) ── ├─ import-clothes/ SKILL.md + agents/openai.yaml + scripts/import-to-wardrobe.mjs └─ generate-outfits/ SKILL.md + agents/openai.yaml + references/outfit-image-prompt.md (런타임 생성, gitignore됨) data/ # 원본·작업파일·완성 에셋·library.json

구조를 읽는 열쇠는 "폴더 이름이 곧 역할"이다. src/는 브라우저에서 도는 클라이언트, scripts/Vite 서버 안에 사는 서버 로직, .agents/skills/는 앱 대신 에이전트가 같은 파이프라인을 돌리게 하는 Codex 스킬, public/은 PWA 자산이다. 그리고 실제 데이터가 쌓이는 data/런타임에 생성되고 저장소엔 없다(gitignore).

파일줄 수무엇을 배우나
scripts/import-job-api.mjs711이 저장소의 90%. 인식 프롬프트·크로마키 제거·despill·검증·원자적 저장·작업 GC가 전부 여기
src/App.jsx650Canvas로 색 팔레트 추출, 알파 인식 스포이드 샘플링, localStorage 오버레이 병합
src/import-flow.jsx294drag/drop/paste 입력 + 3단계 리뷰 상태기계, 900ms 폴링, 낙관적 갤러리 추가
.agents/skills/…/import-to-wardrobe.mjsPNG 검증(RGBA·투명+불투명 픽셀 필수) + 바이트 해시 → 안정 UUID로 멱등 임포트

6학습 포인트

"작지만 밀도 높은" 저장소 — 프레임워크 없이 문제를 직접 푸는 법을 배운다

기술별로 배울 것

주제이 저장소에서 배울 구체적인 것
Vite 플러그인configureServer/configurePreviewServer 미들웨어로 백엔드 없이 REST API 한 벌 짓기. apply:"serve"의 의미
크로마키 · despill초록화면 합성의 실전 수학 — 거리 기반 키잉, feather 램프, spill = keyed − neutral 스필 제거, 검증 후 throw
구조화 출력OpenAI Responses의 json_schema로 enum·hex 정규식·정규화 바운딩박스를 강제해 파싱 가능한 인식 결과 얻기
다중 참조 편집/images/edits에 순서 있는 image[](정체성+옷)로 정체성 보존 가상 피팅
로컬 영속성원자적 JSON 쓰기(tmp+rename+폴백), 작업별 폴더, 재시작 자가 복구, 내용 주소 기반(content-addressed) 안정 UUID로 멱등 재임포트
낙관적 UI서버 레코드 + localStorage 편집/삭제 오버레이 병합으로 즉각 반응하는 UI
클라이언트 색 과학Canvas 팔레트 추출(버킷 평균 + 최소 색거리), 알파 인식 스포이드 반경 탐색
PWA 캐시/_ipx 리사이즈 엔드포인트에 stale-while-revalidate + LRU(최대 800개) 이미지 캐시
앱=스킬 미러링같은 파이프라인을 Codex SKILL.md로도 표현(QA 루프·매니페스트·결정적 임포터) → "앱을 에이전트가 조작 가능하게"
이 저장소가 특히 좋은 교보재인 이유

규모가 작아서(파일 30개) 하루면 전체를 다 읽을 수 있다. 그런데도 "AI 앱을 실제로 만들 때 마주치는 결정들"이 압축돼 있다 — 모델을 어디까지 믿고 어디부터 직접 풀까, 투명 배경을 어떻게 신뢰성 있게 뽑을까, 서버 없이 로컬 도구를 어떻게 완결할까, 앱 로직을 어떻게 에이전트도 쓰게 노출할까. "큰 프레임워크에 기대지 않고 문제를 직접 푸는" 감각을 기르기에 이상적이다.

7시스템 · 실행 요구사항

GPU 불필요 · OpenAI API 키 필요 · 내 사진 1장 필요 · 전부 로컬
항목요구
Node.js22 이상 (CI가 node 22 고정, README 배지 Node 22+). npm(package-lock.json)
OpenAI API 키필수. 비전·이미지 모델 접근 권한 필요. OPENAI_API_KEY 환경변수
내 참조 사진data/model-reference.png — 웹 임포터 활성화 조건이자 "입은 모습" 생성에 필요
GPU불필요. 무거운 이미지 연산은 OpenAI(클라우드) 또는 CPU sharp(libvips)가 처리
네이티브 의존성sharp가 사전 빌드 바이너리를 받음 — 사실상 유일하게 신경 쓸 네이티브 부분
클라우드/배포없음. 클론 → npm install.env+참조 PNG → npm run dev → localhost:5173
주의 · 임포터 이중 잠금
API 키 "그리고" 내 참조 사진, 둘 다 있어야 임포터가 켜진다

웹 UI의 임포터는 GET /api/import/configready: hasApiKey && hasModelReference를 반환할 때만 활성화된다. 즉 옷 컷아웃만 뽑고 싶어도 앱에선 내 참조 사진(data/model-reference.png)이 있어야 문이 열린다. "누끼만 필요한데 왜 내 사진을?" 싶다면, 참조 사진이 필요 없는 Codex 스킬 경로(import-clothes)를 쓰면 된다. 참고로 개인 사진을 다루는 도구이므로, 참조 사진과 data/ 폴더는 내 기기 밖으로 나가지 않는다.

숨은 설정
.env.example에 없지만 실제로 읽는 환경변수 4개
코드는 읽는데 예시 파일엔 빠진 변수들이 있다: OPENAI_API_BASE_URL(기본 https://api.openai.com/v1 — 프록시·Azure·호환 엔드포인트로 바꿀 수 있음), WARDROBE_DATA_DIR(기본 data), 그리고 단계별로 다른 이미지 모델을 쓰게 해 주는 OPENAI_GARMENT_MODEL·OPENAI_MODELED_MODEL(둘 다 없으면 OPENAI_IMAGE_MODEL로 폴백). 고급 설정을 하려면 .env.example만 보지 말고 소스를 봐야 한다.

8직접 해볼 수 있는 실습 과제

난이도별로 — 소스를 읽는 데서 시작해 파이프라인을 바꿔 보는 데까지
난이도 ★☆☆ — 워밍업

실존 모델로 바꿔 실제로 돌려 보기 난이도: 입문

클론 후 .env에 실제 OpenAI 키와 지금 존재하는 비전·이미지 모델명OPENAI_VISION_MODEL·OPENAI_IMAGE_MODEL로 지정한다. data/model-reference.png에 내 사진을 넣고 npm run dev. 옷 사진 하나를 임포트해 crop→garment→modeled 3단계가 도는 걸 눈으로 확인하자. 미래형 기본 모델명(§3 주의)을 왜 덮어써야 하는지 체감하는 게 목표다.

난이도 ★☆☆

crop이 뜨는 순간까지 로그 심기 난이도: 입문

scripts/import-job-api.mjsopenAIAnalyzeconsole.log를 넣어 모델이 돌려준 items[](이름·카테고리·색·바운딩박스)를 찍어 보자. json_schema가 어떻게 출력을 강제하는지, 정규화 1000×1000 박스가 실제 픽셀로 어떻게 환산되는지(cropDetectedItem) 따라가면 "구조화 출력 → 결정적 후처리"의 연결이 보인다.

난이도 ★★☆

크로마키 tolerance/feather 실험 난이도: 중급

removeChromaBackgroundtolerance(기본 46)·feather(80)를 바꿔 가며 가장자리 잔털과 "옷이 파여 나감" 사이의 트레이드오프를 눈으로 비교하자. 초록·마젠타·시안 중 어떤 옷에서 어떤 키색이 유리한지(chooseChromaKey) 직접 확인하면, "동적 키색 선택"이 왜 필요한지 손에 잡힌다. 심화: 검증에서 throw하는 대신 더 먼 키색으로 자동 재시도하게 고쳐 보기.

난이도 ★★☆

"서버 없는 서버"에 엔드포인트 하나 추가 난이도: 중급

Vite 미들웨어에 GET /api/import/stats를 새로 만들어 library.json을 읽어 카테고리별 옷 개수·색 분포를 반환하게 해 보자. configureServer 미들웨어에 라우트를 어떻게 끼워 넣는지, 응답을 어떻게 직렬화하는지 익히는 과제다. Express 없이 REST를 짓는 감각을 기른다.

난이도 ★★★

"코디(outfits)" 화면 완성하기 난이도: 심화

현재 generate-outfits 스킬은 data/outfits.json과 정사각 룩북 이미지를 만들지만, React 앱엔 이를 보여줄 라우트가 없다(§11 함정). outfits.json을 서빙하는 미들웨어 라우트 + 갤러리 옆 "코디" 탭을 추가해 in-app으로 렌더해 보자. 저장부터 서빙·표시까지 한 기능을 끝까지 관통하는 종합 과제다. 심화: 미들웨어를 독립 Express/Hono 서버로 떼어내 정적 호스팅에도 배포 가능하게 만들기.

9관련 기술 심화 학습 로드맵

주차별 — wardrobe 소스를 축으로 "AI 이미지 앱 만들기"를 넓혀 가는 6주 코스
주차주제 · 학습 활동
1주차Vite 플러그인 시스템. configureServer·apply·configResolved 훅을 Vite 플러그인 문서로 익히고, import-job-api.mjs가 어떻게 "서버"가 되는지 그려 보기.
2주차sharp / 이미지 픽셀 연산. raw RGBA 접근, 합성, 채널 조작. 크로마키·despill·프레이밍 코드를 한 줄씩 재현. 초록화면 합성의 원리를 밑바닥부터.
3주차OpenAI 구조화 출력. Responses API의 json_schema, enum·pattern 강제. "자연어 → 파싱 가능한 데이터"의 신뢰성 설계.
4주차이미지 편집/생성 API. /images/edits 멀티파트, 다중 image[] 참조, size·quality·output_format. 정체성 보존 가상 피팅의 프롬프트 엔지니어링.
5주차로컬 퍼스트 영속성. 원자적 파일 쓰기, 내용 주소 기반 UUID(멱등), 재시작 자가복구. Local-first 에세이로 철학적 배경까지.
6주차앱을 에이전트 스킬로. .agents/skills/SKILL.md·openai.yaml을 읽고, 내가 짠 기능 하나를 Codex 스킬로도 노출해 보기. "GUI ↔ 에이전트" 양면 설계.
병행 학습 팁
"wardrobe 소스 + 표준 문서 + 인덱스의 이웃 딥다이브"를 함께
이미지 파이프라인을 팔 땐 sharp 공식 문서를, 모델 호출을 볼 땐 OpenAI Images 가이드를 곁들이면 "wardrobe의 손구현 ↔ 업계 표준"이 이어진다. 또 §인덱스의 다른 cat-media(이미지·얼굴) 딥다이브와 "같은 이미지 문제를 어떻게 다르게 풀었나"를 비교하고, Codex 스킬 쪽은 cat-claude-skills 딥다이브와 나란히 보면 학습이 배가된다.

10핵심 키워드 사전

wardrobe 소스에서 반복되는 용어 빠른 참조
용어
로컬 퍼스트(local-first)데이터 원본·저장소가 내 기기에 있고, 외부 서비스는 계산만 잠깐 빌려 쓰는 설계. wardrobe는 data/에만 저장(gitignore).
컷아웃 / 누끼배경을 지운 투명 PNG. wardrobe의 목표 산출물. 단, 투명화는 모델이 아니라 앱이 크로마키로 처리.
Vite 미들웨어 플러그인configureServer로 개발 서버 요청 파이프라인에 함수를 끼워 REST API를 구현. apply:"serve"=서빙 때만 작동, 빌드 땐 빠짐.
3단계 상태기계옷 한 벌이 crop → garment → modeled를 거침. 각 단계 = 생성 후 사용자 리뷰(승인/거절/재생성).
Responses API · json_schemaOpenAI 비전 호출. 엄격한 스키마(enum·hex·정규화 1000×1000 박스)로 파싱 가능한 옷 인식 결과를 강제.
크로마키 · chooseChromaKey단색(초록/마젠타/시안) 배경을 투명으로 파내는 합성 기법. 옷 색과 가장 먼 키색을 골라 옷이 파이는 걸 방지.
despill(스필 제거)옷 가장자리에 밴 키색 기운을 중화. spill = keyedLevel − neutralLevel을 여러 패스 반복해 뺌.
feather / tolerancetolerance(기본 46)=완전 투명 경계, feather(80)=부분 투명 램프. 가장자리를 부드럽게 하는 두 손잡이.
verifyNoChromaSpill완성 컷아웃에 키색 오염 픽셀이 남았는지 검사. 엄격 모드에서 남으면 throw → 수동 cleanup UI로.
다중 참조 편집(image[])/images/edits에 순서 있는 여러 이미지(정체성+옷)를 넣어 정체성 보존 가상 피팅을 구현.
내용 주소 기반 UUIDCodex 임포터가 컷아웃 바이트의 sha256으로 안정적 UUID 생성 → 재-임포트 시 중복 없이 갱신(멱등).
낙관적 UI + localStorage 오버레이서버 레코드 위에 브라우저 편집/삭제를 얹어, 필드별 쓰기 API 없이도 즉각 반응·유지.
Open Wardrobe제품 브랜딩 이름(HTML 타이틀·매니페스트·라이선스). dedup 기준 식별자는 tandpfun/wardrobe.

11참고 링크

공식 소스 우선 — 버전·수치는 클론한 소스(v1.0.0, 커밋 f44006c, 2026-07-15) 기준

공식
· GitHub 저장소: github.com/tandpfun/wardrobe (MIT · v1.0.0)
· 원 게시물(데모): x.com/cdngdev/status/2076812846793650485
· TrendShift 카드: trendshift.io/repositories/84145 (Daily #7)

저장소 안에서 먼저 열 곳
· scripts/import-job-api.mjs — 인식·컷아웃·화보·저장의 심장부(§4)
· src/import-flow.jsx — 3단계 리뷰 상태기계 + 폴링/낙관적 UI
· src/App.jsx — 갤러리·팔레트 추출·스포이드·localStorage 오버레이
· vite.config.mjs — "서버 없는 서버"가 배선되는 곳
· .agents/skills/import-clothes/SKILL.md — 같은 파이프라인의 에이전트 버전
· .env.example + README.md — 설정과 요구사항(숨은 변수는 소스로 보완)

곁들여 학습(표준·이웃)
· Vite 플러그인 API · sharp 문서 — 미들웨어·이미지 연산의 표준
· OpenAI Images 가이드 · Responses API — 인식·생성 호출의 원전
· §인덱스의 다른 cat-media(이미지·얼굴) · cat-claude-skills 딥다이브와 비교

참고 · 소스 정독으로 확인한 주의점
Next.js 오해 · 미래형 모델명 · 빈 클론 등 7가지 함정

Next.js가 아니다 — Vite SPA + apply:"serve" 미들웨어. 임포터는 vite dev/preview에서만 살아 있고 정적 빌드엔 /api/import/*가 없다. npm run check는 그냥 빌드(테스트·린트 없음). ② 미래형 모델명 gpt-5.4-mini·gpt-image-2는 하드코딩 — 실존 모델명으로 덮어써야 함. ③ 투명은 앱이 만든다 — gpt-image에 투명을 안 맡기고 크로마키+로컬 제거. ④ .env.example 불완전 — OPENAI_API_BASE_URL·WARDROBE_DATA_DIR·단계별 모델 변수 누락. ⑤ 임포터 이중 잠금 — API 키 그리고 data/model-reference.png 둘 다 필요(Codex 경로는 예외). ⑥ 클론하면 텅 빔data/가 gitignore라 샘플 없음. ⑦ GET /api/import/jobs읽으면서 완료/거절 작업을 삭제(GC)하는 비멱등 설계 + 코디(outfits) 화면 미구현 + 개발서버 allowedHosts:["terminal.local"] 저자 환경 흔적. 트렌딩 순위·수치는 계속 바뀐다.