5분 → 3초로 단축한 React 아이콘 자동화

황준·2025년 10월 4일
1

좋아하는 글

목록 보기
1/8
post-thumbnail

✅ 개요

최근에 디자인 팀에 큰 변화가 있었습니다.
6년간 근무하신 리더 분이 퇴사하고, 새로운 시니어 디자이너분이 합류하면서 디자인 시스템에 많은 변화가 생겼습니다.

특히 아이콘 사용 방식이 바뀌었습니다.
기존에는 24x24로 고정 크기에서 size를 변경하거나 fill을 수정하는 정도였습니다.

현재는 16x24처럼 정사각형이 아닌 비율로 바뀌기도 하고,

특정 부분의 색을 바꿔야하는 문제도 있었습니다.

이러한 상황에서 팀원분이 SVG 수정하는 방법을 제안하여 문제를 해결했습니다.
다만 트레이드 오프로 아이콘을 추가할 때마다 해야할 작업이 많아졌습니다.

아이콘 5개를 추가하는데 5분 20초가 걸릴 정도로 비효율적인 작업이었습니다.
이로 인해 팀원들 모두 불편함을 겪고 있었습니다.

팀 생산성을 높이기 위해, 아이콘 추가 프로세스를 자동화하기로 마음을 먹었습니다.


👊 기존 아이콘 추가 방법

  1. 디자인 시스템에서 아이콘을 다운로드합니다.
  2. 파일 이름(ex: ic-new-icon.svg)을 웹 컨벤션에 맞추어 변환합니다.
  3. path의 fill 속성을 제거하고 색상 변환이 정상 동작하는지 확인합니다.
  4. Icon 컴포넌트에 import하고, 파스칼 케이스로 변환하여 아이콘 맵에 추가합니다.
import { ReactComponent as NewIcon } from '../../assets/icon/ic-new-icon.svg';

const iconMap = {
 newIcon
}

🤔 추가된 프로세스

새로운 디자인 시스템에서는 3단계가 추가되었습니다.

변경 전

<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill="#7E7E94" style="fill:#7E7E94;fill:color(display-p3 0.4941 0.4941 0.5804);fill-opacity:1;"/>
</svg>

추가 작업
1단계: width, height 속성 제거

  • 이유: 고정 크기(24x24)에서 벗어나 자유롭게 크기를 조절할 수 있도록 하기 위함

2단계: style 속성 제거

  • 이유: 불필요한 인라인 스타일 정리

3단계: fill, stroke를 currentColor로 변경

  • 이유: SVG의 currentColor 속성을 활용하여 부모 요소의 텍스트 색상을 상속받아 동적으로 색상 변경 가능
  • ⚠️ 주의: 일부 아이콘은 고정 색상을 유지해야 하므로 변경 전 확인 필요

변경 후

<svg viewBox="0 0 24 24" fill="none">
<path fill="currentColor"/>
</svg>

최종 프로세스

아이콘을 추가하려면 이제 다음 5단계를 거쳐야 했습니다:

  1. 파일 이름을 ic-xxx.svg 형식으로 변환
  2. SVG의 width, height 속성 제거
  3. SVG의 style 속성 제거
  4. fill, stroke 속성을 currentColor로 변경
    • ⚠️ 주의: 고정 색상이 필요한 아이콘은 변경하면 안 됨
  5. Icon 컴포넌트에 import하고 파스칼 케이스로 변경 후, 아이콘 맵에 추가

🎯 자동화 목표와 핵심 과제

위 5단계를 자동화하려다 보니 4단계에서 문제가 발생했습니다.

4. fill, stroke 속성을 currentColor로 변경
   ⚠️ 주의: currentColor가 적용되면 안 되는 아이콘이 있음

모든 아이콘에 일괄 적용할 수 없어서, 각 아이콘마다 수동으로 확인이 필요했습니다.
즉, SVG 정규화 규칙이 명확하지 않았던 것입니다.

💡 정규화(Normalization): 일정한 규칙에 맞게 데이터를 정돈하는 작업

고민: 어떤 순서로 진행할까?

  1. SVG 정규화 조건을 먼저 찾고 → 자동화 작업 시작
  2. 자동화 작업을 먼저 하고 → SVG 정규화 조건 찾기

