Ray Book
에세이

StyleX는 너무 일찍 나왔다

2024년엔 다들 반신반의했던 Meta의 스타일링 시스템이 결국 살아남았다. 설계는 처음부터 이 카테고리에서 가장 앞서 있었는데, 왜 다들 망설였는지.

essaycssstylexstylingmetaopinion

공개되고 반년, 아무도 선뜻 안 썼다

StyleX는 2023년 12월에 오픈소스로 풀렸다. Meta가 자기네 Facebook, Instagram, WhatsApp, Messenger, Threads 전반에서 쓰는 스타일링 시스템을 그대로 공개한 거다. 발표만 놓고 보면 엄청난 이벤트였다. 수십억 MAU 제품을 받치는 CSS-in-JS 도구가 통째로 열린 거니까.

그런데 이상하게도 공개 직후 반년 동안 생태계 분위기는 미지근했다. Hacker News에서나 몇 번 회자되다 조용해졌고, 메이저 스타일링 논의에서 StyleX가 기본 선택지로 올라오는 경우는 드물었다. 주변에서도 "한 번 건드려볼까"라고 말하는 사람은 있어도, 실제 프로덕션에 올리는 팀은 거의 없었다.

이게 기술의 문제였냐 하면, 아니었다. 문서도 꽤 정돈돼 있었고, API도 깔끔했다. 적어도 겉보기에는. 문제는 시기였다. 2023년 말은 runtime CSS-in-JS가 성능 문제로 한창 밀려나던 때라, 이름에 CSS-in-JS가 붙은 새 도구를 반길 분위기가 아니었다. 여기에 Meta가 낸 오픈소스들의 전례가 겹쳤다.

Meta가 낸 것들의 세 갈래 길

Meta의 오픈소스 프로젝트를 쭉 세워놓으면 운명이 세 갈래로 나뉜다.

첫 번째 길, React. 세계 표준이 됐다. 이건 다들 아는 성공 스토리인데, 이걸 기본값이라고 생각하면 곤란하다. 예외에 가깝다.

두 번째 길, Flow. Meta가 JavaScript에 타입 시스템을 붙이려고 만든 건데, TypeScript한테 외부 생태계에서 완전히 밀렸다. 2021년 InfoQ 인터뷰에서 Flow 팀은 "Types-First" 모드로 방향을 틀었다고 밝혔다. 외부 범용성보다 Meta 내부 코드베이스 최적화에 집중하겠다는 선언이었다. 기술적으로 끝난 건 아니지만, 외부 생태계와는 사실상 따로 가게 됐다.

세 번째 길, Relay. Meta의 GraphQL 클라이언트. 기술적으로는 탁월한데 너무 복잡해서 대중화에 실패했다. Apollo Client 같은 더 가벼운 대안에 밀렸고, "Meta 규모에선 맞는데 우리한테는 과하다"는 평이 굳어졌다.

StyleX가 나왔을 때 사람들이 떠올린 게 이 세 길이었다. React처럼 될 것이냐, Flow처럼 내부화될 것이냐, Relay처럼 소수만 쓰는 도구로 남을 것이냐. 셋 중 어느 쪽일지 아무도 확신하지 못했다. 솔직히 말하면, 두 번째나 세 번째일 확률이 더 높아 보였다. Meta가 만든 도구는 늘 Meta 코드베이스를 기준으로 설계되기 때문이다.

테크 리드가 회사를 떠났다

그러다 안 좋은 소식이 하나 들려왔다.

StyleX의 테크 리드였던 Naman Goel이 Meta를 떠나 OpenAI로 옮겼다. 그는 StyleX를 처음 만들고 오픈소스화를 주도한 사람이다. 2025년 11월에 Meta Engineering 블로그가 올린 StyleX 업데이트 포스트는 감사의 말에서 그를 "past maintainers"로 부른다. 과거형이다.

거기다 2024년은 Meta 전반에 레이오프가 이어지던 해였다. 10월에 Reality Labs, Instagram, WhatsApp 조직에서 대규모 인력 감축이 있었다. StyleX 팀이 직접 영향을 받았다는 공개 보도는 없었지만, 당시 분위기는 "어느 팀이든 내일 없어져도 이상하지 않다"는 쪽이었다.

