[STOCAT] vanilla-extract 타이포그래피 토큰, 3줄 -> 1줄로 줄이기

이 글은 STOCAT 프로젝트에서 디자인 시스템 토큰을 정리하면서 발견한 코드 개선 경험을 기록한 글이다.

들어가며

STOCAT 프로젝트에 vanilla-extract로 디자인 토큰 체계를 새로 구축하는 작업을 했다.

색상 토큰(color.role.css.ts)과 타이포그래피 토큰(typography.css.ts)을 정의하고,

기존 컴포넌트들에 순차적으로 적용하는 작업이었다.

그런데 적용하다 보니 반복되는 패턴이 눈에 들어왔다.

// 이게 7개 파일에 걸쳐, 최대 5군데에서 반복되고 있었다
fontSize: vars.typography.body3.fontSize,
fontWeight: vars.typography.body3.fontWeight,
lineHeight: vars.typography.body3.lineHeight,

 

뭔가 잘못된 느낌이 들었다. 토큰을 만든 이유가 반복을 줄이기 위해서인데,

막상 쓰는 쪽에서 3줄씩 꺼내 쓰고 있으니 의미가 반감되는 것 같았다.


문제 파악

타이포그래피 토큰 정의를 다시 들여다봤다.

// src/shared/styles/typography.css.ts
export const typography = {
  h1: { fontSize: "24px", fontWeight: "700", lineHeight: "150%" },
  body2: { fontSize: "16px", fontWeight: "500", lineHeight: "150%" },
  body3: { fontSize: "14px", fontWeight: "700", lineHeight: "150%" },
  // ...
} as const;

 

각 토큰은 fontSize, fontWeight, lineHeight 세 속성을 묶은 객체다.

그리고 vanilla-extract의 createTheme을 통해 CSS 변수로 변환된 이후에도 구조는 그대로 유지된다.

// src/shared/styles/vars.css.ts
export const [themeClass, vars] = createTheme({
  color: { ... },
  typography,  // <- 그대로 주입
});

 

vars.typography.body3은 단일 값이 아니라 { fontSize, fontWeight, lineHeight } 구조의 객체다.

그렇다면 spread 연산자로 한 번에 풀 수 있어야 한다.


시도 : 스프레드 연산자 적용

// 변경 전
export const currentBalance = style({
  fontSize: vars.typography.h1.fontSize,
  fontWeight: vars.typography.h1.fontWeight,
  lineHeight: vars.typography.h1.lineHeight,
  color: vars.color.role.text,
});

// 변경 후
export const currentBalance = style({
  ...vars.typography.h1,
  color: vars.color.role.text,
});

 

style() 함수에 전달하는 객체 안에서 스프레드가 동작한다면,

세 속성이 한꺼번에 전개되어 vanilla-extract가 정상적으로 처리할 수 있을 것이다.

vanilla-extract의 style() 함수는 일반 JS 객체를 인자로 받기 때문에, 스프레드 연산자가 완벽하게 동작한다.

 

빌드를 돌렸다.

pnpm build
✓ 195 modules transformed.
✓ built in 790ms

 

에러 없이 통과했다.


적용 범위

7개 파일, 총 12군데에 적용했다.

파일 대상 스타일 적용 토큰
HomeGreetingSection.css.ts subTitle caption1
HomeBanner.css.ts title body1
HomeWallet.css.ts currentTime caption1
HomeWallet.css.ts currency body5
HomeWallet.css.ts balance body3
HomeMarketSection.css.ts sectionTitle body3
MarketCard.css.ts name body3
HomeStockSection.css.ts currentBalance h1
HomeStockSection.css.ts currentVariation body2
MyStockContent.css.ts corpInfoTitle body3
MyStockContent.css.ts averagePrice body4
MyStockContent.css.ts currentPrice body3
MyStockContent.css.ts increase / decrease body4

 

diff 기준으로 53줄 삭제, 35줄 추가. 코드량이 줄면서 읽기도 훨씬 편해졌다.


왜 처음부터 이렇게 안 했을까?

솔직히 말하면, Vanila-Extract를 잘 몰라서.... 토큰을 처음 적용할 때는 "일단 동작하게" 하는 데 집중했다.

그 과정에서 각 속성을 하나씩 꺼내 쓰는 패턴이 자연스럽게 굳어졌다.

토큰의 구조를 충분히 생각하지 않은 것이 근본 원인이었다.

vars.typography.body3이 객체라는 걸 알면서도, 스프레드로 전개할 수 있다는 발상으로 이어지지 않았다.


배운 것

토큰을 { fontSize, fontWeight, lineHeight } 객체로 묶어서 정의한 건,

관련 속성을 한 단위로 다루기 위해서다. 그렇다면 사용하는 쪽에서도 그 단위를 그대로 활용해야 한다.

스프레드는 그 의도를 가장 자연스럽게 표현하는 방법이었다.


마무리

작은 리팩터링이었지만, 덕분에 "토큰을 왜 객체로 묶어서 정의했는가"를 다시 한번 생각해보게 됐다.

정의와 사용이 같은 논리 단위로 맞아떨어지면, 코드를 읽는 사람이 훨씬 빠르게 의도를 파악할 수 있다.

이 경험을 바탕으로 앞으로 새로운 컴포넌트에 토큰을 적용할 때는 처음부터 스프레드 패턴 구조도 고려해볼 수 있을 것 같다.