들어가며
우리 디자인 시스템에는 아이콘이 273개 있어요. 얼마 전까지 이 아이콘들을 관리하는 방식은 이랬어요.
디자이너가 Figma에서 아이콘을 수정하면, 코드에 반영하기 위해 아사나(Asana)에 앱·웹 개발자용 태스크가 각각 올라와요. 각 개발자는 그 태스크를 받아 아이콘을 코드로 옮기고, 마지막에 디자이너가 반영 결과를 확인해요.
아이콘 하나 바뀌는데 여러 사람의 손과 시간이 필요했던 거죠.
이 글은 이 과정을 매주 자동으로 도는 파이프라인으로 바꾼 이야기예요. 그중에서도 웹을 자동화한 이야기를 다뤄요.
기존 아이콘 관리 방식과 문제점
이 흐름을 조금 더 풀어보면 이래요.
디자이너가 Figma에서 아이콘 추가/수정
→ 아사나에 "아이콘 반영" 태스크 생성 (앱용 · 웹용 각각)
→ 각 플랫폼 개발자가 태스크 받아 SVG를 코드로 옮김
→ 디자이너가 결과 컨펌한두 번이면 부담이 없어요. 문제는 아이콘이 매주 바뀐다는 거였고, 그때마다 이 왕복이 플랫폼 수만큼 반복됐어요.
디자이너는 태스크를 만들고 컨펌까지 해야 하고, 앱·웹 개발자는 같은 아이콘을 각자 따로 옮겨야 했어요. 누구 하나 바쁘면 그 주 반영이 통째로 밀렸고요.
사람이 하다 보니 실수도 잦았어요. 배럴에 export를 빼먹어 빌드가 깨지거나, currentColor 치환을 놓쳐 다크모드에서 색이 안 바뀌는 식으로요.
아이콘 하나 옮기는 건 5분이면 끝나요. 진짜 비용은 거기 붙는 사람과 왕복, 그리고 컨펌이었어요.
자동화 방향과 전체 흐름
그래서 목표를 이렇게 잡았어요.
디자이너가 Figma에서 바꾸면, 각 플랫폼은 알아서 반영한다. 사람은 최종 확인만.
매주 월요일 오후 3시 30분, Figma를 기준으로 각 플랫폼(앱·웹)에 바뀐 아이콘이 있으면 자동으로 반영하기로 했어요. 디자인팀이 3시에 그 주 아이콘을 모아주니, 그 직후로 맞춘 거예요.
이 글에서 다루는 건 웹이에요. 웹은 이런 흐름으로 자동화했어요.
Figma (원본)
→ 매주 월 15:30, 바뀐 아이콘 감지
→ 바뀐 것만 PR 자동 생성
→ 개발자가 확인 후 머지
→ 패키지 배포 → 서비스에 반영Figma에 수정이나 추가가 있으면 PR이 자동으로 올라와요. 개발자가 확인하고 머지하면, 그 순간 패키지가 배포되어 서비스에 반영돼요.
개발자가 하는 일이 "찾고 → 옮기고 → 올리고"에서 "리뷰하고 머지하기"로 줄었고, 아사나 태스크와 컨펌 왕복은 사라졌어요.
남은 건 하나였어요. 이걸 어떻게 안전하게 자동화하느냐.
설계의 두 원칙
만들기 전에 두 가지 원칙을 먼저 정했어요. 이 둘이 파이프라인의 방향을 거의 다 결정했어요.
publish 버튼에 기대지 않기
Figma에는 컴포넌트를 라이브러리로 publish하는 기능이 있고, 보통 이걸 변경 신호로 삼아요. 그런데 저는 쓰지 않았어요. 디자이너가 publish를 깜빡하면 파이프라인이 안 도니까요.
사람 손을 한 번 더 거쳐야 하는 자동화는 그 손을 놓치면 멈춰요. 그래서 publish 여부와 상관없이 Figma API로 파일 트리를 직접 훑어 ic_* 규칙에 맞는 컴포넌트만 모으기로 했어요. 디자이너는 평소처럼 그리기만 하면 돼요.
// figma.ts — 노드 트리를 내려가며 ic_ 아이콘만 수집
function walk(node: FigmaNode, category: Category | null) {
const nextCategory = resolveCategory(node.name) ?? category
const isIcon =
(node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') &&
ICON_NAME_PATTERN.test(node.name) // /(^|\/)ic[_-]/i
if (isIcon) {
icons.push({ id: node.id, name: node.name, category: nextCategory })
return // 아이콘 내부는 더 안 내려감
}
node.children?.forEach((child) => walk(child, nextCategory))
}진짜 바뀐 것만 골라내기
매주 273개를 전부 다시 만들면 diff가 273개짜리 PR이 되어 리뷰가 불가능해요. 그래서 지난번 상태를 .figma-manifest.json에 저장해두고, 이번 상태와 비교해 바뀐 것만 뽑기로 했어요.
아이콘 하나는 이렇게 기록해요.
// .figma-manifest.json — key는 Figma node id
"23218:22300": {
"name": "Monochrome/ic_alarm",
"category": "Monochrome",
"componentName": "IconAlarm",
"fingerprint": "a3f1c9d2e8b0..." // SVG 내용의 sha256 앞 16자리
}핵심은 fingerprint예요. SVG 내용을 해시로 찍어두면 모양이 실제로 바뀌었을 때만 값이 달라져요. 이름만 같고 내용이 그대로면 "바뀐 것 없음"으로 걸러지고요.
key를 node id로 잡은 것도 이유가 있어요. 이름이 바뀌어도 같은 노드면 "이동"으로 추적할 수 있거든요. (이 부분이 뒤에서 쓰여요.)
이전 스냅샷 ──┐
├──▶ 비교 ──▶ 추가 / 수정 / 삭제 / 이동
현재 Figma ──┘자동화가 만드는 게 '사람이 리뷰할 PR'이라, diff를 작게 유지하는 것 자체가 요구사항이었어요.
여기까지가 하루 만에 만든 첫 버전이고, 잘 돌았어요. 그런데 '한 번 잘 도는 것'과 '매주 사람 없이 도는 것'은 다른 문제였어요. 이 지점부터 코드의 절반이 정상 동작이 아니라 실패 처리에 쓰이기 시작했어요.
일시적 실패를 삭제로 오해하는 문제
사람 없이 도는 자동화에서 제일 위험한 건 에러로 멈추는 게 아니라, 잘못된 동작을 조용히 수행하는 거예요.
첫 버전에서 이런 일이 있었어요. Figma API는 아이콘 목록을 잘 내려주는데, 그중 몇 개의 SVG를 내려받는 과정이 잠깐 실패했어요. 그럼 제 코드는 이렇게 해석해버려요.
이번 목록에 그 아이콘이 없네?
→ 삭제됐구나
→ 파일 지우고, 배럴에서 빼자디자이너는 그 아이콘을 지운 적이 없어요. 네트워크가 잠깐 흔들렸을 뿐인데 멀쩡한 아이콘이 '삭제' PR로 올라오는 거죠. 사람 손을 안 거치고 머지됐다면 사고로 이어졌을 거예요.
그래서 "이번엔 못 받았다"와 "삭제됐다"를 같은 상황으로 취급하지 않게 고쳤어요. 실패한 게 원래 있던 아이콘이면 지난번 값을 그대로 유지해요.
// sync.ts — 오삭제 방지
const svg = svgById[icon.id]
if (!svg) {
const before = prev[icon.id]
if (!BASELINE && before) {
next[icon.id] = before // ← 이전 상태 유지, 삭제로 오해하지 않음
console.warn(`⚠️ SVG export 실패 — 이전 상태 유지(삭제 방지): ${icon.name}`)
} else {
// 새로 생긴 건데 실패한 거면 이번만 건너뜀 (다음 주에 다시 시도)
}
continue
}지금 안 보인다고 삭제된 건 아니니까요. 되돌리기 힘든 판단일수록, 확실하지 않으면 놔두는 쪽이 안전해요.
SVG 다운로드 재시도 처리
오삭제를 막고 나니 진짜 원인이 보였어요. 애초에 왜 내려받기가 실패했을까요? Figma에서 SVG 하나를 받아오는 건 사실 네트워크를 두 번 타는 일이었어요.
1) api.figma.com/images/... 호출
→ SVG를 받을 수 있는 "다운로드 주소"를 받음
2) 그 주소(사실은 S3 CDN)에서
→ SVG 내용을 실제로 내려받음figmaFetch에 재시도와 백오프를 걸어놨는데, 그게 1번(api.figma.com)에만 걸려 있었어요. 정작 SVG를 받아오는 2번(CDN 다운로드)에는 재시도가 없었고요. 그러니 CDN이 잠깐 흔들리면 그대로 실패했고, 앞의 오삭제로 이어졌어요.
그래서 CDN 다운로드에도 재시도를 붙였어요. 이때 기준은 '다시 해볼 만한 것만 다시 한다'였어요.
// figma.ts — CDN(S3) 다운로드 재시도
const retryable = res.status === 429 || res.status >= 500 // 잠깐일 수 있는 것만
if (!retryable) return null // 4xx는 다시 해도 똑같음 → 바로 포기401(토큰 만료)이나 404(노드 없음)는 다시 해도 결과가 같아요. 잠깐의 문제가 아니라 설정·인증 문제니까요. 반대로 429(요청 제한)나 5xx는 조금 뒤에 다시 하면 성공할 수 있어요.
상태 코드마다 원인 힌트도 붙였어요. 로그만 봐도 뭐가 문제인지 알 수 있게요.
if (status === 401 || status === 403)
return '토큰 만료/스코프 부족 → FIGMA_ACCESS_TOKEN 확인 (file_content:read 필요)'
if (status === 404)
return '파일/노드 없음 → FIGMA_FILE_KEY / FIGMA_ICON_NODE_ID 확인'재시도는 '실패한 곳'이 아니라 '실패할 수 있는 모든 지점'에 걸어야 했어요. 하나의 작업처럼 보여도 네트워크를 여러 번 타면, 재시도가 빠진 구간이 사고 지점이 되니까요.
아이콘 이동 시 중복 파일 정리
디자이너는 아이콘을 추가·수정만 하는 게 아니라, 폴더를 옮기거나(Monochrome → Multicolor) 이름을 바꾸기도 해요. 여기서 문제가 생겼어요. 제 파이프라인은 node id 기준이라 같은 노드의 새 위치에 파일은 잘 만드는데, 원래 자리의 파일을 안 지웠어요.
src/Monochrome/IconStar.tsx ← 옛날 파일 (안 지워짐)
src/Multicolor/IconStar.tsx ← 새 파일
// 두 배럴이 같은 이름을 내보냄
export * → IconStar 중복 → 💥 빌드 실패그래서 이 경우를 "이동(relocated)"이라는 별도 항목으로 잡았어요. 같은 node id인데 category나 componentName이 바뀌면 이동으로 보고, 새 자리에 파일을 만든 뒤 옛 자리를 치워요.
// manifest.ts — 이동 감지
const moved =
before.category !== entry.category ||
before.componentName !== entry.componentName
if (moved) relocated.push({ from: {...before}, to: {...entry} })// sync.ts — 이동 시 옛 경로(from)만 삭제 (to는 이미 만들어짐)
const removals = [...diff.deleted, ...diff.relocated.map((r) => r.from)]파일을 만드는 것까지가 자동화의 절반이에요. 나머지 절반은 안 쓰는 파일을 치우는 일이고요. 만들기만 하고 안 치우면 남은 파일이 쌓이다 결국 빌드를 깨뜨려요.
컴포넌트 이름 충돌 처리
Figma의 아이콘 이름을 컴포넌트 이름으로 바꾸는 규칙은 이래요.
'Monochrome/ic_air_conditioner_color' → 'IconAirConditionerColor'그런데 서로 다른 두 노드가 같은 이름으로 변환되는 경우가 있어요. 그럼 한 파일이 다른 파일을 덮어써서 아이콘 하나가 사라져요. 사람이 지켜보지 않으니 눈치채기도 어렵고요.
이건 자동으로 처리하면 위험해요. 둘 중 뭘 남길지는 기계가 판단할 수 없으니까요. 그래서 머지 전에 사람이 볼 수 있게 경고를 남겼어요.
// sync.ts — 컴포넌트명 충돌 경고
if (dupId) {
console.warn(`⚠️ 컴포넌트명 충돌: ${e.componentName} — node ${dupId} 와 ${id} 가 같은 파일로 생성됩니다(덮어쓰기)`)
}이름 규칙이 안 맞는 아이콘도 있어요. Figma에서 단어를 붙여 써 ic_currentlocation처럼 넘어오는 경우예요. 이건 쓰고 있는 앱이 깨지지 않도록 예외를 둬서 원래 이름을 유지했어요.
제대로 된 해결은 디자이너가 이름을 ic_current_location으로 고치는 거지만, 그 전까지는 쓰던 앱이 안 깨지는 게 먼저니까요.
export const NAME_OVERRIDES = {
ic_currentlocation: 'IconCurrentLocation',
ic_shoppingbasket: 'IconShoppingBasket',
}기계가 판단할 수 없는 상황은 억지로 자동 처리하지 않고 사람에게 넘겼어요. 자동화의 역할은 '다 처리하는 것'이 아니라 '사람이 봐야 할 지점을 정확히 넘기는 것'이라고 봐요.
누락된 파일 자동 복구
이번엔 반대쪽 문제가 보였어요. manifest에는 아이콘이 있다고 적혀 있는데, 실제 .tsx 파일은 없는 경우예요. 누가 실수로 지웠거나 머지가 꼬인 거죠. 이 상황은 "바뀐 것 없음"으로 판단되면 영영 안 고쳐져요. manifest 기준으론 바뀐 게 없으니까요.
그래서 복구 장치를 넣었어요. manifest와 Figma에는 있는데 실제 파일만 없으면, 바뀐 게 없어도 다시 만들어요.
// sync.ts — 파일이 사라졌으면 diff가 없어도 복구
const missingIds = Object.keys(next).filter((id) => !existsSync(fileOf(next[id])))덕분에 이 파이프라인은 매주 돌기만 해도 저장소를 늘 Figma 원본과 같은 상태로 맞춰줘요. 누가 파일을 지워도 다음 월요일이면 복구되고요.
잘 만든 동기화는 바뀐 걸 반영만 하는 게 아니라, 어긋난 상태를 원래대로 되돌리는 것까지 해요. 이 차이가 하루짜리 스크립트와 믿고 맡길 수 있는 도구를 가른다고 느꼈어요.
자동 실행을 위한 안전장치
문제들을 잡은 뒤, 사람 손을 떼기 전에 안전장치를 몇 개 더 얹었어요.
| 장치 | 왜 |
|---|---|
| 머지는 항상 사람이 | 자동은 PR 만드는 데까지만. 삭제처럼 되돌리기 힘든 변경은 사람이 쓰임새 확인 후 머지 |
| 빌드 통과 확인 | 만들어진 아이콘 전체를 tsc로 컴파일 → 깨진 코드는 PR조차 안 만듦 |
| baseline 모드 | 처음 도입할 때 273개가 전부 "새 아이콘"으로 뜨는 걸 방지. 현재 상태만 먼저 저장 |
| Slack 실패 알림 | 어느 단계든 실패하면 Slack으로 알림 |
| 토큰 만료 관리 | 개인 토큰(3개월)이 만료되면 401로 실패 → Slack이 잡아줌. 주석에 만료일 명시 |
| 동시 실행 방지 | concurrency 그룹 + 같은 날 다시 돌 때 브랜치 재사용(-B + force-with-lease) |
특히 신경 쓴 건 머지만큼은 자동으로 하지 않은 거예요. 아이콘 삭제는 그 아이콘을 쓰는 앱을 깨뜨릴 수 있으니까요. 그래서 PR 본문에 경고를 넣고, 사람이 확인한 뒤 머지하게 했어요.
이 PR은 웹팀의 /pr 커맨드 한글 템플릿을 그대로 따라 만들어져요. 자동으로 올라온 PR인데도 사람이 쓴 것처럼 읽혀서, 리뷰 부담도 덜해요.
도입 전과 후
| 이전 | 지금 | |
|---|---|---|
| 반영 요청 | 아이콘마다 아사나 태스크 생성 | Figma 변경 시 자동 감지 |
| 플랫폼별 작업 | 앱·웹 개발자가 각자 수작업 | 플랫폼별 자동 (웹은 PR 자동 생성) |
| 변경 감지 | 사람이 눈으로 비교 | fingerprint 비교로 자동 |
| 컨펌·배포 | 디자이너 컨펌 → 수동 배포 | 머지하면 패키지 배포까지 자동 |
| 개발자가 하는 일 | 태스크 확인 + 변환 + PR | PR 리뷰·머지만 |
가장 크게 달라진 건, 아이콘 업데이트가 여러 사람의 할 일에서 사라졌다는 거예요. 디자이너는 태스크를 만들 필요가 없고, 저는 월요일에 올라온 PR을 리뷰하고 머지만 하면 돼요. 실수로 export를 빼먹거나 currentColor를 놓치는 일도 없어졌고요.
안전장치 덕분에 웬만한 일에는 흔들리지 않아요. 네트워크가 끊기든, 디자이너가 폴더를 옮기든, 누가 파일을 지우든 알아서 버티거나 다음 주에 복구하니까요.
회고
처음엔 이 문제가 단순해 보였어요. "아이콘 옮기는 데 사람이 너무 많이 붙는다", 그거였거든요. 그런데 만들어보니 진짜 어려움은 다른 데 있었어요. 만드는 게 아니라, 사람 없이도 안전하게 돌게 만드는 것이요.
한 번 잘 도는 스크립트는 하루면 만들어요. 그런데 매주 아무도 안 보는 채로 돌리려면 이야기가 달라져요. "이게 언제, 어떻게 잘못될 수 있지?"를 계속 물어야 했어요.
오삭제, 네트워크를 두 번 타는 문제, 안 치운 파일, 이름 충돌. 전부 첫 버전에선 안 보이다가, "사람 없이 돌리면?"을 떠올리니 하나씩 나왔어요.
그래서 이 파이프라인 코드는 절반이 정상 동작이 아니라 실패 처리예요. 저는 그게 맞다고 생각해요. 사람이 지켜보는 도구는 실패해도 사람이 고치면 되지만, 혼자 도는 도구는 실패까지 스스로 버티거나 정확히 알려줘야 하니까요.
지금 디자이너는 예전처럼 Figma에 아이콘을 그리고, 저는 월요일에 올라온 PR을 리뷰만 해요. 사람 여럿이 오가며 반복되던 과정을, 이제 스스로 문제까지 챙기는 파이프라인이 대신하고 있어요.
참고
- Figma REST API (Images endpoint). 노드를 SVG로 내보내는 API. 반환 map에
null이 섞일 수 있다는 점이 여기 명시돼 있어요. - SVGR Options (
replaceAttrValues). SVG를 React 컴포넌트로 바꾸고, 색을currentColor로 치환할 때 쓴 옵션이에요. - GitHub Actions Workflow syntax (
on.schedule). cron으로 매주 월요일 실행을 걸었어요. - GitHub Actions Control concurrency. 같은 워크플로우가 겹쳐 도는 걸 막는 설정이에요.