StyleX로 미래의 Linear를 스타일링하기

작성일:2026.08.28|수정일:2026.08.29|조회수:4

StyleX로 미래의 Linear를 스타일링하기

이 포스트는 Linear의 개발자 Kenneth Skovhus가 리니어 팀 블로그에 올린 Styling Linear for the future with StyleX 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는 사견이 포함되어있기도 하다.

예상보다 훨씬 오래 걸렸고, 때로는 추진력을 유지하기가 버거웠다. 하지만 1,000개가 넘는 PR 끝에 Linear의 React 애플리케이션을 styled-components에서 StyleX로 옮겼다.

styled-components는 놀랄 만큼 오랫동안 Linear에 잘 맞았다. API는 아주 유연했고, 스타일은 컴포넌트와 함께 둘 수 있었으며, 더 특수한 일이 필요하면 일반 CSS를 꺼내 쓸 수 있었다. 가끔 성가신 border radius 같은 사소한 것에도 유난히 민감한 우리 팀에는 이 방식이 잘 맞았다.

하지만 결국 끝없는 설정 가능성은 부담이 됐다.

CSS는 이미 멀리 떨어진 곳에서 스타일을 건드리기 너무 쉽게 만든다. styled(Button) 같은 패턴은 컴포넌트의 스타일링 계약을 명시하는 대신, 외부에서 컴포넌트를 다시 열어 스타일을 입히는 일을 자연스러운 것으로 만들었다. 몇 달 전 Linear를 리디자인한 뒤 우리는 이 문제를 아주 선명하게 겪었고, 지금까지도 긴 UI 회귀의 꼬리를 쫓고 있다.

팀이 커지면서 UI를 조합하는 방식의 더 단단한 기반을 만들고 싶었다. 에이전트가 코드베이스에 점점 더 많이 기여하는 지금, 이 경계를 정의하는 일은 특히 중요해지고 있다.

좀 더 현실적인 이유는 성능이었다. 우리는 Linear가 빠른 상태를 유지하는 데 신경을 쓰고, CSS-in-JS는 렌더링 중에 스타일을 생성하고 CSS 규칙을 주입하는 일을 추가한다. React 18로 올리면서 동시성 렌더링이 들어온 뒤 성능 회귀를 겪은 것이 마지막 계기였다. 그때 styled-components도 유지보수 모드에 들어갔다.

그러면 질문은 이렇다. 제품 개발을 멈추지 않고, 크고 활발하게 바뀌는 코드베이스를 어떻게 옮길 것인가. 마이그레이션은 반복 작업이 많으니 코딩 에이전트에 잘 맞아 보인다. 그러나 이번 작업에는 통째로 맡기기에는 너무 많은 엣지 케이스가 있었다.

마이그레이션이 길어지는 동안 모델의 능력도 달라졌다. 프로젝트 중간에 Fable과 Sol이 출시되면서 에이전트는 더 나은 작업을 할 수 있게 됐다. 이 글에서는 결정론적 도구, 에이전트, 사람의 판단을 함께 쓴 마이그레이션에서 배운 것을 정리한다. 프로젝트가 진행되는 동안 셋이 맡는 비중도 계속 달라졌다.

StyleX를 선택한 이유

대안을 찾으면서 다음 라이브러리에는 양보할 수 없는 조건이 생겼다. 애플리케이션이 렌더링하는 동안이 아니라 빌드 시점에 스타일 생성 대부분을 처리해야 했다. 멀리 떨어진 곳에서 스타일을 입히는 일을 관례로만 말리지 말고, 의도적으로 어렵게 만들어야 했다. 그러면서도 컴포넌트가 명확한 스타일링 계약을 노출할 방법은 있어야 했다. 특이성 싸움 없이 파일을 넘나들어도 스타일 병합이 예측 가능해야 했고, React와 잘 맞으며 활발히 유지보수되고 장기적으로 안전한 선택지처럼 보여야 했다.

개발자 경험도 중요했고, 이제는 에이전트 경험도 점점 중요해지고 있다. 흔한 경우는 단순하게 남아야 하고, 스타일은 컴포넌트 가까이에 있어야 하며, 필요할 때는 현실적인 탈출구도 있어야 한다.

React에서 쓸 수 있는 주요 스타일링 선택지는 대부분 살펴봤다. 가장 가까운 대안은 정적 추출과 강한 타입 안전성을 제공하는 vanilla-extract였다. 하지만 API가 더 조각나 있다는 느낌이었고, 컴포넌트와 스타일 파일을 나누는 방식도 우리가 일하는 방식과 맞지 않았다.

