Figma 라이브러리 | CoUI
본문으로 건너뛰기
LogoCoUI

Figma 라이브러리

CoUI 컴포넌트를 생성/관리하는 정본 Figma 라이브러리가 어디인지

목차

정본 파일 — CoUI — Cocode Design System v1.0 을 엽니다. 값은 coui_core 가 정본이고, 이 파일은 그 투영입니다.

tools/figma-token-sync 플러그인(Generate Components)은 실행하는 순간 열려 있는 Figma 문서를 대상으로 컴포넌트를 만들거나 갱신합니다 (figma.root) — manifest.json 에는 특정 파일에 대한 바인딩이 없습니다. 즉 누가 다른 문서에서 플러그인을 실행하면 거기에 병렬 컴포넌트 세트가 생깁니다 — 정체성 보존은 "항상 같은 문서에서 재실행한다"는 전제 위에 있고, 그 전제가 사실인지 확인하는 곳이 바로 여기입니다.

정본#

필드값
식별자coui-component-library
표시 이름CoUI Component Library
Figma URL CoUI — Cocode Design System v1.0

값의 단일 출처는 docs_core/content/_data/figma_library.json 한 파일입니다. 정본 문서가 바뀌면 이 파일의 값만 갱신합니다 — 플러그인이나 exporter 코드는 건드리지 않습니다. 이 파일은 packages/coui_core/bin/export_figma_tokens.dart 가 빌드 시점에 읽어 build/tokens.figma.json 의 meta.canonicalLibrary 에 실어 보내고, esbuild --bundle 이 그 JSON을 code.js 에 인라인합니다 (components.json 과 같은 경로 — 플러그인은 런타임에 아무것도 조회하지 않습니다. manifest.json 의 networkAccess: { "allowedDomains": ["none"] } 는 그대로 유지됩니다).

조직 파일은 권한이 있는 계정에서만 열립니다. 링크가 막히면 조직 멤버십을 확인하세요. 플러그인이 "지금 열린 문서가 정본인가"를 `figma.fileKey` 로 기계 대조하는 일은 아래 A-5 가 풀리기 전까지는 사람이 이 URL 을 보고 판단합니다.

Foundation ↔ 문서#

캔버스의 Foundation 페이지와 소비자 가이드를 같은 축으로 맞춥니다. 노드 id 는 구조 스냅샷 기준이며, 페이지를 새로 만들면 바뀔 수 있습니다 — 그럴 때는 파일에서 페이지 이름으로 찾으세요. 값이 갈리면 coui_core 가 이깁니다.

Figma Foundation문서
00. Overview 디자인 토큰
01. Color 색상
02. Typography 타이포그래피
03. Layout & Space 레이아웃
04. Shape 형태
05. Effect 효과
06. Motion 애니메이션
07. Layering 레이어링
08. Icon 아이콘

컴포넌트 페이지(❖ Button 등)의 slug → node-id 는 같은 JSON 의 pages 맵입니다. 문서의 구조 (Anatomy) 섹션이 그 링크로 페이지를 엽니다.

매핑의 근거와 낡음 검사#

Anatomy는 코드 슬롯을 설명하고, Figma 링크는 캔버스의 해당 페이지를 찾는 진입점입니다. 두 문서가 같은 node-id를 쓴다는 것만으로 그 id의 실재가 증명되지는 않습니다. docs_core/test/markdown/figma_library_snapshot_test.dart는 기존 플러그인 구조 캡처의 파일 식별자와 페이지 이름·id를 맵과 대조합니다. 페이지가 없어졌거나, 그 id가 다른 컴포넌트의 것이거나, 캡처가 다른 라이브러리 파일에서 왔으면 실패합니다.

# docs_core 디렉터리에서
dart test test/markdown/figma_library_test.dart test/markdown/figma_library_snapshot_test.dart

이 검사는 커밋된 캡처를 기준으로 합니다. 라이브 Figma의 최신 상태를 조회하는 검사는 아닙니다. 캔버스를 변경한 뒤에는 구조 스냅샷 경로로 캡처를 갱신하고 다시 대조합니다. 캡처로 확인할 수 없는 페이지는 파일 URL과 페이지 이름까지만 링크하며, 숫자 id나 별도의 손 관리 매핑 목록을 새로 만들어 채우지 않습니다.

