NOTES / 04 / 2026.10.21

디자인 파일을 아이콘의 원본으로 만들기.

지난 편

전환을 마치자 거의 모든 화면이 받던 아이콘 묶음 gzip 약 24KB가 사라졌다. 옛 Icon은 다시 쓰지 못하게 import를 lint로 막았고, 옛 파일은 다른 앱이 옮겨 갈 때까지 남겨 뒀다.

한 줄 결론

디자이너가 내보낸 SVG를 개발자가 손으로 고쳐 넣던 과정을 바꿨다. 이제 디자인 파일의 한 페이지를 원본으로 삼아 스크립트가 아이콘을 가져온다. 사람 기억에만 있던 손질 규칙은 코드로 옮겼고, 스크립트가 판단할 수 없는 것은 멈추지 않고 디자이너에게 물을 목록으로 남겼다.

손으로 하던 일

아이콘을 추가하는 과정은 이랬다. 디자이너가 Figma의 export 영역에서 SVG를 내보내 파일로 넘기면, 개발자가 몇 군데를 고쳐서 저장소에 넣었다. 고치는 내용은 이런 것들이었다.

  • 검정(#000000, black) fill을 지운다. 그래야 아이콘이 fill-current로 글자색을 따른다.
  • 루트의 width와 height를 지운다. 크기는 사용처의 클래스가 정한다.
  • 아이콘 전체를 덮는 의미 없는 clipPath를 지운다.
  • 흰색처럼 일부러 고정한 색은 남긴다.
  • 파일명을 이름-variant.svg 규칙에 맞춘다.

이 규칙은 어디에도 적혀 있지 않았고 해 오던 사람의 기억에만 있었다. 실제로 저장소에는 덜 손질된 파일이 몇 개 남아 있었다.

손질 규칙 이야기를 처음 들은 것은 2편의 생성기를 만들던 중이었다. 에이전트는 검정 fill을 지우는 처리를 생성기에 바로 넣었는데, 생성기 MR은 “SVG에서 컴포넌트를 만든다”는 일만 하는 MR이었다. 그래서 그 커밋을 빼고 메모로만 남겼다. 구조 전환을 다 끝낸 뒤 이 메모가 별도 작업이 됐다.

순서에도 이유가 있었다. 아이콘이 하나의 묶음으로 실리던 구조에 동기화부터 붙이면, 디자인 파일에 아이콘이 늘어날수록 모든 화면이 함께 무거워진다. 구조를 먼저 바꿔 두면 아이콘이 늘어도 그 아이콘을 쓰는 화면만 커진다.

먼저 원본이 어디인지 정하기

디자인 파일에는 아이콘이 두 군데 있었다. 디자이너가 파일을 내보낼 때 쓰던 export 영역과, Figma 컴포넌트로 묶어 둔 컴포넌트 영역이다. 둘 중 어느 쪽을 원본으로 삼을지 정하려고 저장소 SVG 370개를 두 영역과 비교했다.

비교는 그림으로 했다. Figma REST API로 SVG를 내보내 저장소 SVG와 같은 크기로 그린 뒤 픽셀을 비교했다. 한 가지 색만 쓰는 아이콘은 모양(alpha)만, 여러 색 아이콘은 색까지 봤다. 차이가 1% 미만이면 같다고 보고, 경계에 있는 것은 나란히 그려 눈으로 확인했다.

export 영역과의 1차 결과는 이랬다.

결과개수
일치307
색만 다름18
모양이 다름27
export 영역에 없음18

차이를 들여다보니 원인이 여러 가지였다. 먼저 export 영역과 컴포넌트 영역의 그림이 서로 다른 것이 있었다. 주문 유형 아이콘 13개는 export 영역 쪽이 획이 더 얇은 새 모양이었다. 디자인은 바뀌었는데 저장소만 옛 그림인 것도 있었다(종목 상세 아이콘 12개). 나머지는 export 영역의 이름이 중복되거나 오타인 것, 대소문자만 다른 것이었다.

컴포넌트 영역은 원본 후보에서 뺐다. 저장소와 이름이 다른 것이 107건이었고, 여러 색 아이콘의 다크 모드 변형이 없었다. export 영역은 카테고리가 텍스트 라벨의 위치로만 구분돼 있어서 스크립트가 읽을 수 없었다. 그래서 카테고리마다 Section으로 묶어 달라고 디자이너에게 요청했다.

디자이너는 export 영역을 별도 Icon Export 페이지로 옮기고 Section으로 나눠 줬다. 앞으로 일하는 방식도 함께 정했다. 디자이너는 그 페이지를 갱신한 뒤 바꾼 아이콘 이름만 알려 주고, 개발자는 스크립트로 코드에 반영한다. 디자이너는 코드 저장소에 접근하지 않으므로, 이 방식을 설명하는 문서는 사내 Pages에 올렸다.

손질 규칙을 코드로

손으로 하던 일을 정리 함수 하나로 옮겼다. 어떤 카테고리가 글자색을 따를지는 설정 파일에 둔다.

{
  "normal": { "dir": "src/core-ds/foundation/Icon/assets", "currentColor": true },
  "order": { "dir": "src/core-ds/foundation/ColorIcon/assets/order", "currentColor": ["orderApps"] }
}

currentColor가 true면 그 카테고리의 fill을 모두 지운다. 배열이면 그 이름의 아이콘만 지운다. 배열을 둔 이유는 order 카테고리 전체의 fill을 지우면 캔들 차트 아이콘의 빨강과 파랑까지 사라지기 때문이다. 그래서 검정으로 고정돼 있던 orderApps만 글자색을 따르게 했다.

규칙을 만드는 동안 지우면 안 되는 것도 하나 찾았다. 여러 색 아이콘의 루트에 있는 fill="none"을 지우면, 테두리만 있어야 할 도형이 검정으로 채워진다. 이 속성은 남기게 했다.

이 함수를 기존 SVG 전체에 한 번 적용하고 픽셀을 비교했다. 차이는 0이었다. 규칙이 실제 그림을 바꾸지 않고 파일만 정리한다는 뜻이다. 생성기의 --check도 같은 함수를 써서, 손질이 덜 된 SVG가 들어오면 CI가 실패한다.

동기화 스크립트

sync:icons는 이 순서로 동작한다.

  1. GET /v1/files/:key/nodes로 Icon Export 페이지를 읽는다. Section 이름이 카테고리, frame 이름이 파일명이다.
  2. 이름이 규칙에 맞는지 검사한다.
  3. GET /v1/images/:key?format=svg로 SVG 주소를 받고 파일을 내려받는다.
  4. 정리 함수를 적용하고, 정리한 뒤에도 규칙 위반이 남았는지 다시 확인한다.
  5. 디스크와 내용이 다른 파일만 쓴다.
  6. 아이콘 생성기와 컬러 아이콘 생성기를 실행한다.

토큰은 개인 액세스 토큰을 로컬 .env.local에서 읽는다. Figma MCP는 데스크톱 앱에 열린 탭만 읽으므로 스크립트에서 쓸 수 없다. 그래서 REST API를 골랐다.

멈추지 않고 보류하기

처음 실제로 돌렸을 때 스크립트가 중간에 멈췄다. 같은 이름의 frame이 두 개 있었고, 그림은 서로 달랐다. 어느 쪽이 맞는지는 스크립트가 정할 수 없다.

그렇다고 문제 하나 때문에 나머지 수백 개를 반영하지 못하면 곤란하다. 그래서 동작을 바꿨다. 판단할 수 없는 아이콘은 빼고 나머지를 반영한 뒤, 끝에 디자이너에게 보낼 확인 요청 문구를 Figma 링크와 함께 출력한다. 보류하는 경우는 이름 규칙 위반, 내보내기 실패, 정리 후에도 남은 위반, 그리고 frame에 배경이 들어간 경우다.

frame 배경 문제는 리뷰에서 발견했다. 두 아이콘에 연두색 배경이 들어 있었는데, 디자이너가 frame의 Fill을 켜 둔 상태로 내보낸 것이었다. 정리 규칙은 이것을 아이콘의 일부로 볼 수밖에 없어서, frame에 배경 fill이 있는 경우를 찾아 보류하는 검사를 추가했다.

스크립트가 일부러 하지 않는 일도 있다.

  • 이름 변경을 감지하지 않는다. 이름이 바뀌면 새 이름은 “추가”, 옛 파일은 “코드에만 있음”으로 나온다. 옛 이름을 쓰는 곳을 바꿀지는 사람이 정한다.
  • 코드에만 있는 파일을 지우지 않는다. 목록으로 보고만 한다. 디자이너가 알려 준 변경 목록을 넘기면 그것과 대조한다.

첫 동기화는 diff가 크다

첫 실행에서는 그림이 같아도 path 좌표 표기가 달라서 diff가 컸다. 이 결과를 기준으로 삼아 한 번 커밋했고, 그 뒤로는 연속으로 돌려도 바뀌는 파일이 0개다. 첫 커밋에서 그림이 실제로 바뀐 41개는 전후를 나란히 그린 비교 시트로 확인했다. 실제 화면에 영향이 있는 것은 그중 일부였다.

앱 개발자를 위한 다운로드 페이지

같은 아이콘을 쓰는 모바일 앱은 Flutter로 만들어서 컴포넌트 생성기를 쓰지 않고 SVG 파일을 직접 가져다 쓴다. 처음 요청은 “아이콘 전체를 zip으로 받게 해 달라”였다. 만들다 보니 필요한 건 전체 묶음이 아니라 아이콘 하나를 찾아 받는 일이었다. 그래서 검색 화면에서 하나씩 내려받는 페이지로 방향을 바꾸고, zip 다운로드도 남겨 두었다.

이 페이지는 Figma가 아니라 저장소의 SVG로 만든다. CI에는 Figma 토큰이 없고, 리뷰를 거치지 않은 디자인 변경이 앱에만 먼저 들어가는 것도 막고 싶었다. 저장소에 머지된 아이콘만 페이지에 나온다.

첫 실제 요청에서 바로 검색의 문제가 드러났다. 요청 글에 적힌 normal/cicleKRW로 찾으면 결과가 0건이었다. 카테고리/이름 형식을 받게 하고, 완전 일치, 앞부분 일치, 부분 일치 순으로 정렬하게 했다. 정확한 결과가 없을 때만 철자가 한두 글자 다른 것을 보여 준다. 검색 라이브러리는 쓰지 않았다.

자동화 대신 스킬

처음 그린 흐름은 끝까지 자동이었다. 디자이너가 Slack 워크플로로 요청하면 CI가 동기화 MR을 만들고, 머지되면 스레드에 “반영 완료”가 달린다. 하지만 이렇게 하려면 CI용 Figma 토큰(유효 기간 최대 90일), Slack 봇 토큰, 관리자 승인이 필요했다.

그래서 자동화를 바로 만들지 않고 에이전트 스킬로 절차만 고정했다. 스킬은 이렇게 동작한다.

  1. 요청 스레드에 처리 중 표시(모래시계 반응)를 단다. 두 사람이 같은 요청을 처리하지 않게 하려는 것이다.
  2. 스레드의 카테고리/이름 목록으로 대조 목록을 만들고 sync:icons를 돌려 MR을 만든다.
  3. 이름 변경이나 삭제 요청이 있으면 사용처 검색 결과를 보여 주고 사람의 판단을 기다린다.
  4. 머지 후 다운로드 페이지 배포가 끝나면 “반영 완료” 답글을 단다.

이 스킬로 실제 요청 두 건을 처리했다.

남은 일

  • 아이콘을 디자인 시스템에서 떼어 별도 저장소로 옮길 계획이다.
  • 자동 MR과 Slack 자동화는 이번 범위에서 뺐다.
  • Icon과 iconMap 삭제는 3편에서 쓴 대로 다른 앱의 전환을 기다린다.

돌아보며

네 편에 걸쳐 쓴 일을 한 문단으로 줄이면 이렇다. 번들러가 정적으로 판단할 수 있는 모양으로 구조를 바꾸고, 그 모양을 생성기와 codemod가 만들게 했고, 원본인 디자인 파일의 변경을 코드까지 반영하는 과정을 스크립트로 만들었다.

이 과정에서 여러 번 확인한 것은 세 가지다.

  • 검증은 통과 여부보다 무엇을 비교했는지가 중요하다. 실패 목록이 같다는 이유로 “회귀 없음”이 나왔고, 캐시 조건이 달라 2,396개가 다르다고 나왔다.
  • 도구가 말해 주지 않는 동작은 직접 확인한다. codemod는 건너뛴 곳을 말하지 않았고, oxlint는 설정을 합치지 않았고, 번들 검사는 작은 회귀를 놓쳤다.
  • 판단이 필요한 것은 멈추지 말고 목록으로 넘긴다. 동기화 스크립트가 모르는 것을 보류하고 확인 요청 문구를 출력하게 바꾼 뒤에야, 디자이너와 개발자가 각자 할 일을 나눌 수 있었다.
Notes 피드로 가기