Meta의 StyleX는 더 나은 균형을 보여줬다. 스타일은 컴포넌트와 함께 남고, API는 작으며, 스타일 해석은 결정적이다. 타입 안전한 스타일링 계약을 제공하면서도 컴포넌트 내부를 파고들어 외부에서 재스타일링하기는 의도적으로 더 어렵게 만든다.

이 제약에는 트레이드오프가 있다. 완전히 유연한 CSS 템플릿 리터럴에서 제한된 원자적 시스템으로 옮기면 부모에 의존하는 선택자, 전역 선택자, 래퍼를 통한 컴포넌트 재스타일링 같은 패턴은 더 어려워진다. 다만 조금 아이러니하게도, 그게 바로 목적이다. 옮기기 가장 어려운 패턴 상당수가 기존 시스템을 규모 있게 추론하기 어렵게 만든 패턴이기 때문이다.

거대한 마이그레이션을 감당 가능한 단위로 만들기

지금 이 마이그레이션을 시작한다면 Fable과 Sol에 직접 작업을 훨씬 더 많이 맡길 것이다. 하지만 시작 당시 styled-components는 자동화하기 유난히 어려운 대상이었다. 튜링 완전한 언어와 완전한 CSS의 힘, 완전히 열린 API를 제공하기 때문이다. 같은 의도를 표현하는 방법이 셀 수 없이 많고, 당시에는 에이전트가 그럴듯하지만 실제로는 틀린 결과를 내기가 불편할 정도로 쉬웠다.

Linear에 정식 디자인 시스템이 없었다는 점도 일을 어렵게 했다. 공통 컴포넌트는 있었지만, 그중 많은 것은 명확한 스타일링 계약 대신 폭넓은 API를 가진 채로 발전해 왔다.

이 제약 아래에서 나는 결정론적 코더모드부터 만들었다. styled-components-to-stylex-codemod는 큰 테스트 모음과 꽤 의도적인 아키텍처에서 시작했고, 이후 에이전트가 구현을 확장하는 데 도움을 줬다. 온라인 플레이그라운드, 파일 간 선택자 처리, 지금까지 만난 대부분의 엣지 케이스를 위한 회귀 방어를 포함한다.

마이그레이션 중 위험 줄이기

제품 개발을 동결하고 싶지 않았으므로 styled-components와 StyleX는 한동안 공존해야 했다. 그 중간 기간의 위험은 몇 가지 방식으로 줄였다.

팀 전체에서 채택을 끌어내기

마이그레이션의 기술적 세부 사항 외에도, 사람들을 styled-components에서 멀어지게 할 필요가 있었다. 진행 상황을 보이게 만들고, 코드베이스의 나머지 부분도 StyleX로 옮기는 데 추진력을 만들고 싶었다.

개발 도구 막대에 현재 페이지에서 남아 있는 styled-components 수를 추적하는 카운터를 추가했다. Linear를 돌아다니면 숫자가 올라가거나 내려갔다.

개발 성능 도구 막대에서 styled-components CSS rule count가 175로 표시된 화면

또 어떤 컴포넌트가 styled-components를 쓰고 어떤 컴포넌트가 StyleX를 쓰는지 강조하는 도구도 만들었다. 코드베이스의 특정 부분이 옮겨졌는지 쉽게 볼 수 있었고, 어쩌면 더 중요한 일은 사람들이 styled-components에서 벗어나도록 자극했다는 점이다.

코드베이스 전체의 진행률을 추적하고 마침내 styled-components에 작별을 고할 수 있는 날을 추정하기 위해 그래프도 계속 만들었다. 실제로 Zoom에서 술을 들고 축하했다.

React 스타일 파일에서 styled-components를 쓰는 비율과 StyleX만 쓰는 비율은 다음처럼 바뀌었다.
새 styled-components 파일 수도 처음에는 매주 꽤 추가됐지만, 마이그레이션 후반에는 거의 0이 됐다.

마지막으로 PR에서 새 styled-components 사용을 알리는 봇을 붙였다. 이 덕분에 모두가 책임을 의식하게 됐고, 회귀가 몰래 들어오는 일도 더 어려워졌다.

규모 있는 StyleX 강제하기

