본문으로 건너뛰기
오세두
프로젝트 목록

Atlas

여섯 개 제품 팀이 함께 쓰는 디자인 시스템

연도
2024년
역할
설계 및 개발 리드
팀 규모
4명
기술 스택
  • TypeScript
  • React
  • Tailwind CSS
  • Storybook
  • Vitest

모노레포 안에 Button이 여섯 개 있었습니다. 비유가 아니라 실제로 여섯 개였고, 그 여섯 개는 focus ring의 두께, disabled 상태의 opacity, 로딩 스피너가 라벨을 대체하는지 옆에 붙는지를 각자 다르게 결정하고 있었습니다. 어느 쪽이 맞는지는 아무도 몰랐고, 사실 아무도 물어보지 않았습니다.

Atlas는 그 여섯 개를 대체한 결과물입니다. 2024년에 네 명이 만든 40여 개 컴포넌트 라이브러리이고, 지금은 여섯 제품 팀이 모두 이 위에서 화면을 만듭니다. 그리고 파일 하나를 고치면 여섯 제품의 외형이 함께 바뀝니다.

문제는 컴포넌트가 아니었습니다#

첫 판단이 틀렸습니다. 저는 충분히 잘 만든 컴포넌트를 내놓으면 팀들이 알아서 갈아탈 거라고 생각했습니다. 3주를 들여 Button 하나를 다듬었습니다. API도 깔끔했고, 키보드 동작도 명세대로였고, 다크 모드도 처음부터 들어 있었습니다. 그리고 거의 아무도 쓰지 않았습니다.

이유는 시시했습니다. 그 Button으로 갈아타는 순간 각 팀이 소유한 화면에 visual diff가 생기고, 자기가 요청하지도 않은 diff에 승인 도장을 찍고 싶은 사람은 없습니다. 디자이너와 일정을 잡아야 하고, QA에 회귀 검증을 부탁해야 하고, 그 스프린트에서 원래 하려던 일을 미뤄야 합니다. 컴포넌트의 품질은 이 계산에 거의 들어오지 않았습니다. 걸림돌은 컴포넌트가 아니라 마이그레이션 리스크였습니다.

그래서 작업 순서를 뒤집었습니다.

토큰을 먼저, 컴포넌트를 나중에#

컴포넌트를 내놓고 채택을 설득하는 대신, 각 제품의 현재 외형을 그대로 재현하는 토큰 레이어를 먼저 배포했습니다. 색도 간격도 타이포도 바뀌지 않습니다. 스크린샷을 겹쳐 봐도 차이가 없어야 했고, 그게 목표였습니다. 변화가 보이지 않으면 디자인 리뷰 없이 머지할 수 있기 때문입니다.

/* 모든 제품이 이걸 import했고, 그 뒤에도 똑같이 보였습니다. */
@theme inline {
  --color-surface: var(--surface);
  --color-fg: var(--fg);
  --color-brand: var(--brand);
}

각 제품 저장소에는 그 제품이 원래 쓰던 hex 값을 그대로 담은 --surface, --fg 정의만 남겼습니다. 값을 새로 정하지 않고 기존 스타일시트에서 긁어모아 옮긴 것이 핵심입니다. 첫 PR은 대부분 서른 줄 안쪽이었고, "보이는 변화 없음"이라는 한 줄로 리뷰가 끝났습니다. 가장 저항이 클 것 같은 팀부터 먼저 찾아갔습니다. 그 팀이 머지하고 나면 나머지 팀에는 긴 설명이 필요하지 않기 때문입니다.

여섯 제품이 모두 토큰을 읽기 시작한 다음부터 계산이 달라졌습니다. 브랜드 색을 조정하는 일이 여섯 개의 PR에서 파일 하나로 줄었습니다. 다크 모드도 같은 방식으로 한 번에 붙었습니다. 이 신뢰를 확보한 뒤에야 컴포넌트를 하나씩 교체하기 시작했고, 이번에는 제가 설득하는 대신 팀들이 "Select도 Atlas에 있나요"라고 먼저 물어봤습니다.

채택은 품질 문제이기 전에 리스크 문제입니다. 예라고 답하는 비용을 먼저 낮추면, 품질에 대한 주장은 그다음에 저절로 통합니다.

40개라는 숫자보다 중요한 것#

흥미로운 숫자는 40이 아니라 테스트 표면입니다.

레이어개수시간이 실제로 들어간 곳
프리미티브12포커스 관리, 키보드 동작
조합 컴포넌트19쓰지 말아야 할 때를 문서화하는 일
레이아웃 유틸리티9거의 공짜. 이름이 붙은 CSS입니다

프리미티브 12개가 개당 소요 시간에서 압도적이었고, 그 시간은 대부분 키보드와 스크린 리더 작업이었습니다. 스크린샷에는 전혀 나타나지 않는 부분입니다. Dialog 하나의 포커스 트랩을 제대로 만드는 데 조합 컴포넌트 세 개를 붙이는 것보다 오래 걸렸습니다. 그래서 프리미티브에는 전부 Tab으로 훑고 지나가는 Playwright 테스트를 달았습니다. 눈에 보이지 않게 조용히 깨지는 동작이기 때문입니다. 같은 시기에 제품 쪽에도 Playwright E2E 파이프라인을 붙였고, 배포 후 롤백은 연 12건에서 2건으로 줄었습니다.

조합 컴포넌트 19개는 만드는 것 자체는 빨랐습니다. 오래 걸린 쪽은 경계를 정하는 일이었습니다. DataTable이 정렬까지 책임질지, 정렬은 제품에 맡기고 우리는 헤더의 상태 표시만 제공할지 같은 결정이 그렇습니다. 이런 질문은 코드로 답이 나오지 않아서 매번 두 개 이상의 제품 코드를 열어 보고 정했습니다. 그 결과 라이브러리가 자리를 잡은 뒤 새 화면 하나를 만드는 시간은 대략 절반으로 줄었습니다.

다시 한다면#

두 가지입니다.

Atlas를 모노레포와 같은 버전으로 묶었습니다. 그래서 breaking change가 한 번 생기면 여섯 제품이 동시에 올라가야 했습니다. Menu의 props 이름을 정리하는 작업 하나 때문에 세 팀의 스프린트에 예정에 없던 티켓이 들어갔고, 그 경험 이후 저는 필요하다고 판단한 정리를 두 번 미뤘습니다. 독립 버전으로 갔다면 일관성은 조금 잃었을 겁니다. 대신 각 팀이 자기 일정에 맞춰 major를 흡수할 수 있었습니다. 지금 보면 그쪽이 더 나은 거래였습니다.

그리고 문서를 마지막에 썼습니다. Storybook은 첫날부터 있었지만, "이걸 언제 써야 하는가"에 해당하는 산문은 몇 달 뒤에 나왔습니다. 그 공백에서 사람들은 합리적이지만 틀린 추측을 했습니다. Dialog를 툴팁처럼 쓰거나, 레이아웃 유틸리티를 만능 wrapper로 쓰는 식이었습니다. 저는 그걸 나중에 하나씩 되돌렸습니다. 판단이 아직 뜨거울 때 근거를 적어 두는 편이, 오용을 나중에 교정하는 것보다 훨씬 쌉니다.