자동화를 먼저하고, 정규화 조건을 찾기로 하였습니다.

이유:

  • SVG 정규화는 팀 내에서도 명확한 규칙을 찾지 못한 상태여서 어려울 수 있음
  • 정규화 없이도 자동화만으로 충분히 번거로운 작업을 줄일 수 있음

이렇게 SVG 정규화를 최종 보스라 생각하고 자동화부터 시작했습니다.

🔧 작업 순서

1단계: 파일명 변경

지정한 경로에서 파일을 읽어와 정규화 규칙에 따라 파일명을 표준 형식으로 변환합니다.

변환 규칙:

  • name=New, Icon=on.svgic-new-icon.svg

처리 로직:

  • 규칙에 맞는 파일: 표준 형식으로 변환
  • 이미 변환된 파일: 그대로 유지 (예: ic-old-icon.svg)
  • 규칙에 맞지 않는 파일: 작업 제외 대상으로 분리 (예: newIcon, icon1123)
  • 중복된 파일명: 이미 존재하는 경우 작업 제외

작업 제외 대상은 로그로 출력하여 확인할 수 있도록 했습니다.

2단계: 등록된 아이콘 목록 확인

Icon 컴포넌트 파일을 읽어 이미 등록된 아이콘 목록을 가져옵니다.

import { ReactComponent as ZoomOut } from '../../assets/icon/ic-zoom-out.svg';

const iconMap = {
  ZoomOut
}

예를 들어, ZoomOut은 이미 등록되어 있으므로 중복 등록을 방지합니다.

3단계: 처리 대상 파일 식별

1단계에서 변환한 파일명(ic-new-icon.svg)을 파스칼 케이스(NewIcon)로 변환하여, Icon 컴포넌트에 이미 등록되었는지 확인합니다.

등록되지 않은 신규 아이콘만 필터링하여 다음 단계로 전달합니다.

4단계: SVG 정규화

SVG 파일 내용을 정리합니다:

  • 불필요한 width, height 속성 제거
  • style 속성 제거
  • fill, strokecurrentColor로 변경 (단, 검토 필요)

Before:

<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
  <path fill="#7E7E94" style="fill:#7E7E94;fill:color(display-p3 0.4941 0.4941 0.5804);fill-opacity:1;"/>
</svg>

After:

<svg viewBox="0 0 24 24" fill="none">
  <path fill="currentColor"/>
</svg>

5단계: 신규 아이콘 등록

Icon.tsx에 새로운 아이콘을 추가합니다.
파일명(ic-new-icon.svg)을 파스칼 케이스(NewIcon)로 변환하여 등록합니다.

  • import 문을 최하단에 추가
  • iconMap 객체에 최하단에 추가
import { ReactComponent as ZoomOut } from '../../assets/icon/ic-zoom-out.svg';
import { ReactComponent as NewIcon } from '../../assets/icon/ic-new-icon.svg'; // 추가

const iconMap = {
  ZoomOut,
  NewIcon // 추가
}

구현 방식:
Node.js의 fs, path 를 활용하여 스크립트를 작성하고, 테스트 코드를 통해 검증하며 개발했습니다.

6단계: 타입 체크 및 코드 포맷팅

디자인 시스템 업데이트 후에는 타입 빌드와 코드 포맷팅이 필요합니다.
execSync를 사용하여 자동으로 실행되도록 구현했습니다.

💡 execSync: Node.js에서 셸 명령어를 동기적으로 실행하는 함수

이를 통해 타입 생성과 import sorting까지 완료됩니다.

✅ 결과

스크립트를 실행하면 파이프라인이 작동하고, 성공/실패 목록이 출력됩니다.

🪝 pre-commit 훅 추가

스크립트 실행도 자동화하고 싶었습니다.
Lefthook의 glob 패턴을 활용하여 아이콘 파일 변경 시 스크립트가 자동 실행되도록 설정했습니다.

효과:

  • 아이콘별로 커밋하도록 자연스럽게 유도
  • 수동 스크립트 실행 불필요

🎮 최종 보스: SVG 정규화