이런 상황이 겹치니 다들 더 주저했다. 만든 사람은 떠났고, 모회사는 감축 중이었고, Meta가 쌓아온 "공개하고 내부화시키는" 전례가 있었다. 프로덕션 스타일링 시스템을 갈아끼우는 게 얼마나 큰 결정인지 생각해보면, 이 타이밍에 StyleX에 올인한다는 건 합리적인 선택이 아니었다.

그런데 뜯어보면 잘 만들어놨다

그런데 여기서 분위기가 한번 바뀐다. 이런 뒤숭숭한 상황과는 별개로, StyleX의 설계 자체는 이 카테고리에서 가장 앞서 있었다.

가장 먼저 눈에 들어오는 건 atomic CSS 컴파일이다. 빌드 타임에 모든 스타일을 원자 단위 클래스로 쪼개 정적 CSS 파일로 떨어뜨린다. Meta는 atomic 클래스로 CSS 크기가 80% 줄었다고 밝혔다. 프로젝트가 커져도 번들이 선형으로 증가하지 않고, 어느 순간부터 평탄해진다는 얘기다.

API는 이렇게 생겼다.

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  base: { fontSize: 16, color: 'grey' },
  highlighted: { color: 'rebeccapurple' },
});

<div {...stylex.props(styles.base, styles.highlighted)} />

겉보기엔 styled-componentsemotion과 비슷하다. 그런데 동작 방식이 다르다. stylex.props가 반환하는 건 className 문자열이지 런타임에 생성된 스타일 객체가 아니다. 모든 스타일은 이미 빌드 타임에 정적 CSS로 뽑혀 있고, 런타임은 그중 어떤 클래스를 적용할지 고르는 역할만 한다.

그리고 이 라이브러리에는 더 중요한 게 하나 있다. 마지막에 적용한 스타일이 이긴다는 규칙이다.

나중에 쓴 스타일이 우선한다

CSS-in-JS를 써본 사람이면 한 번쯤 겪는 지옥이 있다. 부모 컴포넌트의 스타일과 자식 컴포넌트의 스타일이 합쳐질 때, 뭐가 이길지 예측이 안 되는 문제. CSS의 specificity 규칙이 개입하고, 선언 순서가 꼬이고, 결국 !important로 덮고 끝낸다.

StyleX 공식 문서는 여기에 아주 명확하게 답한다.

The last style applied always wins.

stylex.props(styles.base, styles.highlighted)에서 highlighted의 color가 base의 color를 덮는다. 정의 순서가 아니라 적용 순서가 기준이다. 내부적으로는 atomic 클래스 간의 충돌을 컴파일러가 추적해서, 뒤에 오는 것이 항상 이기도록 className을 정리한다.

이게 왜 중요하냐면, CSS-in-JS가 10년 넘게 씨름한 문제 대부분이 그냥 사라지기 때문이다. 조건부 스타일도 그냥 배열로 붙이면 된다.

<div {...stylex.props(
  styles.base,
  isActive && styles.active,
  isHover ? styles.hover : styles.inactive,
)} />

falsy 값은 무시되고, truthy인 것들만 순서대로 적용되고, 겹치는 속성은 뒤에 온 것이 우선한다. specificity를 머릿속에 굴릴 필요가 없다. 이것만으로도 도입할 이유가 충분한 팀이 많을 거다.

타입이 붙는 토큰, 그리고 0에 가까운 런타임

StyleX가 기존 CSS-in-JS와 또 다른 지점이 테마와 토큰 시스템이다.

// tokens.stylex.ts
import * as stylex from '@stylexjs/stylex';

const DARK = '@media (prefers-color-scheme: dark)';

export const colors = stylex.defineVars({
  primaryText: { default: 'black', [DARK]: 'white' },
  accent: { default: 'blue', [DARK]: 'lightblue' },
});

defineVars는 CSS 변수를 타입과 함께 선언하는 API다. 파일명이 반드시 .stylex.ts (또는 .stylex.js, .stylex.tsx) 로 끝나야 하고, named export여야 한다. 이 제약이 있어야 컴파일러가 파일을 특별 취급해서, 런타임 의존성 없는 고유한 CSS 변수명으로 뽑아낼 수 있다.

