vanilla-extract를 프로젝트에 1년간 사용해보며, 처음 가졌던 인상이 완전히 달라졌다.
처음엔 단순히 "타입 지원이 되는 CSS-in-JS 도구" 정도로 여겼다. styled-components, Emotion의 대체제로 쓸 수 있을지, 익숙하게 써오던 Tailwind와는 어떤 차이가 있을지 궁금했다.
가볍게 도입했지만, React Server Component 환경에서 기존 CSS-in-JS의 구조적 한계를 마주하며 관점이 바뀌기 시작했다. 런타임에 스타일을 생성하는 방식은 서버 컴포넌트와 궁합이 맞지 않았고, 클라이언트 전용 처리나 복잡한 우회가 필요했다. 점점 구조적인 피로도가 쌓였다. 그런 상황에서 vanilla-extract의 런타임 없는 정적 스타일링 구조는단순한 기술 선택을 넘어서 현실적인 대안처럼 다가왔다.
이 글은 vanilla-extract의 철학, 내부 구조, 타입 추론 방식, 빌드 흐름, 성능 특성까지 전방위적으로 다룬다.
단순한 사용법 소개가 아니라, 왜 이런 구조를 택했는지, 어떤 상황에서 강점을 가지는지 실제 프로젝트 경험과 기술적 맥락을 바탕으로 풀어보고자 한다.
그 과정에서 자연스럽게 다음과 같은 질문들이 떠올랐다:
기존 CSS-in-JS 방식은 왜 서버 컴포넌트에서 문제를 일으킬까?
vanilla-extract는 타입 안전성을 어떻게 구현하고 있을까?
sprinkles, recipe, createTheme의 내부 구조는 어떻게 동작할까?
DevTools와 번들 결과를 보면 실제로 어떤 CSS가 만들어질까?
Tailwind, Emotion, Stitches와 비교했을 때 본질적인 차이는 무엇일까?
위 질문들은 한 번에 다루기 어렵기에, 이후 시리즈 글에서 하나씩 더 깊게 다뤄볼 예정이다.
스타일링 도구의 진화 흐름과 기존 방식의 한계
프론트엔드에서 스타일링은 단순히 "화면을 꾸미는 일"로 끝나지 않는다. 실제로는 성능, 유지보수, 확장성, 협업 구조까지 전반적인 설계에 큰 영향을 준다.
vanilla-extract는 그냥 또 하나의 스타일링 도구가 아니다. 지금까지의 방식들이 가진 한계를 하나하나 짚으면서, 그걸 구조적으로 풀어보려는 시도에 가깝다.
전통적인 CSS
전역 스코프라서 한 파일에서 만든 클래스가 어디서든 충돌할 수 있었고
클래스 이름도 직접 지어야 해서, 일관성 유지가 쉽지 않았고
설계 없이도 덮어쓰는 게 가능해서 구조가 점점 꼬이기 쉬웠다
/* 너무 유연한 구조 */
.button {
background: red;
}
작은 페이지에선 문제 없지만, 스케일이 커지면 스타일 관리가 점점 복잡해졌다.
CSS Modules
.module.css 단위로 파일 스코프를 만들어 충돌을 막았고
Webpack이 클래스 이름을 자동 해싱해서 전역 오염도 방지했지만
여전히 JS/TS와 별도 DSL처럼 분리돼 있어 타입/자동완성의 단절이 있었다
/* Button.module.css */
.button {
background: red;
}
/* Button.tsx */
import styles from './Button.module.css';
<button className={styles.button} />
- 클래스 이름은 문자열로만 다뤄지고, IDE 자동완성도 기대하기 어려웠다
- CSS 값의 구조나 의미를 컴포넌트 단에서 직접 다루기 어려운 구조였다
작은 팀에선 유용했지만, 디자인 시스템이나 타입 기반 개발로 확장하긴 어려웠다.
CSS-in-JS (styled-components, Emotion 등)
스타일을 JS 안에서 작성하며 컴포넌트와 완전히 통합되었고
props 기반 조건부 스타일, 테마 조작, 동적 구성까지 자유로워졌다
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
`
하지만 유연성의 대가도 컸다.
- 실행 컨텍스트가 많아지며 디버깅과 번들 성능 모두 관리 부담이 생김
- 컴포넌트 수가 많아질수록 CSS 생성량도 비례해 최적화 없이 비용 증가
스타일을 더 유연하고 편하게 짤 수 있게 됐지만,
어떤 스타일이 언제 적용되는지 예측하기가 오히려 더 어려워졌다.
🔗 관련 참고자료:
- The dark side of CSS-in-JS — Max Stoiber
- Why we dropped styled-components — Sentry 팀 블로그
유틸리티 기반 시스템 (TailwindCSS, WindiCSS 등)
- 정해진 클래스 조합만으로 UI를 구성
- 스타일은 모두 정적으로 생성되고, 런타임 비용은 없다
- 사용되지 않는 클래스는 PurgeCSS로 제거 → 결과물 최소화
<!-- 클래스명만 보면 역할이 드러나지 않음 -->
<div class="p-4 text-blue-700">...</div>
- 클래스 네이밍 규칙이 강제되고, config 파일 수준에서 확장해야 함
- 타입 시스템이 없어서 오타나 실수를 컴파일 타임에 잡기 어렵다
빠른 UI 조립에는 최적화되어 있지만, 설계 관점에선 추상화가 어렵고, 디자인 시스템 확장성엔 한계가 있다.
그래서 등장한 vanilla-extract
vanilla-extract는 다음과 같은 질문에서 출발한다:
❓ 스타일은 왜 JS/TS의 유형 시스템 밖에 존재해야만 할까?
❓ 정적 스타일 시스템과 설계 자유도는 양립할 수 없을까?
❓ 디자인 토큰을 코드로 명확하게 표현하고 통제할 수는 없을까?
이 질문에 대한 해답으로 등장한 개념이 다음 세 가지이다:
- 스타일을
.css.ts에서 TypeScript로 선언 - 빌드시
.css로 정적으로 분리되어, 런타임 비용 0% - 스타일 정의에 타입 시스템이 직접 관여함
// 완전히 타입 안전한 스타일 시스템
const button = recipe({
variants: {
size: {
sm: { fontSize: '12px' },
lg: { fontSize: '18px' },
},
},
});
- 정의된 속성 외에는 사용 자체가 불가능 (자동완성 + 타입 오류)
- CSS 변수도
createThemeContract()로 선언 → 테마 전환도 타입 기반으로 처리
vanilla-extract는 단순히 CSS를 작성하는 도구가 아니다.
디자인 시스템을 타입으로 안전하게 코드에 담을 수 있게 해주는 방식이다.
성능도 빠르고, 타입으로 실수도 막아주고, 스타일 구조까지 깔끔하게 설계할 수 있는
세 가지 강점을 모두 갖춘 새로운 스타일링 방법이라고 할 수 있다.
vanilla-extract의 철학과 설계 목표
co-creator of CSS Modules인 Mark Dagiesh의 인터뷰를 인용하여 작성하였다.
이 도구는 CSS-in-JS 생태계가 겪어온 두 가지 근본적인 문제인 런타임 비용과 타입 불일치를 해결하려는 철학적 시도에서 출발했다.
그 배경에는 하나의 오랜 질문이 있다. “스타일을 좀 더 구조적으로, 안전하게 다룰 수는 없을까?”
"I'm still trying to come up with new ways to rewrite CSS. For some reason, I can't let it go."
— Mark Dalgleish
Mark가 만들어온 도구들인 CSS Modules, Braid, vanilla-extract 를 보면,
“CSS를 어떻게 하면 더 구조적으로 다룰 수 있을까?” 라는 고민이 일관되게 느껴진다.
그가 vanilla-extract에서 제시한 해답은 단순한 성능 개선이나 타입 보완이 아니다.
디자인 시스템을 예측 가능하게 만들고, 안심하고 확장할 수 있도록 만드는 구조적 접근이다.
vanilla-extract가 어떤 철학을 가지고 설계됐는지, 그 안에 담긴 3가지 핵심 원칙을 중심으로 정리해본다.
1) 런타임 없는 스타일 시스템

기존 CSS-in-JS 방식은 스타일이 JS 안에서 동작하기 때문에
브라우저에서 자바스크립트가 실행되기 전까진 스타일이 적용되지 않는다.
이 구조가 가지는 대표적인 문제는 아래와 같다:
- CSR 환경에선 초기 렌더링 지연
- SSR 환경에선 flickering 발생
- DevTools에서 디버깅 어려움
- JS 번들에 CSS 포함 → 번들 크기 증가
"It has the same performance characteristics of something like Sass… and at runtime there’s really no work left to do."
— Mark Dalgleish
vanilla-extract는 이 문제를 아예 구조적으로 차단한다.
모든 스타일은 빌드 타임에 .css.ts → .css로 정적 추출되며, 브라우저는 순수한 CSS 파일만 불러온다.
이 방식의 장점은 명확하다
- 초기 렌더링 속도 향상 (스타일 생성 비용 0%)
- JS와 CSS 번들 분리 → Tree-shaking 최적화
- 클래스 이름 해시 기반 → 충돌 없음
- DevTools에서 클래스 추적 쉬움
정적 CSS 구조 덕분에 오히려 스타일 구조 파악이 쉬워진다.
2) 타입 시스템에 의한 스타일 제어
"If you bet on the JS ecosystem… suddenly you're writing your styles in a type-safe language."
— Mark Dalgleish
CSS를 쓸 때 흔히 겪는 문제는 오타나 잘못된 값이 브라우저에서 실행되고 나서야 드러난다.
vanilla-extract는 이 부분을 TypeScript로 풀었다.
- 잘못된 스타일 입력 → 컴파일 에러로 즉시 감지
- 자동완성 → 디자인 토큰 범위 안에서만 작성 가능
- 허용되지 않은 값은 아예 입력 자체가 막힌다
특히 defineProperties()와 createSprinkles() 조합은 디자인 시스템 기반으로 만든 스타일 속성을 타입으로 강제할 수 있게 해준다.
const Box = (props: { padding: 'small' | 'medium' }) => (
<div className={sprinkles({ padding: props.padding })} />
)
"My editor is telling me that I’m getting my styles wrong."
— Mark Dalgleish
3) 디자인 시스템 = 토큰 시스템
기존 CSS 변수는 대부분 전역으로 선언된다. 하지만 그만큼 의존성과 추적이 어려운 단점도 있다.
vanilla-extract는 이 변수조차도 모듈 단위로 스코프를 제한하고, 타입으로 제어할 수 있도록 설계됐다.
"We want scoped variables as well. So if you want to use that brand color variable, you have to import it from the thing that created it."
— Mark Dalgleish
createThemeContract()– 토큰 스키마 정의createTheme()– 실제 값을 CSS 변수로 매핑createGlobalTheme()– 전역 테마 정의themeClass– 테마별 root 클래스 제공
이 구조의 장점은
- 디자인 토큰을 명시적으로 관리하고 CSS 변수로 매핑
- 다크 모드 전환은 클래스만 교체하면 됨
- 협업 시, 디자인 규칙을 타입 기반으로 공유 가능
"In trying to take a step forward, we've actually taken a big step back." — Mark Dalgleish
결론은 vanilla-extract는 CSS를 정적으로, 구조적으로 다루기 위한 도구
"In a perfect world, I should never have to write CSS."
— Mark Dalgleish
현실에서는 결국 커스텀 스타일이 필요할 수밖에 없다.
하지만 그 순간조차 명확한 스코프와 타입 시스템 안에서 통제할 수 있다면,
디자인 시스템은 더 예측 가능하고, 더 확장 가능해진다.
"It’s not that CSS-in-JS is bad. It’s just that the platform caught up."
— Mark Dalgleish
결국 중요한 건 “무엇을 쓰느냐” 가 아니라, “그걸 어떤 구조로 다루느냐”다.
vanilla-extract는 그런 구조를 고민하는 팀을 위한 도구다.
디자인 시스템을 정적으로 선언하고, 타입 기반으로 안전하게 확장할 수 있게 만들어준다.
"If you agree with the philosophy, then you should definitely use it."
— Mark Dalgleish
✍️ 마무리하며
vanilla-extract는 단순히 “타입을 지원하는 CSS-in-JS” 그 이상이다.
스타일을 정적이고 구조적으로 선언하고, 디자인 시스템을 타입으로 명확하게 통제할 수 있게 해준다.
- 더 빠른 렌더링
- 더 안정적인 코드
- 더 명확한 협업 구조
이 세 가지를 동시에 잡고 싶다면, vanilla-extract는 한 번쯤 제대로 살펴볼 가치가 있다고 생각한다.
이 글에서 다룬 핵심 개념들은 이후 시리즈에서 더 깊게 파고들 예정이다.
References
https://kentcdodds.com/chats/04/22/mark-dalgleish-chats-about-vanilla-extract
https://x.com/markdalgleish/status/1375332726664953860?utm_source=chatgpt.com
