아키텍처
intro에서 guardrail 세 개를 짚었습니다. 여기서는 그 세 개가 코드에서 어떻게 생겼는지를 봅니다. FSD 6레이어 의존 방향, 메시지 타입 단일 파일, 백엔드 어댑터. 셋 다 “AI가 임의로 가로지르지 못하게” 빌드 또는 타입이 막아주는 자리입니다.
FSD 6레이어 의존 방향
섹션 제목: “FSD 6레이어 의존 방향”폴더는 위에서 아래로 의존이 흐릅니다. 화살표 반대로 import 하면 빌드가 깨집니다.
graph TB app[app — 라우팅, provider, manifest 글루] pages[pages — popup, options, sidePanel] widgets[widgets — 화면 블록] features[features — 사용자 동작 한 단위] entities[entities — 도메인 모델] shared[shared — 유틸, 메시지 타입, 어댑터 인터페이스]
app --> pages pages --> widgets widgets --> features features --> entities entities --> shared각 레이어의 자리는 이렇게 갈립니다.
shared: 도메인 무관한 유틸, UI 키트, 메시지 타입, 어댑터 인터페이스. 어디서나 가져다 쓸 수 있고, 자기 자신만 의존합니다.entities: “수집한 페이지”, “백엔드 설정” 같은 도메인 모델. discriminated union 으로 잡는 자리.features: 사용자 동작 한 단위. “현재 페이지 분석”, “결과 백엔드로 전송”, “백엔드 모드 변경”.widgets: feature 여러 개를 묶은 화면 블록. popup 헤더, sidePanel 본문 같은 것.pages: popup, options, sidePanel 같은 진입 화면 단위.app: 진입점 글루. 라우팅, provider, manifest 와 React 트리를 잇는 자리.
eslint-plugin-boundaries가 위 화살표를 강제합니다. 역방향 import는 물론, 같은 레이어 안에서 슬라이스끼리 가로지르는 것도 막습니다. features/analyze-page에서 features/send-to-backend를 직접 import 하면 빌드 실패. 슬라이스 바깥에서 들어가는 유일한 통로는 그 슬라이스의 index.ts 입니다.
AI에게 코드를 맡길 때 어느 레이어에 둘지를 모델이 직접 고르게 두면 십중팔구 features와 widgets 사이를 흐릿하게 섞습니다. 슬라이스 이름까지 사람이 지정해서 주는 편이 안전합니다. “features/configure-backend에 옵션 폼 컴포넌트 추가” 식으로.
의존 방향을 이 모양으로 잡은 근거는 docs/adr/0001-fsd-import-direction.md에 한 장 들어있습니다. 코드는 무엇을 했는지를, ADR은 왜 그랬는지를 들고 있습니다.
메시지 타입 단일 파일
섹션 제목: “메시지 타입 단일 파일”content script와 background, popup 사이로 오가는 모든 메시지는 shared/lib/messaging/messages.ts 한 장에 정의된 discriminated union을 통과합니다.
export type Message = | { type: 'analyze-page'; tabId: number } | { type: 'send-result'; payload: PageResult } | { type: 'config-changed'; mode: BackendMode }송신은 chrome.runtime.sendMessage(msg) 또는 chrome.tabs.sendMessage(tabId, msg) 에서 같은 union 으로 type 좁힘이 따라옵니다. 수신은 chrome.runtime.onMessage.addListener 안에서 switch (msg.type) 로 분기합니다.
새 메시지를 추가할 때 절차는 messages.ts 에 union 가지를 한 줄 더 적는 것뿐입니다. 송신 쪽과 수신 쪽 양쪽이 동시에 컴파일 깨지면서 강제로 같이 갱신됩니다. 같은 페이로드 모양이 두 군데에 따로 정의되는 사고가 여기서 막힙니다.
sequenceDiagram participant CS as content_script participant BG as background (service worker) participant POP as popup participant ADP as backend adapter participant BE as backend (console / supabase / server)
CS->>BG: chrome.runtime.sendMessage (analyze-page) BG->>ADP: backend.send(payload) ADP->>BE: 모드별 분기 BE-->>ADP: ok / error ADP-->>BG: Result<T> BG-->>CS: response POP->>BG: 사용자 동작 트리거 BG-->>POP: 상태 갱신popup은 background를 향해서만 말을 겁니다. content script와 popup이 직접 붙는 경로는 의도적으로 비워뒀습니다. 그 자리는 늘 background를 거치게 만들어야 service worker가 깨어나고, 권한 검사와 어댑터 호출이 한 곳에 모입니다.
백엔드 어댑터 전략
섹션 제목: “백엔드 어댑터 전략”shared/api/backend/에 어댑터 인터페이스 한 개와 구현 셋이 들어있습니다.
shared/api/backend/├── index.ts 공개 진입점, factory 포함├── types.ts Backend 인터페이스, BackendMode union├── console-log.ts 콘솔에 페이로드만 찍는다├── supabase-direct.ts Supabase로 바로 쏜다└── server-relay.ts 자체 서버를 거친다호출하는 쪽 (feature 또는 entity) 은 어떤 모드인지 모르고 backend.send(payload) 만 부릅니다. 모드 결정은 옵션 페이지에서 하고 chrome.storage.sync 에 저장됩니다. factory가 그 값을 읽어 인스턴스를 고릅니다.
이 어댑터의 본 목적은 백엔드가 정해지지 않은 단계에서 UI 작업을 시작할 수 있게 하는 것입니다. 첫날은 console-log 모드로 페이로드 모양만 검증하고, Supabase 테이블이 잡힌 다음 supabase-direct 로 바꿉니다. 호출 코드는 한 줄도 안 건드립니다.
어댑터를 새로 늘릴 때는 types.ts 의 BackendMode union 에 가지를 한 줄 추가하고, 같은 인터페이스를 구현한 파일을 shared/api/backend/ 에 한 장 더 둡니다. factory의 switch 가 컴파일 시점에 누락을 잡습니다.
자동화 훅
섹션 제목: “자동화 훅”Claude Code 훅 세 개가 .claude/hooks/ 에 박혀 있고 .claude/settings.json 의 hooks 필드가 연결합니다. 사용자 또는 AI가 명령을 의식적으로 따르지 않아도 컨텍스트가 무너지는 자리를 막습니다.
| 스크립트 | 이벤트 | 책임 |
|---|---|---|
session-start.sh | SessionStart | active.md, progress.md 마지막 30줄, feature_list.json, extension-spec.md 첫 50줄, MEMORY.md 를 stdout 으로 prepend. fork 직후 첫 진입은 silently exit |
stop-reminder.sh | Stop | active.md 가 20자 미만이면 사용자에게 한 줄 메모 안내 |
pre-commit-check.sh | PreToolUse (Bash matcher) | tool_input.command 에 git commit 매치되면 pnpm run typecheck && pnpm run lint, 실패 시 exit 2 와 함께 로그 마지막 30줄을 stdout 으로 흘려 AI에게 차단 사유 전달 |
PreToolUse 의 Bash matcher 는 stdin으로 들어온 JSON 의 tool_input.command 를 jq 또는 sed로 추출해 git commit 패턴만 typecheck 게이트로 보냅니다. 다른 Bash 명령은 zero exit으로 통과시켜 워크플로 마찰을 늘리지 않습니다.
패키지 매니저는 pnpm-lock.yaml → yarn.lock → package-lock.json 순으로 lockfile을 보고 자동 결정합니다. package.json 이 없거나 typecheck 스크립트가 빠져 있으면 base 초기 단계로 보고 통과합니다.
훅을 새로 추가할 때는 .claude/hooks/<name>.sh 를 작성하고 (#!/usr/bin/env bash 시작, chmod +x), settings.json 의 적절한 이벤트 항목에 등록하고, .claude/hooks/README.md 표에 한 줄 더 적습니다. 모든 훅은 독립이라 한 개를 빼도 다른 두 개에 영향이 없습니다.
비개발자 호흡으로 같은 시스템을 정리한 자리는 비개발자용 07장 에 있습니다.
복구 슬래시 명령
섹션 제목: “복구 슬래시 명령”자동화 훅이 사용자 의식 바깥에서 도는 안전망이라면, 사용자 명시 호출 쪽에는 슬래시 명령 두 개가 박혀 있습니다. 둘 다 .claude/skills/ 안 스킬이 wire 합니다.
| 명령 | 트리거 자리 | 핵심 동작 |
|---|---|---|
/recover-from-blocked | review-extension 이 두 번 연속 changes_requested 끝에 blocked 으로 떨군 직후 | 최신 .claude/state/review-*.md 에서 점수 < 3 차원 추출, 같은 영역의 builder-*.md 두 iteration 을 비교해 same-mistake 감지. 사용자에게 평이한 보고 + 3옵션 (rollback / 사용자 도움 retry / manual) |
/resume | /clear 직후, 또는 며칠 만에 다시 진입한 자리 | state 파일 8개 + 최근 git log 종합해 stage 감지 (empty → paced → analyzed → designed → customized → implemented → built → packaged + 보조 분기). session-start.sh 훅이 raw 를 prepend 한 자리에 한 화면 합성 요약을 사용자에게 출력 |
/recover-from-blocked 의 옵션 A (rollback) 는 git reset --hard 를 부르는 파괴적 자리라, AskUserQuestion 한 번 + yes 명시 입력 한 번. 두 번 차단이 의도적으로 들어 있습니다. dirty working tree 면 그 변경도 사라진다는 안내가 한 줄 더 따라옵니다.
/resume 은 active.md 와 감지된 stage 가 어긋나면 합성을 멈추고 사용자에게 어느 쪽이 맞는지 묻습니다. 다른 PC 에서 작업이 진행됐거나 finish 후 active.md 갱신을 잊은 자리가 가끔 있어, 잘못 합성된 답보다 명시 확인이 쌉니다.
비개발자 호흡으로 같은 두 명령을 정리한 자리는 비개발자용 08장 에 있습니다.
Figma MCP 토큰 동기
섹션 제목: “Figma MCP 토큰 동기”5-customize-extension 스킬의 5-b.0 prelude 가 Figma MCP 가 등록된 환경에서 한 단계 분기를 더 만듭니다. 디자인 토큰을 손으로 옮기는 자리를 URL 한 번으로 끊습니다.
분기는 .claude/state/mcp-tools.json 의 selected 배열에 figma 또는 claude_ai_Figma 키가 있을 때만 켭니다. 없으면 5-b 본 단계로 곧장 떨어져 color_tone 한 단어로 추천 색상 셋을 짜는 기존 흐름과 같습니다.
flowchart TB start[/5-customize-extension] --> mcp{mcp-tools.json에 figma 키} mcp -- "있음" --> pick{디자인 토큰 어디서?} mcp -- "없음" --> tone[color_tone 한 단어 추천 셋] pick -- "Figma 자동" --> url[Figma URL 입력] pick -- "톤 단어" --> tone url --> parse[fileKey + nodeId 파싱] parse --> vars[get_variable_defs 호출] vars --> map[text/bg/primary/secondary/border 매핑] map --> tokens[tokens.css CSS 변수 박음] tokens --> brand[brand.json: theme_source figma + synced_at] vars -- "변수 일부 누락" --> ctx[get_design_context 보조] ctx --> mapURL 파싱은 figma.com/design/:fileKey/...?node-id=:nodeId 패턴에서 nodeId 의 - 를 : 로 변환하는 표준 규칙(Figma MCP 가이드)을 그대로 따릅니다. mcp__claude_ai_Figma__get_variable_defs(fileKey, nodeId) 가 디자인 변수를 JSON 으로 반환하면 그 중 색상 변수만 추출해 src/shared/ui/tokens.css 의 --color-* CSS 변수에 매핑합니다. 변수 일부가 비면(예: secondary 가 없는 시안) get_design_context 가 보조 컨텍스트와 screenshot 을 같이 던져 AI 가 같은 톤 안에서 보충 추천합니다.
brand.json 에는 다음이 박힙니다.
{ "theme_source": "figma", "figma_file_key": "<fileKey>", "figma_node_id": "<nodeId>", "synced_at": "<ISO>", "manual_overrides": ["secondary"]}manual_overrides 가 본 prelude 의 운영 핵심입니다. 두 번째 호출(figma_file_key 가 같음) 시 sync 는 변경된 변수만 갱신하고, tokens.css 에서 사용자가 손으로 고친 자리는 덮어쓰지 않습니다. 어떤 변수를 manual override 로 잡을지는 사용자가 5-b 에서 직접 토큰 값을 편집한 직후 brand.json 에 기록되는데, 이 기록은 /5-customize-extension 재호출 시 diff 비교로 자동 감지됩니다(파일 mtime 또는 git blame 기반이 아니라 brand.json 의 마지막 sync 시점 토큰 값과 현재 값 비교).
폰트 변수는 theme_depth 가 색상+폰트 또는 풀 테마 일 때만 같이 가져와 src/shared/ui/font.ts 에 import 줄을 박습니다. 색상만 모드에서는 폰트 변수를 무시합니다.
비개발자 호흡으로 같은 흐름을 정리한 자리는 비개발자용 05장 에 있습니다.
다음 섹션은 verification입니다. extension-spec.md §2 시나리오를 /test-extension이 어떻게 Vitest 테스트로 변환하는지, 그 결과를 raw 출력 대신 시나리오 언어로 다시 합성해 돌려주는 자리, 그리고 정적 측 review-extension과의 보완 관계를 봅니다.