여기서 얻는 게 두 가지 있다. 하나는 디자인 토큰에 TypeScript 타입이 자동으로 붙는다는 거다. 테마 변경, 컬러 오타, 시맨틱 토큰 재명명 같은 작업을 할 때 IDE가 잡아준다. 다른 하나는 미디어 쿼리가 변수 정의 안으로 녹아든다는 거다. 다크 모드를 토큰 레벨에서 정의하면, 그걸 쓰는 컴포넌트는 아무것도 몰라도 된다.

런타임 얘기도 한 번 짚고 가자. StyleX를 "zero runtime"이라고 부르는 건 반만 맞다. 공식 문서는 이렇게 쓴다.

No runtime style injection. Generate a static CSS file at compile-time. Tiny and fast runtime for merging class names.

스타일 생성은 컴파일 타임에 끝나지만, stylex.props가 어떤 className을 실제로 선택할지 결정하는 작은 런타임은 남아있다. 이게 중요한 건 React Server Components와의 궁합 때문이다. 서버에서 런타임에 CSS를 주입하는 전통적 CSS-in-JS는 RSC 환경에서 문제가 많다. StyleX는 스타일 자체가 이미 정적이기 때문에, 클라이언트에서 className을 머지하는 얇은 런타임만 있으면 된다. RSC, 정적 사이트, 엣지 렌더링 어디서든 무리 없이 돈다.

2025년, 릴리스는 안 멈췄다

2024년 내내 우려가 있었지만, 릴리스 로그를 보면 기우였다는 게 드러난다. 2024년엔 0.5.0에서 0.9.3까지 올라갔고, 2025년엔 월 두 번꼴로 0.10부터 0.17까지 나왔다. 이 기간에 shareable media queries, View Transitions, Anchor Positioning, combinator 기반 스타일링 같은 현대 CSS 기능이 꾸준히 붙었다.

외부 도입도 생겼다. Figma와 Snowflake가 StyleX를 쓰기 시작했다는 게 2025년 11월 Meta Engineering 포스트에서 확인된다. Meta 내부에선 Facebook, Instagram, WhatsApp, Messenger, Threads가 모두 StyleX 위에서 돈다.

Meta Engineering 블로그에 올라온 StyleX: A Styling Library for CSS at Scale 포스트
위기설이 돌던 시기를 지나 Meta가 직접 올린 StyleX 포스트 (2025년 11월). Engineering at Meta

이게 무슨 의미냐면, 처음 우려했던 세 갈래 중 어느 쪽도 아니었다는 거다. React처럼 세계 표준이 된 건 당연히 아니지만, Flow처럼 내부화되지도 Relay처럼 고립되지도 않았다. Tailwind와 동일한 대중성을 노리는 건 아마 앞으로도 아닐 거고, 그럴 필요도 없다. 대규모 프로덕트, 엄격한 타입, RSC를 쓰는 팀에겐 이미 가장 합리적인 선택지다.

수렴점에 가까워지는 카테고리

스타일링 라이브러리의 역사는 꽤 어지러웠다. 인라인 스타일, CSS Modules, styled-components, emotion, vanilla-extract, Tailwind, Panda CSS. 각자 해결한 문제도 다르고 포기한 것도 다르다.

StyleX는 이 계보에서 특정 문제 하나를 더 푼다기보다는, 지금까지의 합의점들을 한 번에 묶어낸다. 컴파일 타임 추출, 원자 단위 번들, 타입 안전성, 예측 가능한 합성 규칙, 테마 시스템, RSC 호환. 뭔가를 새로 발명했다기보다는, 여러 라이브러리가 각자 증명해온 좋은 아이디어들을 한데 모아놓은 결과물이다.

그래서 StyleX를 "이 카테고리의 마지막 답"이라고 말하긴 어렵다. CSS 표준이나 브라우저 기능으로 흡수되는 일은 아마 없을 거다. 다만 CSS-in-JS라는 카테고리 안에서 설계가 수렴할 지점에 가장 가까이 가 있는 건 사실이다. 2024년엔 이 판단을 내리기 어려웠고, 2025년이 돼서야 결과가 보였다.

한 번 관심에서 밀려난 기술이 다시 올라오는 경우는 흔치 않다. StyleX는 그 흔치 않은 경우에 들어 있다.

참고 자료