마이그레이션 이후 코드베이스가 가져야 할 관례를 강제하는 도구도 만들었다.

커스텀 린트 규칙은 의도한 경로를 분명하게 만들고, 오래된 패턴이 다시 스며드는 일을 막는다. Oxlint는 흔한 작성 실수, 디자인 토큰 사용, React prop이 생성된 StyleX 결과를 덮어쓸 수 있는 경우를 포함한 빠른 로컬 검사를 맡는다.

파일 단위 린터보다 더 많은 맥락이 필요한 규칙도 있다. 그래서 컴포넌트, import, 스타일을 파일 사이에서 따라가는 타입 인식 저장소 검사기도 만들었다. 이 도구는 스타일링 API를 검증하고, StyleX의 속성 단위 병합 때문에 기본 스타일이나 조건부 스타일이 조용히 사라질 수 있는 경우를 찾는다.

규칙은 몇 가지 큰 범주로 나뉜다.

마지막으로 포인터와 터치 기기에서 일관된 hover·press 피드백처럼 StyleX가 직접 지원하지 않는 동작에는 Vite용 빌드 시점 확장도 몇 개 추가했다.

StyleX는 닫힌 시스템이 아니다

탈출구를 완전히 없애려 하지는 않았다. 전역 선택자나 서드파티 DOM 재스타일링처럼 여전히 본질적으로 CSS 모양을 한 문제에는 CSS Modules를 범위가 분명하고 명시적인 대안으로 쓴다.

다른 곳에서는 StyleX를 확장했다. 가장 대표적인 것은 테마다. Linear의 테마는 사람이 직접 작성하는 대신 생성된다. 사용자 정의 테마는 기본 색, 강조 색, 대비 같은 몇 가지 입력값에서 시작하고, LCH 공간에서 100개가 넘는 색 변수를 만든다. 보통 사람들이 생각하는 배경색 교체보다 훨씬 더 깊다. 행이 선택되면 선택된 배경을 새 기본색으로 삼아 전체 테마를 다시 만들고, 레이블·테두리·컨트롤도 모두 다시 계산한다. 포커스, 메뉴, 떠 있는 표면도 마찬가지이며 각각 중첩된 ThemeProvider를 통해 자기 하위 트리에 범위를 둔다.

Offline Mode와 Core Performance 열을 포함한 어두운 테마의 Linear Agent tasks 보드

이 모델은 StyleX의 기본 테마 기능과 특히 잘 맞지는 않는다. 컴포넌트는 여전히 평범한 stylex.defineVars() 토큰을 기준으로 스타일을 작성하지만, 우리는 이 토큰을 자리표시자로 취급한다. ThemeProvider는 StyleX가 생성한 해시된 커스텀 속성 이름을 뽑아 내고, 생성된 테마마다 provider 경계에 범위를 둔 CSS 규칙 하나를 런타임에 주입한다. 결과적으로 테마 모델은 그대로 두면서, 스타일링 시스템의 나머지는 렌더링 경로 밖으로 옮겼다.

얻은 것

마이그레이션으로 Linear의 성능은 좋아졌지만, 계속되는 성능 최적화 작업과 얽혀 있어 정확한 종단 간 효과를 분리하기는 어렵다.

스타일 비용을 최대한 분리해 보기 위해 Fable로 3월의 프로파일과 현재 클라이언트 프로파일을 비교하고, 스타일 생성과 주입에 관련된 작업을 따로 봤다. 화면 요소가 많은 페이지에서는 런타임 스타일 주입을 제거해 메인 스레드 CPU 작업이 대략 20~35% 줄었다. 테스트에서는 중간급 기기에서 약 30% 더 빨라졌다. 화면 이동 중 수백 개 CSS 규칙을 주입하던 클라이언트는 이제 0개를 주입한다.

하지만 어쩌면 더 가치 있는 변화는 Linear의 스타일링 경계가 훨씬 더 선명해졌다는 점이다. 컴포넌트는 명시적인 스타일링 계약을 갖고, 우선순위는 결정적이며, 예전에는 관례나 캐스케이드에 의존했던 많은 패턴이 이제 도구로 강제된다. 1,000개가 넘는 PR 뒤에 남은 것은 런타임에서 더 빠르고, 잘못 쓰기 더 어렵고, 에이전트가 점점 더 많이 작성하는 코드베이스에 더 잘 맞는 스타일링 시스템이다.

댓글

댓글을 불러오는 중...