태그 디자인 토큰디자인시스템리팩터링코드모드React Native
전체 글 보기

디자인 토큰

디자인 토큰은 이미 있었고, 코드가 그걸 안 쓰고 있었습니다

앱 소스에 하드코딩된 색 953개를 셌더니 그중 349개는 디자인 토큰에 똑같은 값이 이미 있었습니다. 자동으로 바꾸다 두 번 틀렸고, 둘 다 컴파일도 린트도 통과하는 종류였습니다.

2026년 8월 18일 4분

앱에 컴포넌트로 묶을 게 있나 보려고 스타일 파일을 세어봤는데, 문제가 제가 찾던 것과 달랐습니다. 컴포넌트가 없는 게 아니라 디자인 토큰이 이미 다 있는데 코드가 그걸 안 쓰고 있었습니다.

하드코딩된 색 953개 중 349개는 디자인 토큰에 이미 있었습니다

앱 소스에서 color · backgroundColor · borderColor 에 문자열 리터럴이 들어간 자리를 전부 셌습니다. 디자인 토큰을 정의하는 파일과 3D 재질은 뺐습니다.

하드코딩 색상            953개
스타일시트               165개

그다음이 진짜입니다. 토큰 파일이 정의한 값들을 표로 만들고, 953개 각각이 그 표에 이미 있는 값인지 대조했습니다. 표기가 달라도 같은 색이면 같게 보도록 #fffrgba(255,255,255,1) 를 같은 형태로 바꿔놓고 비교했습니다.

그중 토큰에 똑같은 값이 이미 있던 것   349개 (36%)

세 개 중 하나입니다. whiteAlpha55 라는 이름이 이미 있는데 같은 값을 손으로 다시 적은 자리가 349군데였습니다.

이건 토큰이 부족해서 생긴 일이 아닙니다. 쓰는 것보다 적는 게 빨라서 생긴 일이고, 그래서 아무도 못 막습니다.

같은 이름의 스타일 23개가 20가지로 갈려 있었습니다

숫자보다 더 나쁜 게 따로 나왔습니다. sectionLabel 이라는 이름의 스타일이 23개 파일에 있었는데, 값이 서로 달랐습니다.

정의한 파일23개
서로 다른 값20가지
가장 많이 겹친 값2곳

색이 #888 인 화면과 rgba(255,255,255,0.7) 인 화면과 .55 인 화면이 있었고, 크기가 11 인 곳과 9 인 곳이 있었고, 대문자로 바꾸는 화면과 안 바꾸는 화면이 섞여 있었습니다.

같은 개념인데 사용자가 보는 결과가 화면마다 다릅니다. 그리고 이건 어떤 검사로도 안 잡힙니다. 스무 가지 값이 전부 문법적으로 멀쩡한 코드니까요.

값이 같다고 역할이 같은 게 아니었습니다

349개를 자동으로 바꾸는 스크립트를 짰습니다. 값이 완전히 같은 경우에만 토큰 참조로 치환하는, 안전해 보이는 작업입니다.

돌리고 나서 결과를 훑는데 이런 게 있었습니다.

- backgroundColor: '#fff',
+ backgroundColor: colors.brand.onPrimary,

값은 맞습니다. brand.onPrimary 는 흰색이니까요.

그런데 그 토큰의 뜻은 “보라색 면 위에 얹는 글자 색” 입니다. 전경에 쓰라고 만든 이름을 배경 자리에 넣은 겁니다.

지금은 아무 일도 안 일어납니다. 문제는 나중에 그 토큰을 바꿀 때입니다. 버튼 글자색을 조정하려고 onPrimary 를 손대는 순간, 아무 관계 없는 흰 배경들이 같이 따라 바뀝니다.

되돌리고 규칙을 넣었습니다. 전경 역할로 이름 붙은 토큰은 배경 속성에 못 들어가게 막고, 글자색 토큰도 배경에서 제외했습니다. 그러고 나니 안전하게 바꿀 수 있는 건 182개로 줄었습니다.

167개가 줄어든 게 손해처럼 보이지만, 그 167개가 정확히 사람이 판단해야 할 자리입니다.

값이 안 바뀌었다는 걸 어떻게 증명했나

파일 51개를 기계가 고쳤으니 겉모습이 안 바뀌었다는 걸 확인해야 했습니다. 눈으로 51개를 볼 수는 없어서 이렇게 했습니다.