기본 자동화 파이프라인은 완성했습니다.
이제 남은 과제는 SVG 정규화 규칙을 찾아내는 것입니다.

🎨 SVG 정규화 도전

현재까지 구현된 정규화 조건:

  • ✅ 불필요한 width, height 속성 제거
  • style 속성 제거
  • fill, strokecurrentColor로 변경

🚨 문제 발생: 과도한 변환

fill, stroke를 일괄적으로 currentColor로 변경하자 문제가 발생했습니다.

변경되어야 하는 경우:

  • 체크 아이콘, 화살표 아이콘 등 → 텍스트 색상에 따라 변경 필요

변경되면 안 되는 경우:

  • 브랜드 로고, 상태 표시 아이콘 등 → 특정 색상 유지 필요
  • 예: 경고 아이콘의 노란색, 에러 아이콘의 빨간색

🔍 문제 패턴 분석

일단 모든 파일을 정규화한 뒤, 문제가 발생한 SVG들을 모아 패턴을 분석했습니다.

변경하면 안 되는 케이스:

  • 리소스 참조 색상
    (예: fill="url(#grad1)", stroke="url(#p1)")
  • 그라디언트/패턴/이미지 요소 포함
    (예: <linearGradient>, <radialGradient>, <pattern>, <image>)
  • 브랜드/포인트 색상 등 의도된 고정색
    (예: #1976d2, #ff4757, blue, tomato)
  • 파일명에 Color 메타데이터 포함
    (예: Name=Check-Circle, Color=🩶, Tinted=Off.svg)

위 조건을 적용하니 문제가 되던 SVG는 해결되었습니다.

하지만 새로운 문제 발생:
이번에는 변경되어야 할 색상이 변경되지 않는 케이스가 생겼습니다.
너무 보수적으로 접근한 것입니다.

💡 패턴 발견: Gray 색상의 비밀

변경되지 않은 SVG들을 모아 확인해보니 공통점을 발견했습니다.
모두 #7e7e94 (gray8) 색상을 사용하고 있었습니다.

Figma에서 아이콘들을 확인해보니:

  • 대부분의 아이콘이 gray8 사용
  • 고정색이 필요한 일부 아이콘만 특정 색상 사용

새로운 가설:
디자이너들이 #7e7e94를 가변색의 기본값으로 사용하는 것은 아닐까?

주말이라 확인할 수는 없었지만, 이 가설로 변경을 시도했습니다.

✨ 정규화 규칙 단순화

개선된 변환 조건:

Before:

// 모든 fill 색상을 currentColor로 변경
if (fillAttribute && fillAttribute !== 'none') {
  // 문제: 고정색까지 모두 변경됨
}

After:

// 특정 색상(gray8)만 currentColor로 변경
const COLORS_TO_CONVERT = ['#7e7e94'];

if (COLORS_TO_CONVERT.includes(fillAttribute)) {
  // currentColor로 변경
}

설계 의도:

  • 가설이 변경될 수 있어 확장 가능하도록 배열로 관리
  • 새로운 가변색 후보가 추가되어도 배열에만 추가하면 됨

최종 변환 규칙:

  • fill/stroke#7e7e94 (gray8)인 경우 → currentColor로 변경
  • ❌ 그 외 모든 색상 → 원본 유지

제외되는 케이스:

  • 리소스 참조 색상 (url(#...))
  • 그라디언트/패턴/이미지 요소
  • 브랜드 고정색 (#1976d2, blue 등)
  • Color 메타데이터 포함 파일

🎯 최종 단순화

위 조건을 적용하니 모든 SVG가 올바르게 정규화되었습니다.

다시 제외 조건에 해당하는 SVG들을 확인해보았습니다.
"gray인 경우만 변경"이라는 단일 규칙으로도 충분하다 생각했습니다.

복잡한 조건들을 제거하고 적용해본 결과, 예상대로 완벽하게 작동했습니다.

최종 규칙:

fill/stroke가 #7e7e94 (gray8)인 경우 → currentColor로 변경

✅ 디자이너 검증

주말이 끝나고 디자이너와 확인한 결과:

  • gray를 의도적으로 가변색으로 사용한 것은 아니었음
  • 하지만 이 규칙을 적용해도 문제없을 것 같다는 의견
  • 디자인 시스템 컨벤션으로 채택하기로 결정

이후 전체 SVG를 여러 차례 변환하며 규칙을 검증했고, 안정적으로 작동함을 확인했습니다.

🐛 파일명 변환 예외 케이스 처리

검증 과정에서 기존 규칙으로 처리할 수 없는 케이스들을 발견했습니다.

1. 동의어 충돌 문제

문제 상황:

Name=Message-Filled, Fill=On, Noti=Off, Emphasis=Off.svg
→ ic-message-filled-fill (의도하지 않은 중복)

파일명에 Filled(형용사)가 있는데, 속성으로 Fill(명사)이 추가되면서 filled-fill처럼 어색한 이름이 생성되었습니다.

해결 방법:
동의어를 매핑하여 중복을 제거하도록 처리했습니다.

const ATTRIBUTE_SYNONYMS = {
  fill: ['filled'],
};

현재는 filledfill만 존재하지만, 향후 유사한 케이스가 추가될 가능성을 고려해 객체 구조로 설계했습니다.

2. 이모지 색상 메타데이터

문제 상황:

Name=Check-Circle, Color=🩶, Tinted=Off.svg

디자이너가 색상을 이모지(하트)로 표현하여 파일명 변환이 불가능했습니다.

해결 방법:
이모지를 색상 문자열로 매핑하는 변환 테이블을 작성했습니다.

const EMOJI_COLOR_MAP = {
  '❤️': 'red',      '💛': 'yellow',  '💙': 'blue',
  '🧡': 'orange',   '💚': 'green',   '💜': 'purple',
  '🖤': 'black',    '🤍': 'white',   '🩷': 'pink',
  '🤎': 'brown',    '🩶': 'gray',    '🩵': 'skyblue'
};

결과:

Name=Check-Circle, Color=🩶, Tinted=Off.svg
→ ic-check-circle-gray.svg

📝 문서화 및 테스트 코드

이러한 예외 처리 로직과 변환 규칙들을 문서와 테스트 코드로 정리했습니다.

테스트 코드의 장점:
문서는 최신 상태인지 검증이 필요하지만, 테스트 코드는 실행만으로 즉시 검증할 수 있습니다.
제가 없을 때도 다른 팀원이 안전하게 유지보수할 수 있도록 추가하였습니다.

작성한 테스트:

SVG 정규화 검증 테스트

파일명 변환 테스트

이외에도 총 28개의 테스트를 작성하였습니다.

🎉 배포 및 공유

웹 회의에서 제가 해결하고자 하는 문제와 사용 방법을 알려드렸습니다.
파일명 규칙관련 문서와 테스트 코드를 공유하며 마무리했습니다.

📊 개선 결과

구분소요 시간작업 방식
Before5분 20초수동 작업 (5단계)
After3초자동화 스크립트
개선율106배 단축-

이를 통해 단순 반복 작업을 줄이고, 더 중요한 개발
업무에 집중할 수 있게 되었습니다.

마무리

공유했을 때 팀원들 반응이 좋았습니다.
번거로웠던 작업을 줄이고, 개선할 수 있어서 기뻤습니다.

SVG 정규화 규칙을 찾고 검증하는 과정에 많은 시간을 썼습니다.
386개의 아이콘을 확인하며 어떤 색상은 변경하고 어떤 색상은 유지해야 하는지 패턴을 찾는 일이 쉽지 않았습니다.

작업하면서 Node.js에 대한 이해가 깊었으면 좋겠다라는 생각이 들었습니다.
파일 처리할 때, 지금 방식이 최선인지 확신하기 어려웠기 때문입니다.

그래서 내년 상반기에 Node.js를 공부하려고 합니다.
단순히 작동하는 코드를 넘어 더 깊은 이해 기반으로 작성하고 싶습니다.

좀 더 공부하면 보이지 않던 자동화 포인트도 더 잘찾을 수 있지 않을까합니다

긴 글 읽어주셔서 감사합니다!

profile
잘하고 싶은 사람

0개의 댓글