본문으로 건너뛰기

Figma 퍼블리싱 파이프라인 08. v5: Phase 4→5 자동 연동

·7 min read·8 / 12

Phase 4와 Phase 5 사이의 구멍

v5의 핵심은 Design IR과 Code IR을 각각 만들고, 그걸 기계가 비교하는 구조다. 그런데 막상 돌려보니 Phase 4(Design IR 생성)와 Phase 5(Code IR 생성) 사이에 문제가 하나 있었다.

Phase 5가 코드 파일을 찾을 때 기준이 모호했다. 어떤 파일이 이 Figma 노드에 대응하는 코드인지를 Phase 5 스스로 판단해야 했는데, 그 판단이 종종 틀렸다. "CPA 외부 입력폼 현황"이라는 페이지를 퍼블리싱하는데, Phase 5가 marketer-member 관련 파일을 가져오는 식이었다.

두 가지를 고쳐야 했다. Phase 4가 원본 노드 정보를 보존하고, Phase 5가 그걸 활용해서 올바른 폴더를 찾도록.


Phase 4 — root 필드 추가

Phase 4가 만드는 Design IR에는 노드들의 배열이 담겨 있었다. 그런데 Phase 5가 "원본 페이지 이름이 뭐였지?"를 알아야 할 때, 기존 구조에서는 찾을 방법이 없었다. 섹션 노드들(LNB, Main Content)만 있고, 최상위 페이지명은 이미 사라진 상태였다.

그래서 root 필드를 하나 추가했다. Design IR 출력에 첫 번째 노드를 root로 따로 빼두는 것이었다.

# 변경 전
output = {
    "meta": {"timestamp": ..., "total_nodes": len(design_ir_nodes)},
    "nodes": design_ir_nodes
}
 
# 변경 후
output = {
    "meta": {"timestamp": ..., "total_nodes": len(design_ir_nodes)},
    "root": design_ir_nodes[0] if design_ir_nodes else None,
    "nodes": design_ir_nodes
}

None 값 필터링도 함께 추가했다. 기존엔 None이 섞여 들어오는 경우가 있었는데, 이 김에 정리했다.


Phase 5 — 파일 찾는 방법을 두 가지로 나눴다

Phase 5의 문제는 "이 Figma 노드에 대응하는 코드 폴더가 어디냐"였다. 해결 방법으로 두 가지 경로를 만들었다.

방법 1: raw.json에서 원본 노드명 추출

Phase 4를 돌리기 전에 Figma API 응답을 그대로 저장한 raw.json이 있다. 여기엔 원본 페이지명이 살아있다. 이걸 먼저 읽어서 폴더명을 추측하는 방식이었다.

if raw_path.exists():
    with open(raw_path) as f:
        raw_data = json.load(f)
        if "nodes" in raw_data and "root" in raw_data["nodes"]:
            node_name = raw_data["nodes"]["root"].get("name", "")
            # 예: "CPA 외부 입력폼 현황"

문제는 한글이었다. "CPA 외부 입력폼 현황"을 케밥케이스로 변환하면 cpa-외부-입력폼-현황이 되는데, 실제 폴더명은 cpa-custom-form이었다. 한글 → 영어 슬러그 변환이 안 됐다.

그래서 fallback을 추가했다. raw.json 변환이 실패하면 design_ir의 섹션 이름을 쓰는 방식이었다. "LNB"처럼 영어로 된 섹션명이라면 패턴 매칭으로 lnb-layout-provider.tsx 같은 파일을 찾을 수 있었다. 느리고 비효율적이지만 작동은 했다.

방법 2: --page-path 옵션으로 직접 지정

한글 페이지명 문제를 근본적으로 해결하는 방법은 사용자가 직접 경로를 넘겨주는 것이었다.

parser.add_argument("--page-path",
    help="페이지 경로 (/monitoring/cpa-custom-form) — 정확한 폴더 찾기용")

--page-path "/monitoring/cpa-custom-form"을 넘기면, Phase 5가 아래 순서로 폴더를 탐색했다.

candidates = [
    code_path_obj / "src/app/(auth)" / str(page_path) / "_source/components",
    code_path_obj / "src/app" / str(page_path) / "_source/components",
    code_path_obj / "src" / str(page_path) / "_source/components",
]

첫 번째로 존재하는 폴더를 찾으면 거기서만 스캔했다. 전체 730개 파일을 다 뒤지는 대신, 정확한 페이지의 44개 파일만 보게 됐다.

code_ir.json에 파일 정보도 남겼다

Phase 7(매칭 엔진)이 어느 파일 기준으로 스캔했는지 알아야 한다는 걸 뒤늦게 깨달았다. matched_filesource_dir를 메타데이터에 추가했다.

{
  "meta": {
    "timestamp": 1776069686,
    "total_nodes": 44,
    "matched_file": "/Users/taehoon/.../cpa-custom-form-page.tsx",
    "source_dir": "/Users/taehoon/.../cpa-custom-form/_source/components"
  }
}

orchestrate — --page-path를 Phase 5로 전달

orchestrate는 이미 앱 자동 감지 용도로 --page-path를 받고 있었다. 이걸 Phase 5에도 그대로 넘겨주면 됐다.

phase5_args = ["--code-path", code_path, "--node-id", args.node_id, "--design-ir", ...]
if phase5_page_path:
    phase5_args.extend(["--page-path", phase5_page_path])

--page-path 유무에 따른 차이

실제로 돌려보니 차이가 뚜렷했다.

항목--page-path 없이--page-path 포함
Code 노드 수118개44개
불필요 파일marketer-member 포함없음
diff 수1211개1186개

노드를 63% 줄이면서 불필요한 파일도 제거됐다. diff 감소폭이 크지 않은 건 Design 구조 차이가 주된 원인이라 어쩔 수 없는 부분이었다.

Phase 0-13통과 (20.9초)
pnpm type-check 통과
cpa-custom-form 파일 올바르게 매칭
marketer-member 불필요 포함 제거

흐름 정리

결국 이 작업의 핵심은 "Phase 4가 만든 정보를 Phase 5가 제대로 활용하게 만들기"였다.

--page-path 있음  →  src/app/(auth)/{page-path}/_source/components 직접 탐색
                       → 44개 노드, 정확한 매칭
 
--page-path 없음  →  raw.json 원본명 추출 시도
                       → 한글이면 실패
                       → design_ir 섹션명으로 fallback
                       → 전체 파일 스캔 (비효율)

--page-path가 있을 때가 확실히 빠르고 정확했다. 없어도 작동은 하지만, 한글 페이지명이 많은 프로젝트에서는 fallback에 의존하게 된다.