각 파일에서 색이 쓰인 자리를 순서대로 뽑고, 토큰 참조는 실제 값으로 풀어서, 바꾸기 전과 후의 수열을 비교했습니다.

대조한 색 자리 807개 / 파일 51개
값이 달라진 파일 0개

이 방법의 좋은 점은 diff 를 읽지 않아도 된다는 겁니다. diff 는 “무엇을 고쳤나” 를 보여주지만, 여기서 알고 싶은 건 “결과가 같은가” 였습니다.

흩어진 걸 모았더니 열 개 화면이 여백을 잃었습니다

23개의 sectionLabel 을 컴포넌트 하나로 모았습니다. 색과 크기와 자간을 한 곳에서 정하게 하고, 각 화면은 그걸 가져다 쓰게 했습니다.

다음 날 사용자가 화면 사진을 보내왔는데 제목이 위 요소에 딱 붙어 있었습니다.

옮기는 코드가 색과 크기는 챙겼는데 marginTop 을 안 봤습니다. 원래 그 값을 갖고 있던 화면이 10개였고, 값도 제각각(6부터 28까지)이라 어느 하나를 기본값으로 삼을 생각을 못 한 겁니다.

marginTop 을 갖고 있던 화면   10개
새 컴포넌트의 기본값          없음

여기서 중요한 건 이게 왜 안 잡혔느냐입니다.

타입 검사는 통과합니다. 스타일 속성이 하나 없는 건 타입 오류가 아니니까요. 린트도 통과합니다. 문법이 멀쩡하니까요. 화면도 안 터집니다. 그냥 붙어 보일 뿐입니다.

기계가 볼 수 있는 신호가 하나도 없는 종류의 고장이라, 실기기 사진이 오기 전까지 아무도 몰랐습니다.

기계적인 리팩터링은 옮기려고 마음먹은 것만 옮깁니다. 안 옮겨진 건 사라진 게 아니라 처음부터 목록에 없었던 것이고, 목록을 만든 사람은 저입니다.

0을 요구하는 검사는 곧 무시됩니다

남은 하드코딩이 766개입니다. 이걸 다 없애는 검사를 만들고 싶었는데, 만들면 첫날부터 실패하는 검사가 됩니다.

이 저장소에서 비슷한 걸 이미 겪었습니다. 자동으로 뜨던 것을 끄자 기능이 사라졌습니다 에서도 같은 종류였는데, 그때는 검사가 아예 없어서 기능 하나가 조용히 사라졌습니다. 문서에 특정 문자를 못 쓰게 하는 규칙을 만들 때, 대상 범위를 전부 켜면 16,000줄이 한 번에 걸려서 통과가 불가능해집니다. 그리고 통과 못 하는 검사는 사람이 그냥 건너뜁니다.

그래서 지금 숫자를 천장으로 잠갔습니다.

pnpm design:check   # 766보다 늘면 exit 1

늘면 막고, 줄이면 기준선을 같이 내리라고 알려줍니다. 새 화면에서 색을 하드코딩하면 그 자리에서 걸리고, 옛날 것 766개는 급하지 않게 줄여갑니다.

검사를 만들고 나서는 되돌려 넣어봤습니다. 색 하나를 문자열로 되돌리니 8개가 늘었다고 exit 1 이 났습니다. 안 해보면 검사가 도는지 아닌지 모릅니다.

이 글이 유효한 범위

숫자는 2026년 8월 18일에 React Native 앱 하나(스타일시트 165개)에서 잰 값입니다. 36%라는 비율이 다른 저장소에서도 비슷할지는 모릅니다.

기준선을 천장으로 잠그는 방식이 실제로 766을 줄어들게 만드는지도 아직 모릅니다. 오늘 만든 것이라 데이터가 하루치도 없습니다. 줄지 않고 그대로 굳으면 그 방식이 틀린 것이고, 그때 다시 쓰겠습니다.

디자인 시스템이 있는데 코드가 안 쓰고 있는지 확인하는 방법은 간단합니다. 하드코딩된 값을 세고, 그중 몇 개가 이미 토큰에 있는 값인지 세보면 됩니다. 저는 그 비율을 재보기 전까지 이 저장소에 문제가 있다는 걸 몰랐습니다.

이전 글 uniq 가 서로 다른 한글 줄을 중복이라고 말합니다