문서 목차
이 페이지에서목차

첫 구현에서 Figma와 어긋나는 자리 — 측정으로 드러난 점검표

화면을 **처음** 만들 때 보는 점검표다. 한 제품의 전 화면 Figma 일치 작업(수백 건의 PR, 4천 장이 넘는 골든 이미지,

화면을 처음 만들 때 보는 점검표다. 한 제품의 전 화면 Figma 일치 작업(수백 건의 PR, 4천 장이 넘는 골든 이미지, 전체 RGBA 불일치율로 잰 결과)에서 나중에 고치느라 가장 비싸게 돌아온 것을 처음 구현 때 막는 것이 목적이고, 아래 수치는 그 작업의 실측이다. figma-to-coui 의 번역 명세(F0–F9)가 "무엇을 무엇으로 옮기나"라면 이 문서는 "옮기고도 어긋나는 자리와 그것을 처음부터 피하는 법"이다. 상태 목록 · 검증 사본 · 실행 검증 같은 절차는 전 화면 일치 작업이 소유하고, 비교 도구의 사용법은 골든 테스트 가이드 6절과 cc-pixel-loop:visual-verify 가 소유한다.

0. 무엇이 점수를 움직였나 — 구현 순서의 근거#

화면 하나의 일치율이 움직인 폭(전체 RGBA 불일치율, 402×874 · 라이트 · 글자 1배 기준)을 큰 순서로 적는다.

순위자리움직인 폭처음부터
1 빠진 요소 · 빠진 상태 — 숨은 버튼, 로딩 · 빈 · 오류 프레임, 시안과 다른 상태의 fixture 수 %p ~ 십수 %p (복원 한 건에 −4.5 · −9.7%p) §2
2컴포넌트 · Variant · Size — 손으로 그린 것을 시안의 CoUI 컴포넌트로−3.4 ~ −16%p§3
3넓은 면의 색 역할 · 재질−2.3 ~ −7.3%p (채널 차이가 3/255여도 면이 넓으면 %p 단위)§1
4쌓이는 오프셋 — 패딩 · 간격 · 치수 · 그림자−1.1 ~ −3.4%p§5
5글자 굵기 · 줄 상자 · 자간0.03%p 이하§4
  • 5번이 작다고 미루지 않는다. 서체를 처음부터 역할로 쓰면 비용이 0이고, 부분 체인 · 원시 TextStyle 로 만든 뒤 고치면 픽셀이 하나도 바뀌지 않는 리팩터가 수십 건 쌓인다. 반대로 1–2번이 맞기 전의 서체 미세 조정은 점수에 보이지 않는다.
  • 엄격한 전체 비교는 같은 색이 아닌 모든 픽셀을 센다. 그래서 면적이 큰 요소가 이긴다 — 작은 글자 속성을 맞추는 일로 한 화면을 임계 아래로 끌어내리지 못한다.

1. 입력을 읽는 법 — 코드 출력이 아니라 노드 값#

  • 생성된 디자인 컨텍스트가 생략하거나 왜곡하는 값은 노드에서 직접 읽는다. fill 마다의 paint opacity(노드 opacity 는 1인데 fill 이 0.22), 보이는 stroke 없이 strokeWeight 만 있는 경우, 효과의 spread · alpha(컨텍스트는 0, 노드는 −1 · 0.05), 숨은 레이어(visible), 줄 상자의 정수 px. design-spec.figma.js 가 이 값을 노드에서 읽는다.
  • 색은 hex 가 아니라 바인딩된 변수 이름으로 옮긴다. 라이트에서 같은 값인 두 토큰(surfaceContainerLowest · surfaceBright)이 다크에서 갈라지고, 같은 hex 가 두 역할에 쓰인 시안도 있다. 변수 id 를 이름으로 풀어 앱 테마가 덮어쓴 값과 대조한다 — 라이브러리 기본 팔레트만 보면 앱이 덮어쓴 색에서 회귀한다. 다크 프레임이 없으면 다크의 기준도 없다.
  • 변수는 시작할 때 한 번에 옮긴다. Figma 변수 → 앱 테마 역할 표를 라이트 · 다크 모두 만들고, 매핑하지 않은 역할은 라이브러리 기본값이 된다(시안의 error-container 를 빠뜨리면 기본값이 그려진다). 화면 비교에서 걸릴 때마다 한 역할씩 옮긴 작업은 같은 4천여 장을 다섯 번 다시 찍었다. 채널 차이가 3/255뿐인 역할 하나로 한 화면이 5.3% → 3.0% 가 됐다 — 면이 넓으면 눈에 안 띄는 색 차이도 %p 단위다.
  • 전역 토큰의 값을 바꾸기 전에 그 역할을 쓰는 Figma 소비처를 모두 센다. 한 역할의 전역 값만 시안에 맞추는 후보를 측정하니, 같은 역할을 쓰는 다른 화면 600여 장이 함께 움직였다. 소비처마다 시안이 다른 변수를 가리키면 역할을 나눈다.
  • 시안의 값이 접근성 대비(본문 4.5:1)에 못 미치면 적용하지 않고 잔여 차이로 기록한다. 접근성이 시안을 이긴다.
  • DS 를 올리면 Figma 쪽 변수 · 인스턴스도 같은 릴리스로 맞춰야 한다. 코드의 팔레트만 갱신되면 같은 화면의 수치가 라이브러리 핀만 바뀌어 움직인다. 핀을 올릴 때 Figma 파일의 변수 동기화(CoUI Token Sync) 여부를 기록한다.

2. 시안을 구현 단위로 분류한다#

  • 요소마다 지원 · 미지원 · 정책 충돌 · 제품 계약 공백으로 가른다. 시안에만 있는 데이터 · 기능(통계, 타이머, 조회수, 법적 본인인증 문구)을 앱이 지어내 채우지 않는다 — 제품 계약 공백으로 디자인 단계에 돌려보낸다. 분류하지 않으면 그 요소가 가장 큰 잔여 차이로 남는다. 가장 큰 개선 중 하나는 색 · 간격이 아니라 서버가 값을 주도록 계약을 넓힌 한 건이었다.
  • 시안 문구가 제품이 보증하지 못하는 약속(본인확인 보장, 지도 표시 약속, 남은 횟수)이면 복사하지 않는다. 승인된 검증 사본의 범위 안에서만 정책에 맞추고, 그 효과는 UI 개선으로 세지 않는다.
  • 시안이 그린 상태와 fixture 의 상태를 먼저 맞춘다. 시안의 샘플 상태(목록 행 수, 경고 배너, 입력 단계, 스크롤 위치)와 fixture 가 다르면 올바른 구현이 수치를 올린다. fixture 는 프로덕션 조회 의미(정렬 · 중복 제거 · 개수 · 콜백 · 지도 타일)를 따른다 — 의미를 따르게 고치자 모든 지표가 나빠진 적도 있다. 맞춘 뒤의 수치와 전의 수치는 비교하지 않는다.
  • 시안 주석이 정한 동작(정렬 순서, 빈 상태 문구 횟수, 20의 배수에서의 빈 페이지)은 서버 계약에 없으면 구현 시점의 수용 테스트로 고정한다. fixture 의 동률이 정렬 결함을 가리므로 테스트는 고유 순서를 쓴다.
  • 시안이 약속한 요소가 프로덕션 빌드 · 플랫폼 구성에서도 나타나는지 본다. 환경값이 빌드에 전달되지 않아 버튼이 통째로 숨은 사례, 플랫폼 이름 대소문자 차이로 요청이 실패해 첫 화면에 갇힌 사례가 있었다.
  • 서드파티 SDK 가 그리는 기본 위젯(로그인 버튼 등)은 DS 컴포넌트와 시안 자산으로 바꾸고 SDK 의 컨트롤러 · 흐름만 유지한다. 기본 위젯은 시안과 크기 · 글꼴이 다르고 골든에서 글리프가 빌 수 있다.

3. 컴포넌트와 상태#

  • 인스턴스에 준 스타일은 상태 외형을 이긴다. 위젯 파라미터(xxxStyle)는 테마 · 변형 · 기본값 위에 놓인다(병합 순서는 coui-composition-and-extension). 오류 · 선택 · 비활성 같은 상태가 있는 컴포넌트에 쉬는 모양(테두리 색 · 전경색)을 인스턴스에 적으면 상태 변형이 그 필드를 바꾸지 못한다 — OTP 칸에 borderColor 를 모든 상태에 적었더니 오류 칸의 빨간 테두리가 사라졌다. 크기 · 변형 프리셋에 위임(size: · variant:)하고 프리셋과 다른 값만 덧쓴다. 확인은 오류 · 선택 상태에서 해석된 paint 색을 읽는 테스트다.
  • 합성 컴포넌트에 준 스타일이 실제 리프에 닿았는지 해석된 값으로 본다. 전화번호 입력에 17px · 600 을 준 슬롯이 국가 목록 글자에만 닿고 입력한 번호는 14px · 400 으로 남았다. 테스트가 통과해도 입력 필드(EditableText)까지 닿았다는 뜻이 아니다. 리프로 가는 슬롯이 없으면 앱에서 덧대지 말고 CoUI 에 먼저 올린다.
  • 크기 Variant 는 글꼴 · 내부 간격 · 반경 한 묶음이다. 시안이 Size=sm 인데 md 를 쓰고 일부 값만 덮으면 일부만 맞는다. 시안의 Variant · Size 를 그대로 고른다(F3). 체크 상자만 24px 로 키우고 체크 아이콘은 기본 10px 로 남기는 식의 부분 확대도 같은 결함이다.
  • 컴포넌트의 기본 최소 높이가 행을 부풀린다. 시안의 행이 글자 높이(25px)인데 평범한 Button 의 기본 높이 40 이 아래를 전부 밀었다. 그 버튼 하나의 높이만 명시한다.
  • 선택 · 오류 상태의 테두리 두께 보정이 높이를 바꾼다. 선택 카드가 선택 때 테두리가 두꺼워지며 패딩을 줄여 51px 를 유지했는데 시안은 52px 였다. 상태별 치수를 시안에서 읽고, 상태를 바꿀 때 높이가 같은지 단언한다.
  • 0폭 테두리는 헤어라인을 그릴 수 있다. 컴포넌트에 따라 borderWidth: 0 만으로는 1px 선이 남으므로 borderColor: transparent 를 함께 주고 렌더로 확인한다. 포커스 링과 쉬는 테두리가 겹치면 이중 경계가 된다.
  • 라이브러리 기본값이 시안과 다르고 제품 전체에서 반복되면 인스턴스마다 덮지 않는다. 다이얼로그 하나의 그림자 단계 · 본문 간격 · 중앙 정렬 · 재질을 열 곳에서 따로 덮은 사례가 있었다. 한 곳(CoreComponentTheme)에 두는 것이 F6-4 이고, 덮는 값이 제품 결정인지 DS 의 결함인지는 시작 전에 정한다(§7).
  • 열린 상태를 골든 · 위젯 테스트에 넣는다. 메뉴 · 선택 팝업 · 다이얼로그는 닫힌 화면만 찍으면 좁은 팝업에서 통화 코드가 여러 줄이 되는 것도, 조상이 스크롤될 때 닫히는 것도 보이지 않는다. 가려지는 액션(결과가 4개일 때 화면 밖으로 밀리는 동의 버튼)과 입력 안쪽 아이콘의 실제 hit-test 도 이 단계에서 본다.

4. 서체#

(역할로 옮기는 법 · 상속 · 단위는 coui-text 의 「기존 화면을 역할 기반으로 옮기기」가 소유한다. 아래는 그 위의 점검이다.)

  • 역할은 Figma 텍스트 스타일의 이름(바인딩)으로 고른다 — 크기가 같다고 고르지 않는다. 휴대폰에서 24px 제목에 headlineSmall 을 쓴 것은 예전 데스크톱 24px 이 우연히 맞았기 때문이고, 반응형 서체 단계가 들어오자 16px 이 됐다. 시안이 Headline/Large 에 묶였으면 headlineLarge 다. 크기 하나를 fontSize 로 고정하는 전역 override 는 하지 않는다.
  • 역할에 없는 조합(14px Bold, 18/24 SemiBold, 행간 160%, 17px · −0.15px)은 목록으로 모아 디자이너에게 확인한다. 가까운 역할에 값만 덧쓰는 것은 확인된 뒤의 일이다.
  • 굵기는 노드 값으로만 정하고, 호출 지점에서 명시한다. 역할과 컴포넌트의 기본 굵기는 릴리스 사이에 바뀐 적이 있다(타이틀 700 → 600, 버튼 라벨 600 → 500). 기본값에 기댄 호출부는 핀을 올릴 때 조용히 어긋나고, 그 뒤처리로 굵기를 고정하는 PR 이 열여덟 건 이어졌다. 제품이 쓰는 굵기는 앱 테마나 공유 래퍼 한 곳에서 선언하고, 아래의 해석 값 단언을 핀 경보로 쓴다.
  • 줄 상자는 Figma 의 정수 px 를 읽어 비율로 환산한다(13px 글자의 줄 상자 20px → 20 / 13). 비율(%)을 그대로 곱하면 반올림이 달라지고, 정수 줄 높이를 일괄 적용하면 HUG 높이가 소수인 노드에서 오히려 악화한다. coui-text 의 예와 같다.
  • 자간 단위가 다르다. CoreTextStyle · 체인은 em(−1% = −0.01), Flutter TextStyle 은 논리 px(−0.14px)다. 섞어 쓰지 않는다.
  • 부분 체인(크기만 · 굵기만)과 원시 TextStyle 은 서체 · 행간 · 자간을 조상의 DefaultTextStyle 에서 물려받는다. CoUIApp 아래에서는 앱 서체지만 MaterialApp 아래의 위젯 테스트에서는 Roboto 의 행간 · 자간으로 풀려, 테스트와 골든이 서로 다른 글자를 본다. 골든은 이 폴백을 못 잡는다. 해석된 스타일(RichText 의 family · size · weight · height · letterSpacing)을 CoUIApp 아래에서 단언한다. 버튼 · 배지 안의 글자는 자식에 역할 체인을 붙이지 말고 컴포넌트의 라벨 슬롯을 쓴다(체인이 전경색을 다시 입혀 variant 의 색을 덮는다).
  • 크기만 고르는 .s14 류가 남아 있으면 감사 대상이다. 한 프로젝트의 감사에서 화면당 수십 곳이 나왔고, 역할 체인으로 바꾼 뒤에도 수천 장의 이미지가 한 장도 바뀌지 않았다 — 처음부터 역할이었으면 필요 없던 작업이다.
  • 기계로 막으려면 check-text-roles 를 CI 게이트에 건다(cob_project_tools 를 쓰는 저장소의 tool/project check-text-roles). 역할 없는 크기 체인(Text(…).s12)과 서체 · 크기 · 행간 · 자간을 준 원시 TextStyle 만 잡고, 역할 위의 덧쓰기 (.bodySmall.s12) · CoreTextStyle 은 막지 않는다. 기존 위반은 허용 목록(경로 # 사유)이나 그 줄의 // text-role: allow — 사유 로 시작해 줄여 가는 래칫이다(허용이 더는 필요 없으면 낡은 항목으로 실패한다). 이 게이트를 처음 켠 제품에서는 두 곳이 잡혔다 — 한 곳은 역할(Body/Small 13 · 150% · −1%)과 수치가 같아 옮겨도 골든이 한 장도 바뀌지 않았고, 다른 한 곳은 가까운 역할로 옮기자 146px 이 달라져 허용으로 남겼다. 옮기기 전에 그 화면의 골든을 먼저 돌려 본다.

5. 치수 · 표면#

  • 고정 높이 · 고정 폭 대신 최소 크기 + Figma 패딩 + 자연 성장. 시안의 높이는 그 글자 · 그 언어에서의 결과다. 49px 행을 height: 49 와 패딩 0 으로 옮기면 큰 글자 · 긴 번역에서 넘친다. IntrinsicHeight 는 번역에서 높이를 작게 계산한다 (scale-invariant-layout).
  • Figma 프레임에는 기기 인셋이 없다. 시안의 하단 탭바 패딩(위 8 · 아래 28)은 한 기기의 안전영역을 박은 값이다. 콘텐츠 영역과 실기기 인셋을 나누고, 하단 고정 CTA 는 max(기기 안전영역, 시안의 최소 여백)(SafeArea(minimum:))으로 둔다. 인셋 0 · 20 · 34 에서 단언한다.
  • 간격은 이웃 간격의 외삽이 아니라 노드의 절대 좌표 차로 읽는다(외삽한 18 은 틀렸고 16 이었다). 단, 박스 좌표가 맞는다고 잉크가 맞는 것은 아니다 — 간격 · 배지 패딩을 메타데이터에 맞춘 변경이 점수를 올리지 않은 사례가 많았다. 기하 변경은 바꾼 뒤 점수를 재고 채택하고, 되돌린 후보와 수치를 기록한다(§7).
  • 스크림은 시안의 검정 50% 를 명시한 8비트 알파(0x80000000)로 쓴다. 0.5 같은 부동소수 알파는 8비트로 바뀔 때 한 단계 어긋나(흰 바탕에서 128 대 127) 넓은 면 전체가 다른 색이 된다. 배리어 색의 알파가 컴포넌트 기본 opacity 와 곱해지는 컴포넌트(Drawer)는 의미에 맞는 슬롯(barrierOpacity)을 쓴다.
  • 그림자는 시안의 컴포넌트 설명 · 효과 노드에서 정한다. 다이얼로그 시안이 "배리어가 이미 배경을 가리므로 패널 그림자는 shadow/sm" 이라고 적었는데 앱이 상속한 단계는 xl 이었다. 이 한 줄이 그 화면들에서 −8 ~ −15%p 를 만들었다.
  • 전체 화면 중앙에 둔 모달은 위 · 아래 안전영역이 비대칭(위 62 · 아래 34)이면 시안보다 14px 낮게 앉는다. SafeArea 를 유지한 채 실제 자식 크기 · 제약으로 비대칭을 보정한다.

6. 언어 · 확대 · 접근성은 첫날부터#

  • 화면마다 11개 언어 × 글자 배율 {1, 1.3, 2} × 폭 320 × 키보드 매트릭스를 실제 번들 글꼴로 돌린다. 기본(ko · 1배) 골든은 이 수정 전후로 바이트까지 같아야 한다 — 같지 않다면 기본 화면을 건드린 것이다. 라벨과 버튼을 한 Row 에 둔 줄이 독일어 67px · 러시아어 19px 로 넘친 사례가 전형이다.
  • 번역 키는 UI 문맥(배지 · 필터 · 제목 · 표 머리)별로 나눈다. 필터용 긴 문구가 배지 열을 넓혀 누적 열 이동을 만들었다.
  • 번역에 새 글자(한자 · 아랍 문자 · ≈)가 들면 번들 글꼴의 서브셋이 깨진다. 글꼴 재생성 경로를 모든 개발 기계에서 쓸 수 있게 두고, 글리프 게이트를 CI 에 둔다.
  • 선택 상태 · 토글 라벨 · 동의 체크 · 배지의 의미는 픽셀에 보이지 않는다. SemanticsAction 기반 위젯 테스트(선택됨 + 상호 배타 그룹, MergeSemantics, 터치 영역 44×44)로 처음부터 고정하고, 점수가 나빠져도 컨트롤을 빼서 점수를 지키지 않는다 (flutter-a11y). 입력은 보이는 라벨을 접근성 이름으로 노출한다.
  • 오류 · 상태 문구는 하드코딩 색 대신 테마 텍스트 + live region 으로 쓰고 다크 · 작은 화면 · 확대에서 본다.
  • 표시 형식은 표시 계층에서만 만든다. 천 단위 구분 · 로케일 표기는 표시용 컨트롤러가 하고 정규 입력 · 정밀도 · IME 조합 구간은 건드리지 않는다.

7. 시작 전에 정할 것#

  • "승인된 라이브러리 기본값 유지"와 "시안과 일치"가 부딪힐 때의 규칙을 작업 시작 전에 정한다. 경계가 문서에 없으면 PR 마다 다르게 판단한다. 권장: 색 역할은 앱 테마 한 곳에서 시안을 따르고, 굵기 · 간격은 호출 지점에서 시안 값을 명시하고, 접근성이 시안을 이기며, 컴포넌트 구조(상태 · 슬롯 · 치수)가 시안과 다르면 CoUI 에 먼저 올린다.
  • 라이브러리 핀 갱신은 "핀 + 영향 캡처 + 기준선"을 한 번에. 런타임이 바뀐 핀만 올리고 나머지는 릴리스 때 올린다. 상류의 기본값 변경(반응형 서체처럼 여러 화면에 닿는 것)은 핀에 들이기 전에 프리뷰로 합성해 전수 캡처 비교한다.
  • 진단 · 실험 캡처는 제품 골든 디렉터리 밖에 두고 명시 등록한다. 기준 이미지가 없는 시나리오가 기본 비교를 깨뜨린다.
  • 같은 파일을 여러 세션이 고치지 않게 소유 범위를 선언한다. 한 작업이 진행되는 사이 같은 파일을 먼저 머지한 PR 이 일곱 건이었다.

8. 측정과 보고#

측정 규율(지표 분리 · 바닥값 · 같은 핀 비교 · 참조 고정 · 키별 델타 · 기각 기록)은 전 화면 일치 작업의 「측정 규율」과 「기준선 갱신」을 따른다. 첫 구현에서 특히 기억할 두 가지만 적는다.

  • 올바른 수정이 수치를 올릴 수 있다. 해석된 스타일 · 치수 단언(수정 전 실패 → 수정 후 통과)으로 정확성을 증명하고, 명세에 맞는 값을 점수 때문에 되돌리지 않는다.
  • 잔차가 데이터 · OS 상태바 · 글자 래스터로 설명되면 멈춘다. 요소가 적은 상태 화면만 2% 아래에 닿았고, 본문 위주 화면은 1.9 ~ 2.1% 에 모였다. 마스크로 가리거나 가짜 상태바를 그려 임계를 넘기지 않는다.

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기