본문으로 건너뛰기
면접 학습
복습세션 →

이력서 기반

토픽 5 · Phase 1

모노레포 디자인 시스템 + 멀티 프로덕트 패키지 재편

질문 7개
  1. Q5-1

    디자인 시스템을 처음 구축하실 때 core/theme 패키지를 분리한 이유는요? 효과는요?

    핵심 포인트

    • core = UI 컴포넌트 (Button, Modal 등 동작·구조), theme = 디자인 토큰 (색상, 타이포그래피, 간격)
    • 분리 효과 1: theme만 업데이트해도 core 재배포 없이 디자인 변경 반영 가능
    • 분리 효과 2: 한 core를 여러 theme에 재사용 (다중 브랜드·라이트/다크)
    • 분리 효과 3: 각 패키지 변경 빈도가 달라 빌드·배포 사이클 분리 가능
    모범 답안먼저 답해보고 펼치기

    디자인 시스템에서 "스타일 토큰"과 "컴포넌트 동작"은 변경 빈도와 변경 주체가 달라요. 토큰은 디자이너가 자주 손대고, 컴포넌트는 개발자가 손대요. 이 둘을 한 패키지에 묶으면 토큰 한 줄 바꿀 때마다 모든 컴포넌트가 함께 재배포되고, 사용처가 의존성을 다 업데이트해야 하는 비효율이 생깁니다.

    그래서 처음부터 두 패키지로 분리했어요. core는 Button·Modal·Accordion 같은 UI 컴포넌트의 동작과 구조, theme은 색상·타이포그래피·간격 같은 디자인 토큰. core는 theme의 토큰 인터페이스를 import해서 쓰지만 구체 값은 알지 못해요. theme이 갱신돼서 새로 배포되면, 사용처는 theme만 업데이트하면 되고 core는 그대로예요.

    이 분리의 진짜 가치는 멀티 브랜드 / 라이트·다크 모드 전환에서 드러났어요. 하나의 core 위에 여러 theme을 갈아끼울 수 있는 구조가 자연스럽게 만들어져서, 나중에 멀티 프로덕트로 확장할 때 그대로 활용됐습니다. 변경 빈도가 다른 두 가지를 한 패키지에 묶지 마라 — 모노레포 분리 기준의 가장 일반적인 원칙이고, 본 케이스에서 가장 잘 들어맞은 사례였어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. theme이 너무 작은 패키지가 되지 않나요? 패키지 분할의 손익분기점이 있을 텐데요.

      작긴 합니다. 다만 "독립 배포 가능성"이 가치라서 크기보다는 변경 사이클이 기준이에요. theme이 한 달에 5번 바뀌고 core는 한 분기에 1번 바뀐다면 분리가 정당화됩니다.

    • Q-b. CSS variables로 토큰을 표현했다면 패키지 분리 자체가 필요했을까요?

      CSS variables 기반이면 사실 런타임에 toggling이 가능해서 패키지 분리 강제력은 약해져요. 다만 토큰의 type-safe 사용(자동완성, 잘못된 토큰 컴파일 에러)을 보장하려면 TypeScript 인터페이스가 필요하고, 그 인터페이스의 변경은 여전히 패키지 단위로 관리되는 게 깔끔했습니다.

    • Q-c. theme 패키지에 콘텐츠가 너무 적어서 별도 빌드가 오버헤드일 텐데요?

      빌드는 1초 미만이라 부담 안 컸어요. 오히려 빌드 사이클이 분리되어 있어서 core를 손볼 때 theme 빌드가 안 돌아가는 게 이득이었습니다.

    CS · 이론
    • Single Source of Truth (SSOT): 토큰을 한 곳에서 관리하는 것이 디자인 시스템의 본질
    • Design tokens: 색상·타이포·간격 등 디자인 결정을 코드로 표현한 단위. 업계 표준은 W3C Design Tokens Community Group
    • Package boundary와 변경 빈도: 모노레포 분할 기준은 "변경 빈도와 변경 주체가 같은가"
    • Type-safe theme: TypeScript의 const assertion + mapped type으로 토큰을 컴파일 타임에 검증
  2. Q5-2

    단일 core/theme에서 core-{product}/theme-{product}로 재편하셨다고 했는데, 그 시점과 비용은 어땠나요?

    핵심 포인트

    • 트리거: 신규 프로덕트 출시로 프로덕트마다 다른 테마·UI 변형 요구가 누적
    • 단일 패키지에 조건 분기를 추가하면 복잡도 + 배포 의존성 폭발이 예상되어 선제적 재편
    • 핵심 의사결정: "지금 분리하지 않으면 나중엔 못 자른다"는 판단
    • 비용 통제: API 시그니처 동일 유지 → import 경로만 일괄 변환 (find-and-replace)
    • 부수 효과: Emotion v10 → v11 병행 업그레이드를 함께 해결
    모범 답안먼저 답해보고 펼치기

    조직 내 신규 프로덕트가 연이어 출시되면서, 단일 core/theme로 감당이 안 되는 신호가 보였어요. 프로덕트 A는 둥근 모서리가 표준이고 B는 직각, A는 따뜻한 톤·B는 차가운 톤 같은 차이가 누적되면서, 한 컴포넌트 안에 if (product === 'A') 분기가 늘어나기 시작했습니다.

    그 시점에 결정해야 했어요. 지금 패키지를 자르거나, 단일 패키지에 조건 분기를 계속 쌓거나. 후자는 단기로는 빠르지만, 6개월 후 분기가 30개쯤 되면 어떤 컴포넌트든 한 번 손대기 부담스러워지는 패턴이라 위험하다고 봤어요. 그래서 선제적으로 core-{product} / theme-{product} 형태로 분리를 단행했습니다.

    비용 통제가 핵심이었어요. 컴포넌트 API 시그니처를 그대로 유지한 게 결정적이었습니다. 사용처는 import { Button } from '@org/core'import { Button } from '@org/core-productA' 로 바꾸기만 하면 되도록 했어요. find-and-replace로 일괄 변환이 됐고, 코드 의미를 손볼 일이 없었어요. 이 과정에서 동시에 Emotion v10 → v11 메이저 업그레이드까지 한 번에 묻어 갔어요. 어차피 import 경로를 다 만져야 하니, 같은 PR에서 v11 호환 코드로 옮기는 게 효율적이었습니다.

    이 재편 덕에 한 프로덕트의 디자인 변경이 다른 프로덕트에 영향을 주지 않는 안전한 구조가 됐고, 각 프로덕트 개발자들이 자기 패키지 안에서 자유롭게 실험할 수 있는 환경이 만들어졌어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 모든 프로덕트에 공통인 컴포넌트는 어떻게 했나요? 패키지마다 중복 코드가 생기지 않나요?

      진짜 공통은 base 패키지로 한 번 더 분리해서 각 product 패키지가 base를 import 하는 구조였어요. base → core-{product} → product app의 3단 의존성입니다.

    • Q-b. 같은 컴포넌트가 product마다 다르게 동작한다면, 디자인 시스템이라고 부를 수 있을까요?

      좋은 지적이에요. 그래서 분리 기준을 명확히 했어요. 동작 다름은 안 됨, 시각만 다름은 OK. 동작이 다른 변형이 필요하면 컴포넌트 자체를 분리하거나 props로 표현하도록 가이드했습니다. theme 분리만으로 해결되는 시각 차이가 분리의 정당화 근거였어요.

    • Q-c. 멀티 프로덕트 Storybook 통합은 어떻게 운영하셨나요?

      하나의 Storybook에 카테고리(addon-options) 로 product별 그룹을 두고, 각 product 패키지의 컴포넌트를 한 군데서 비교할 수 있게 했어요. 디자이너분들이 product 간 일관성을 검토하기 좋았고, 새 컴포넌트가 추가될 때 다른 product에 같은 패턴이 있는지 빠르게 확인할 수 있었습니다.

    CS · 이론
    • 선제적 리팩토링 vs 반응적 리팩토링: "냄새가 나기 직전에" 자르는 결단이 모노레포 운영의 키
    • API parity / 시그니처 보존: 마이그레이션 비용을 결정하는 가장 큰 변수
    • Codemod: 단순 변환은 sed, 복잡한 변환은 jscodeshift / ts-morph
    • Multi-brand design system: GitHub Primer, IBM Carbon, Material UI v5 등 업계의 멀티 브랜드 사례
    • Mono-repo dependency graph: 각 패키지의 의존 방향. 순환 의존이 생기면 패키지 분리 자체를 다시 봐야 함
  3. Q5-3

    Rollup으로 ESM/CJS 이중 빌드를 하셨다고 했는데, 왜 이중 빌드가 필요한가요? Vite·esbuild가 아닌 Rollup을 고른 이유는?

    핵심 포인트

    • 사용처의 환경이 다양: Next.js (ESM 선호), CRA·webpack 4 (CJS 호환 필요), Node 스크립트 (CJS) 등
    • ESM만 제공하면 일부 사용처에서 import 에러, CJS만 제공하면 트리쉐이킹 손실
    • 두 포맷을 동시에 빌드해서 package.json exports 필드로 환경별 분기
    • Rollup은 라이브러리 빌드의 사실상 표준 — 작은 번들, 깔끔한 출력, 플러그인 생태계
    • Vite는 앱 개발 dev server에 강점, esbuild는 속도에 강점이지만 라이브러리 출력 품질은 Rollup이 우세
    모범 답안먼저 답해보고 펼치기

    디자인 시스템 패키지의 사용처가 다양하다는 게 출발점이에요. 어떤 프로덕트는 Next.js 13+ 라 ESM이 자연스럽고, 어떤 사내 도구는 webpack 4 기반 CRA 라 CJS 호환이 필요하고, 빌드 스크립트는 Node에서 require 형태로 import해요. 한 포맷만 제공하면 누군가는 호환 문제로 막힙니다.

    그래서 ESM과 CJS를 둘 다 빌드해서 package.json의 exports 필드로 환경에 맞춰 자동 선택되게 했어요.

    {
      "exports": {
        ".": {
          "import": "./dist/index.mjs",
          "require": "./dist/index.cjs",
          "types": "./dist/index.d.ts"
        }
      }
    }
    

    번들러는 Rollup으로 갔어요. 이유는 셋입니다. 첫째, 라이브러리 빌드 출력 품질. Rollup은 dead-code elimination이 깔끔하고, 출력이 사람이 읽을 수 있을 정도로 단정합니다. Vite는 내부적으로 Rollup을 쓰지만 앱 개발 dev server에 최적화되어 있어 라이브러리 빌드에는 부가 설정이 필요해요. 둘째, 트리쉐이킹. 컴포넌트 30개 중 사용처가 5개만 import하면 5개만 들어가야 하는데, Rollup이 가장 안정적으로 잘 합니다. 셋째, 플러그인 생태계. emotion, TypeScript, dts, terser 등 라이브러리 빌드에 필요한 플러그인이 다 잘 정리되어 있어요.

    esbuild도 검토했어요. 빌드 속도는 Rollup의 10배 이상 빠르지만, type 선언 자동 생성과 일부 트리쉐이킹 옵션에서 아직 부족했고, 라이브러리 출력의 안정성에서 Rollup이 우세했습니다. 빌드 속도는 라이브러리에서 critical path가 아니라 의사결정에서 우선순위가 낮았어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 타입 선언(.d.ts)은 어떻게 자동 생성하셨나요?

      Rollup의 rollup-plugin-dts 또는 tsc --emitDeclarationOnly + rollup-plugin-dts 조합을 썼어요. 타입 선언도 한 파일로 번들해서 사용처의 IDE 부하를 줄였습니다.

    • Q-b. Emotion의 styled 컴포넌트 클래스명을 보존하려고 minify를 어떻게 다루셨나요?

      terser 옵션에서 emotion 관련 식별자를 mangle 제외했어요. 클래스명이 보존되어야 디버거에서 컴포넌트를 찾기 쉬워서, DX 측면에서 양보 못 하는 부분이었습니다.

    • Q-c. ESM-only로 가는 흐름이 일반적이지 않나요? CJS를 굳이 같이 줘야 했나요?

      신규 프로젝트는 ESM-only가 정답에 가까워요. 다만 본 시점엔 사내 legacy 프로덕트가 webpack 4 기반이라 CJS 호환이 필수였고, 점진적으로 ESM-only로 가는 로드맵을 두고 있었습니다.

    • Q-d. tsup이라는 옵션은 검토하셨나요?

      검토했어요. tsup은 esbuild 기반이라 빠르고 설정이 간단해서 매력적이었는데, 도입 시점엔 아직 안정화 초기였고 트리쉐이킹·플러그인 생태계에서 Rollup이 우세해서 보류했습니다. 신규로 시작한다면 tsup도 좋은 후보예요.

    CS · 이론
    • ESM vs CJS: 정적 import (트리쉐이킹 가능) vs 동적 require. Node.js와 브라우저 모듈 시스템 통합의 역사
    • package.json exports 필드: 환경별·경로별 진입점을 선언적으로 분기하는 현대적 패키지 메타데이터
    • Tree-shaking 조건: side-effect-free 모듈 + 정적 분석 가능한 import. sideEffects: false 명시 중요
    • Rollup vs Vite vs esbuild vs Webpack: 각각의 적합 영역과 트레이드오프
    • dual package hazard: ESM과 CJS 양쪽이 다른 인스턴스로 로드되어 발생하는 문제. exports 필드 설계 시 주의
  4. Q5-4

    Lerna 모노레포를 선택한 이유와 한계는요? 지금 다시 한다면 어떤 도구를 고르시겠어요?

    핵심 포인트

    • 도입 시점(2020~2021)에 Lerna가 모노레포의 사실상 표준
    • 핵심 가치: 패키지 간 로컬 링크, 일괄 publish, 의존성 hoisting
    • 한계: bootstrap 속도, 빌드 캐싱 부재, Lerna 자체의 메인테이너 변동
    • 지금 신규라면: pnpm workspaces + Turborepo, 또는 Nx
    • 솔직 답변: 도입 시점 선택은 적절했고, 신규는 다른 선택지를 골랐을 것
    모범 답안먼저 답해보고 펼치기

    도입 시점이 2020~2021년 즈음이었는데, 그 시점에 모노레포 표준은 사실상 Lerna였어요. yarn workspaces와 결합해서 패키지 간 로컬 링크 + 일괄 publish 흐름을 만드는 게 일반적이었고, 우리도 그 패턴을 그대로 채택했습니다.

    가치는 분명했어요. core 패키지가 변경되면 자동으로 theme도 dependent로 인식되어 빌드 순서가 정해지고, 일괄 npm publish가 한 번에 끝나는 워크플로우가 안정적이었습니다.

    한계도 있었어요. 첫째, bootstrap이 느렸어요. 패키지 수가 늘면 의존성 install이 분 단위로 길어졌습니다. 둘째, 빌드 캐싱이 없어서 의존이 안 바뀐 패키지도 매번 빌드 됐어요. 셋째, Lerna 자체의 메인테이너 변동으로 한동안 정체기가 있었습니다. (이후 Nrwl이 인수해서 다시 활발해졌지만 그건 후 이야기)

    지금 새로 시작한다면 pnpm workspaces + Turborepo 조합이에요. pnpm은 strict하면서 디스크 효율도 좋고, Turborepo는 빌드 그래프 분석·캐싱·원격 캐시까지 갖춰서 모노레포 핵심 운영 비용을 크게 낮춰 줍니다. Nx도 좋은 선택지인데 학습 곡선이 약간 가파르고, 단순 라이브러리 모노레포라면 Turborepo가 가성비가 좋아요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 패키지 간 의존성 관리에서 hoisting과 strict의 차이는요?

      yarn classic과 npm은 의존성을 hoisting해서 한 곳에 모으는데, 그러면 phantom dependency(선언 안 한 패키지를 우연히 import 가능)가 생깁니다. pnpm은 isolated node_modules로 strict하게 분리해서 phantom dependency를 원천 차단해요.

    • Q-b. 모노레포의 단점은 없나요?

      빌드/CI 시간이 누적되고, 한 PR이 여러 패키지를 건드릴 수 있어 변경 영향 범위가 커져요. 큰 조직에선 코드 소유권이 흐려질 수도 있고요. 그래서 일정 규모 이상에서는 Bazel 같은 더 정교한 빌드 시스템으로 가거나, polyrepo로 다시 자르는 의사결정도 합니다.

    • Q-c. 본인 경험에서 Lerna의 가장 큰 페인포인트는?

      빌드 캐시가 없어서 CI에서 매번 모든 패키지를 풀 빌드한 점이었어요. Turborepo로 옮긴 팀들이 CI 시간이 70% 줄었다는 케이스를 보면서 후회한 부분입니다.

    CS · 이론
    • Monorepo vs Polyrepo: 코드 공유, 의존성 일관성, 변경 영향 범위에서의 트레이드오프
    • Phantom dependency: 선언하지 않은 의존성을 우연히 사용 가능한 hoisting 부작용
    • Build graph & caching: Turborepo·Nx·Bazel의 핵심 기능. 변경된 패키지와 그 의존자만 다시 빌드
    • Workspaces (yarn/pnpm/npm): 패키지 매니저의 모노레포 지원 기능. Lerna가 이걸 위에 한 겹 더 얹은 도구
    • Changesets: 모노레포의 버전 관리·CHANGELOG 자동화 표준 도구. Lerna versioning을 대체하는 흐름
  5. Q5-5

    SVG → React 컴포넌트 자동 변환 파이프라인을 어떻게 구축하셨나요? SVGR과의 차이는요?

    핵심 포인트

    • 디자이너가 Figma export로 떨어뜨린 SVG 폴더를 watch → React 컴포넌트로 자동 생성
    • xml2js로 SVG XML을 파싱, 색상 속성을 theme 토큰 변수로 치환, React 컴포넌트로 emit
    • 핵심 가치: Light/Dark 모드 동시 지원 + 토큰 일관성 자동 보장
    • SVGR은 일반적인 SVG→React 변환 도구 — 우리가 추가한 차별점은 "토큰 주입과 라이트/다크 자동 처리"
    • 도입 효과: 아이콘 추가 프로세스가 단축되고, 사람의 실수로 색상이 어긋나는 일이 사라짐
    모범 답안먼저 답해보고 펼치기

    디자이너가 Figma에서 SVG로 export하면 보통 색상이 hex 값으로 박혀 있어요. 그걸 그대로 React 컴포넌트로 만들면 라이트/다크 모드에서 색이 안 따라가는 문제가 생깁니다. 사람이 손으로 hex를 토큰 변수로 바꾸는 건 50개 100개 단위의 아이콘에선 비현실적이에요.

    파이프라인은 이렇게 동작합니다. 디자이너가 SVG 파일들을 폴더에 떨어뜨려요. 스크립트를 한 번 실행하면, xml2js로 각 SVG를 XML 트리로 파싱하고, fill·stroke 속성에 들어 있는 hex 값을 미리 정의한 매핑 테이블 — #3D8BFF → var(--color-primary) 같은 — 으로 치환합니다. 그런 다음 SVG XML을 React JSX로 emit해서 컴포넌트 파일을 자동 생성해요. 결과적으로 모든 아이콘 컴포넌트가 동일한 토큰 변수를 참조하게 되니, theme이 light/dark로 바뀌면 아이콘 색도 자연스럽게 따라갑니다.

    SVGR과의 차이는 "토큰 주입과 라이트/다크 인식"이에요. SVGR은 일반적인 SVG → JSX 변환을 잘하지만, 색상을 토큰으로 바꾸는 건 사용자의 후처리로 둡니다. 우리는 그 후처리를 파이프라인에 박아 넣어서, 아이콘이 들어오는 순간 시스템 일관성이 자동으로 강제되도록 했어요.

    도입 효과는 두 가지였어요. 첫째, 아이콘 추가 시간 단축. 갯수가 50개든 100개든 단 한 번의 스크립트 실행으로 끝납니다. 둘째, 사람의 실수가 사라졌어요. hex 값을 손으로 옮기다가 오타가 나거나 토큰을 안 쓰는 케이스가 자동화로 원천 차단됐습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 매핑 테이블이 부정확하면 색이 틀리게 변환될 텐데요?

      디자이너와 함께 매핑 테이블을 미리 합의했고, hex가 매핑에 없으면 변환 스크립트가 워닝을 띄우고 대상 파일을 명시합니다. 새 색이 들어오면 매핑부터 추가하는 룰이었어요.

    • Q-b. SVGO 같은 최적화는 함께 했나요?

      네, 변환 전 SVGO로 불필요한 메타데이터·주석·소수점 정밀도를 정리했습니다. 아이콘 한 개당 평균 20~30% 사이즈 감소 효과가 있었어요.

    • Q-c. 자동 변환된 컴포넌트는 어떻게 테스트하셨나요?

      Storybook에 자동 등록되는 카테고리를 만들고, 각 아이콘이 light/dark에서 어떻게 보이는지 시각 회귀 테스트를 돌렸어요. Chromatic 기반의 visual regression이었습니다.

    • Q-d. SVGR + custom template으로도 같은 효과를 낼 수 있을 텐데, 직접 만든 이유는요?

      도입 시점엔 SVGR custom template의 자유도가 부족했고, xml2js 기반 직접 구현이 우리 매핑 룰과 더 잘 맞았어요. 신규로 시작한다면 SVGR + plugin이 더 무난한 선택일 수 있습니다.

    CS · 이론
    • Code generation: 사람이 작성하던 코드를 빌드 타임에 자동 생성하는 기법. 디자인 시스템·GraphQL 코드젠·OpenAPI 클라이언트가 대표 사례
    • AST 기반 변환: XML/HTML/JS 모두 파싱→AST→변환→emit의 동일 파이프라인
    • xml2js / fast-xml-parser: Node 환경의 SVG XML 파싱 도구
    • CSS variables vs inline color: SVG에서 토큰을 표현할 때의 두 갈래. CSS variables가 라이트/다크에 자연스러움
    • Visual regression testing: Chromatic, Loki 등으로 컴포넌트 시각 변화를 자동 검증
  6. Q5-6

    Figma 토큰을 코드 토큰으로 자동 동기화하셨다고 했는데, 어떤 흐름인가요? Light/Dark 모드 설계는 어떻게 토큰 레벨에서 했나요?

    핵심 포인트

    • Figma의 디자인 토큰을 추출 → JSON 파일로 export → 스크립트가 TypeScript theme 파일로 변환
    • 디자이너가 Figma에서 토큰 수정 → 스크립트 한 번 실행 → 코드 토큰 즉시 갱신
    • Light/Dark는 토큰 레벨에서 설계: 두 모드의 토큰을 동일 키로 매핑, 컴포넌트는 의미적 토큰만 참조
    • 컴포넌트 코드는 라이트/다크를 인지조차 안 함 — theme 교체만으로 전환
    모범 답안먼저 답해보고 펼치기

    디자이너가 Figma에서 토큰을 수정하면 코드에 반영되기까지 며칠이 걸리던 게 출발점이었어요. 디자이너가 슬랙에 올려주면 개발자가 hex를 일일이 옮기고, PR 만들고, 리뷰받고, 머지하는 과정이 반복됐습니다.

    흐름은 이렇게 자동화했어요. 디자이너가 Figma 플러그인이나 export 도구로 디자인 토큰을 JSON으로 뽑아냅니다. 우리 모노레포에는 그 JSON을 받아 TypeScript theme 파일로 변환하는 스크립트가 있고, 디자이너가 토큰을 갱신했을 때 PR 한 번에 스크립트 실행 → 자동 생성된 theme 파일 변경분이 들어가요. 사람이 hex를 직접 만지지 않습니다. (※ 실제 Figma 연결 방식이 플러그인인지 Tokens Studio인지 본인 구현에 맞춰 답변)

    Light/Dark 모드 설계가 가장 신경 쓴 부분이에요. 핵심 사상은 컴포넌트는 의미적(semantic) 토큰만 참조한다예요. 컴포넌트 코드에는 color.primary 가 아니라 color.text.default 처럼 의미를 담은 키를 쓰고, light theme에서 color.text.default = #1a1a1a, dark theme에서 color.text.default = #f5f5f5 같이 같은 의미 토큰이 두 모드에서 다른 값을 갖도록 매핑했어요. 결과적으로 컴포넌트 코드는 light/dark의 존재를 모르고, theme 객체를 갈아끼우는 것만으로 모드가 전환됩니다.

    이 설계 덕에 다크 모드를 신규 도입했을 때 컴포넌트 코드는 한 줄도 바뀌지 않았고, 디자이너가 dark theme 토큰만 채워 넣으면 끝나는 워크플로우가 됐어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. semantic token과 primitive token을 어떻게 구분했나요?

      두 레이어로 분리했어요. primitive는 blue.500 = #3D8BFF 같은 색 자체, semantic은 text.default = blue.900 처럼 사용 의미. 컴포넌트는 semantic만 참조하고, semantic은 primitive를 참조해요. light/dark는 semantic의 매핑만 갈아끼우면 됩니다.

    • Q-b. 모드 전환은 런타임에 어떻게 되나요? CSS variables로 했나요, JavaScript theme 객체 swap으로 했나요?

      Emotion ThemeProvider에 theme 객체를 swap하는 방식으로 했어요. CSS variables도 검토했지만, type-safe한 토큰 사용이 강제되는 점에서 ThemeProvider가 더 잘 맞았습니다. 신규로 시작한다면 CSS variables + Emotion의 혼합이 트렌드에 맞아요.

    • Q-c. 디자이너가 Figma에서 잘못된 토큰을 만들면 어떻게 잡나요?

      변환 스크립트에서 토큰 스키마를 검증해요. semantic 토큰이 존재하지 않는 primitive를 참조하면 빌드 실패. 디자이너가 직접 PR을 보낼 때 CI에서 빨간 줄이 뜨도록 했습니다.

    • Q-d. 시스템 다크 모드 (prefers-color-scheme)와 사용자 명시 선택을 어떻게 조합했나요?

      우선순위는 사용자 명시 > 시스템 설정 > 기본값(light). 사용자 선택을 localStorage에 저장하고, 없으면 prefers-color-scheme 미디어 쿼리로 결정했어요.

    CS · 이론
    • Design tokens (W3C): 색상·타이포·간격을 코드와 디자인 양쪽이 같은 단위로 표현하는 표준
    • Token tier (primitive / semantic / component): 토큰을 의미 추상화 레이어로 나눈 업계 패턴 (Material, Carbon, Spectrum)
    • CSS variables vs JS theme object: 런타임 모드 전환의 두 갈래. CSS variables가 SSR 친화적, JS theme이 type-safety에 우세
    • Tokens Studio (구 Figma Tokens): Figma의 토큰을 JSON으로 추출하는 사실상 표준 플러그인
    • Style Dictionary (Amazon): 디자인 토큰 변환 파이프라인의 표준 도구. 자체 구현 대신 Style Dictionary로 가는 흐름이 일반적
  7. Q5-7

    Emotion v10 → v11 메이저 업그레이드를 멀티 프로덕트 패키지 재편과 함께 진행하셨다고 했는데, 어떻게 안전하게 했나요?

    핵심 포인트

    • v10 → v11은 패키지명 변경(emotion@emotion/react)과 일부 API 변경 동반
    • 일반적으론 별도 PR로 분리하는 게 정석, 하지만 import 경로를 어차피 다 바꿔야 하는 상황을 활용
    • 핵심: 테스트 안전망 확보 → 한 PR 안에서 두 변경을 결합 → CI 그린 확인 → 머지
    • 부수 효과: v11의 새 jsx pragma, 더 작은 번들 사이즈 확보
    모범 답안먼저 답해보고 펼치기

    일반적으로 메이저 업그레이드와 패키지 재편을 같이 묶는 건 위험해요. 문제 생겼을 때 어느 쪽이 원인인지 분리가 어렵거든요. 그런데 본 케이스는 사용처가 어차피 모든 import 경로를 바꿔야 하는 상황이었어요. import styled from '@org/core'import styled from '@org/core-productA' 로 바뀌는데, 그 과정에서 emotion v10의 jsx pragma → v11 방식으로 옮기는 건 같은 파일을 한 번 더 만지는 것뿐이라 추가 비용이 거의 없었습니다.

    세 가지 안전 장치를 두고 진행했어요. 첫째, 시각 회귀 테스트 안전망. Storybook 기반 visual regression을 미리 충분히 깔아 둔 상태였어요. emotion 업그레이드는 클래스명 생성 방식이 미묘하게 달라서 시각 차이가 날 가능성이 있었는데, snapshot 변화가 의도된 것인지를 PR 단위로 확인할 수 있었습니다. 둘째, 점진적 전환. 한 product 패키지를 먼저 v11 + 새 import 경로로 옮기고 운영에 일주일 띄워본 뒤, 안정성이 확인된 후 다른 product로 확장했어요. 셋째, Codemod 활용. emotion v11이 제공하는 codemod를 일부 적용해서 jsx pragma·import 경로 변경을 자동화하고, 사람이 만질 부분을 줄였습니다.

    결과적으로 한 분기에 걸쳐 모든 product가 v11 + 신규 패키지 구조로 전환됐고, 그 사이 운영 장애 없이 진행됐어요. 부수 효과로 번들 사이즈가 약간 줄었고, v11의 jsx 자동 변환 덕에 코드가 더 깔끔해졌습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. 두 변경을 한 PR에 묶은 게 후회되는 부분도 있나요?

      한 product에서 미세한 스타일 회귀가 있었을 때 emotion인지 패키지 분리인지 디버깅에 한나절 걸린 적이 있어요. 결국 emotion 쪽이 원인이었는데, 분리된 PR이었으면 더 빨리 잡았을 거예요. 그래서 다음에 비슷한 상황이면 두 변경을 같은 commit에 두지 않고 PR 안에서 commit을 분리하는 식으로 가겠습니다.

    • Q-b. 시각 회귀 테스트가 없었다면 어떻게 했을까요?

      결합을 포기하고 별도 PR로 분리했을 거예요. 테스트 안전망이 결합의 정당화 조건이었어요. 안전망이 없으면 보수적으로 가는 게 맞다고 봅니다.

    • Q-c. emotion 외에 다른 CSS-in-JS (styled-components, vanilla-extract)는 검토하셨나요?

      본 시점에는 emotion이 표준이었어요. vanilla-extract 같은 zero-runtime CSS-in-JS는 매력적이었지만 마이그레이션 비용이 너무 컸고, RSC와 styled-components의 호환 이슈가 본격 부상하기 전이라 emotion 유지가 합리적이었습니다. 지금이라면 vanilla-extract나 panda-css를 적극 검토할 것 같아요.

    CS · 이론
    • Major version upgrade의 일반 절차: changelog 정독 → codemod 적용 → 테스트 → 점진 배포
    • Visual regression testing: Chromatic, Loki, Percy 등. 시각 변화를 PR diff처럼 검토
    • Codemod (jscodeshift): API 변경을 코드 레벨에서 자동 변환
    • CSS-in-JS landscape (2025): Emotion, styled-components의 전성기 → Server Components 호환성 이슈로 zero-runtime CSS-in-JS (vanilla-extract, Panda CSS) 와 Tailwind CSS로의 분기
    • JSX pragma: /** @jsx jsx */ 같은 주석으로 컴파일러에 JSX 변환 함수를 지정. emotion v11은 자동 jsx 변환을 활용해 pragma 부담을 줄임