테마 주입·서페이스 프리셋·밀도는 코드 축입니다 — 테마, 서페이스 스타일. 그중 서페이스 스타일의 기하(반경·선 두께)와 밀도(간격·컨트롤 행 높이·행을 따르는 모서리)는 캔버스에서도 볼 수 있습니다 — 프레임에 Semantic/ Surface 컬렉션의 모드(neoBrutalism · claymorphism 등)를 고정하면 그 스타일의 모서리와 선이, Semantic/ Dimension 컬렉션의 모드(compact · spacious)를 고정하면 그 밀도의 간격과 행이 보입니다. 둘을 함께 고정하면 코드와 같은 순서(밀도 먼저, 스타일 나중)로 합성됩니다. 스타일이 그림자에 하는 일(단단한 오프셋 그림자 · 부풀린 그림자 · 거울 쌍)과 재질(반투명 채움 · 블러 · 굴절 · 림, 스타일이 더하는 보더와 그림자)도 그 모드를 따라 보입니다 — 표면의 채움이 그 뒤의 material 레이어에 그려지기 때문입니다. 다크 모드의 몫 · 다크의 그림자 세기(알파 ×4) · 채도 · 가장자리 광택은 캔버스에 없습니다 — 그림자 행의 알파는 라이트 세기입니다. 결정 문서의 우선순위는 디자인 소스.

타이포는 스타일로 적용한다#

화면의 텍스트에는 텍스트 스타일(Headline, Title, Body, Label …)을 붙인다. 같은 값을 만드는 변수(font-size/*, letter-spacing/*)를 텍스트 노드에 직접 걸지 않는다 — 변수 직접 바인딩은 색과 간격에만.

이유는 두 가지다.

  • 변수는 크기와 자간만 나르고 굵기와 행간은 노드에 남는다. 그래서 같은 역할의 제목이 화면마다 다른 굵기로 그려져도 아무것도 경고하지 않는다. 실제로 화면 제목 150여 개가 당시 시스템 Headline(SemiBold · 140%)과 다른 Bold · 160% 로 그려진 채 발견됐다.
  • 스타일 위에 변수를 겹치면 변수가 이긴다. 시스템에서 스타일을 고쳐도 그 노드만 안 따라온다.

점검법 — 텍스트 노드를 골라 오른쪽 패널을 본다.

보이는 것판정
Text 섹션에 스타일 이름 (Body / Medium 등)✅
스타일 없이 크기 옆에 변수 칩❌ 스타일을 붙이고 변수 칩을 뗀다
스타일 이름 과 변수 칩이 같이❌ 변수 칩을 뗀다 — 스타일이 다시 이기게

컴포넌트 페이지(❖ …)는 플러그인이 생성하고, 같은 규약을 플러그인이 지킨다 — 라벨이 자기 역할 그대로면 그 역할의 스타일을 붙인다. 라벨이 그리는 크기·굵기·행간·자간·글꼴 중 하나라도 역할과 다르면(크기 사다리의 한 칸, Core 가 역할 위에 굵기나 행간을 얹은 자리) 스타일 없이 변수 칩으로 남는다. 스타일은 다섯을 한 묶음으로 덮어쓰기 때문에, 붙이면 그 라벨의 굵기·행간이 역할의 것으로 바뀐다. 그래서 컴포넌트 페이지에서 위 표의 두 번째 줄 모양은 결함이 아니다.

스타일 자체는 코드에서 온다 — 손으로 고치지 않는다#

텍스트 스타일 22개(Display/Large … Code/MD — Label/XL·Label/XL2·Body/XS·Label/XS·Code/XS·Code/SM 포함)는 Import Tokens 가 변수와 함께 이름으로 upsert 한다. 크기·자간·굵기·글꼴은 시맨틱 변수(font-size/*·letter-spacing/*·font-weight/<그룹>)에 묶이고, 행간은 export 가 준 % 리터럴로 쓰인다(Figma 는 행간에 묶인 숫자 변수를 단위와 무관하게 px 로 읽어 비율을 변수로 나를 수 없다). Code/* 셋만 다르다 — 줄 높이가 비율이 아니라 21px 고정이라 PIXELS 로 쓰고 px 변수 line-height/code 에 묶는다. 이 값은 px 로 읽히는 것이 맞다.

컴포넌트 페이지의 라벨은 두 단으로 자간 변수를 찾는다 — 역할이 살아 있으면 letter-spacing/<role>(붙은 스타일이 나른다), 역할이 빠진 자리(Checkbox·RadioGroup 의 lg 라벨처럼 크기가 다른 곳, 역할 위에 굵기·행간을 얹은 곳)면 크기 키 letter-spacing/size-<n>. 두 번째 단은 변수 값이 노드의 자간 리터럴과 같을 때만 묶인다 — 다르면 리터럴로 남고, 그 리터럴은 그 자리의 자간이 아직 정해지지 않았다는 정직한 표시다(묶으면 변수가 리터럴을 덮어 그림이 바뀐다).

그래서 스타일의 굵기·행간이 틀려 보이면 스타일을 편집하지 말고 코드의 역할(CoreTypography)을 고친 뒤 export → Import 를 다시 돌린다. 스타일에 직접 쓴 값은 다음 Import 가 덮어쓴다. 플러그인의 드리프트 점검이 스타일 절을 따로 보고한다 — 없는 스타일, 풀린 바인딩, export 와 다른 굵기·행간.

고정폭 서체 — Geist Mono 를 설치한다#

Code/* 스타일과 컴포넌트 페이지의 코드 라벨은 Geist Mono 로 그려진다. 플러그인을 돌리는 컴퓨터에 Geist Mono 정적 굵기 넷(Regular · Medium · SemiBold · Bold) 을 설치한다 — 저장소의 docs_site/fonts/GeistMono-*.otf 가 코드가 번들한 것과 같은 판이다. 빠진 굵기가 있어도 Generate 는 멈추지 않는다. 그 굵기의 코드 라벨이 본문 서체로 그려지고, 실행 로그에 한 줄 남는다 — 캔버스의 코드가 본문 서체로 보이면 먼저 이것을 확인한다.

Geist Mono 에는 한글이 없다. 코드와 웹은 한글 구간을 Pretendard 로 그리지만, Figma 는 대체 서체를 고르지 못하므로 플러그인이 고정폭 글 안의 한글 구간을 Pretendard 로 칠한다. 컴포넌트 페이지는 Generate 가 알아서 칠한다. 디자이너가 화면에 직접 적은 고정폭 글은 라이브러리를 다시 Generate 해도 바뀌지 않으므로, 그 페이지에서 플러그인의 한글 구간 칠하기(「고정폭 글의 한글을 본문 서체로」)를 돌린다 — 프레임을 고르면 그 안만, 아무것도 고르지 않으면 페이지 전체를 칠한다. 글을 고쳐 쓰면 새 글자가 옆 글자의 서체를 따르므로 다시 돌린다.

구조 스냅샷 — 캔버스가 무엇을 바꿨는지 코드가 알게 하는 법#

플러그인의 구조 → .json 을 누르면 파일 전체(모든 페이지)의 노드 구조와 기하(id · 이름 · 타입 · x/y/w/h)가 파일 하나로 내려온다. 그 파일을 레포의 tools/figma-token-sync/reverse/incoming/ 에 커밋하면, 구조 드리프트 대조가 지난 베이스라인과 비교해 "디자이너가 무엇을 추가·삭제·이동·이름 변경했는지" 를 이슈로 남긴다.

이 버튼이 있는 이유: 자동화가 MCP 로 이 파일을 걷으면 페이지 목록 버그와 호출 예산 때문에 Cover 한 장밖에 못 본다. 플러그인은 파일 안에서 돌아 그 한계가 없다. 색 · 토큰 바인딩 · 타이포는 이 스냅샷에 없다 — 그건 충실도 → .json 이 답한다.

대조는 경고이지 차단이 아니다#

플러그인은 실행 시 현재 문서의 root pluginData에 스탬프가 있는지 봅니다. 없거나 다르면 "이 문서는 기록된 CoUI 라이브러리가 아닙니다 — 계속하면 별도 세트가 생깁니다" 확인을 요구하되, 계속 진행은 항상 허용합니다. 승인자가 명시한 대로 정본은 언제든 바뀔 수 있어야 하고, 차단형 게이트는 정본 교체 시점에 플러그인을 못 쓰게 만들어 오히려 사람이 우회하게 만듭니다.

A-5 (미결) — private org plugin 배포#

figma.fileKey 로 정확한 문서 대조(같은 파일인지 실제로 확인)를 하려면 이 플러그인이 private organization plugin 으로 배포돼야 합니다 (plugin-api.d.ts: fileKey 는 enablePrivatePluginApi 가 있는 private 플러그인에서만 접근 가능 — 현행 개발모드 로드에서는 undefined). 배포 전까지는 root pluginData 스탬프만으로 "처음 보는 문서인가"는 구분되지만 "이게 바로 그 정본 문서인가"까지는 확인할 수 없습니다 — 그 판단은 사람이 이 페이지의 URL 을 보고 합니다.

Organization 이상 플랜 확인이 필요해 별도로 결정되지 않았고, 나머지 작업과는 병렬로 진행 가능해 착수를 막지 않습니다.