모노레포 속 디자인 시스템, 1px까지 지키는 구조 설계법

모노레포 속 디자인 시스템, 1px까지 지키는 구조 설계법

Turborepo + pnpm 모노레포에서 공유 UI 패키지를 분리하고, 서브 경로 앱을 붙이고, Provider 래핑 순서까지 — '피그마에서는 됐는데 왜 안 되지?'를 근절하는 2025년형 프론트엔드 프로젝트 구조를 해부합니다.

모노레포 디자인 시스템 Turborepo Vite pnpm 서브 경로 호스팅 Provider 구조 프론트엔드 아키텍처
광고

핵심 이슈: "공유 UI 패키지 하나"가 만든 나비효과

프로덕션 SaaS 앱 하나가 잘 돌아가고 있었다. React 18, Vite, Tailwind CSS, shadcn UI 컴포넌트 30개 이상. 문제는 두 번째 앱이 같은 UI 컴포넌트를 필요로 한 순간 시작됐다. 30개 넘는 컴포넌트를 복붙해서 두 저장소에서 싱크를 맞추는 건 — 솔직히 말하면, 1px 차이가 두 앱 사이에서 계속 벌어지는 걸 지켜보는 것과 같다. dev.to의 Turborepo + Vite 시리즈(Part 1, 2)는 바로 이 지점에서 출발한다.

사실 이건 디자인 시스템을 진지하게 운영하려는 팀이라면 반드시 마주치는 분기점이다. Figma에서 컴포넌트를 아무리 정교하게 만들어도, 코드 레벨에서 단일 소스(Single Source of Truth)가 깨지면 디자인 토큰의 spacing-4가 앱 A에서는 16px, 앱 B에서는 14px이 되는 참사가 벌어진다. 피그마 활용을 다룬 velog 포스트에서도 강조하듯, 피그마의 실시간 협업과 플러그인 생태계는 디자인 단계의 일관성을 보장하지만, 그 일관성을 빌드 파이프라인까지 관통시키는 건 전적으로 프로젝트 구조의 몫이다.

맥락 해석: CSS @importexports를 무시하는 순간

모노레포 마이그레이션의 가장 날카로운 교훈은 눈에 보이지 않는 버그에 있었다. packages/ui에 shadcn 컴포넌트를 분리하고 package.jsonexports 필드를 정성스럽게 설정했는데, CSS @import "@acme/ui/styles/globals.css"가 통째로 실패한다. PostCSS와 Vite의 CSS 파이프라인은 Node.js exports 필드를 읽지 않기 때문이다.

이건 "Figma에서 볼 때는 괜찮았는데, 실제로 구현하면..."의 인프라 버전이다. JS 모듈은 exports로 잘 해결되지만, 비-JS 에셋(CSS, 폰트, 이미지)이 패키지 경계를 넘는 순간 해상도가 깨진다. 결국 Vite resolve.alias@acme/ui../../packages/ui/src에 직접 매핑하는 것이 실질적인 해법이었다. exports는 문서화 용도로만 남겨둔 것 — 이 현실 인식이 중요하다. 디자인 시스템 패키지를 만들면서 스타일 공유를 빼놓는 건, 컴포넌트 라이브러리에서 테마를 빼놓는 것과 마찬가지니까.

서브 경로 호스팅: 빈 화면과 싸우는 다섯 겹의 설정

Part 2에서 다루는 서브 경로 앱 통합(example.com/marketplace)은 사용자 입장에서는 그냥 URL 하나 바뀌는 것이지만, 개발자에게는 다섯 개 레이어가 한 치의 오차 없이 맞물려야 하는 퍼즐이다. Vite base, React Router basename, 개발 프록시, 트레일링 슬래시 리다이렉트 플러그인, index.html의 에셋 경로 — 이 중 하나라도 어긋나면 빈 화면, 에러 메시지 없음. 2시간을 잡아먹은 범인이 /marketplace 끝의 / 한 글자였다는 대목은, 프론트엔드에서 1px이 왜 중요한지의 인프라적 은유다.

여기서 React Native의 Provider 래핑 순서 문제와 구조적으로 닮은 패턴이 보인다. velog의 React Native Provider 기사에서 GestureHandlerRootView → SafeAreaProvider → KeyboardProvider → TamaguiProvider → PortalProvider 순서가 의존 관계 때문에 강제되듯, 모노레포의 서브 경로 설정도 레이어 간 의존 순서가 존재한다. 모바일에서 SafeArea 계산이 틀리면 노치에 콘텐츠가 잘리고, 웹 모노레포에서 base 경로가 틀리면 에셋 로딩이 침묵 속에 실패한다. 플랫폼이 달라도 "래퍼 순서가 곧 안정성"이라는 원칙은 동일하다.

45개 충돌을 0개로: Git 전략이 디자인 일관성을 지킨다

Part 2의 백미는 피처 브랜치 통합 전략이다. 기존 src/ 구조 기반으로 개발된 110개 파일짜리 브랜치를 모노레포에 합칠 때, 양쪽에서 독립적으로 마이그레이션 스크립트를 돌려 45개 충돌이 발생했다. 더 심각한 건 git checkout <branch> -- <file>로 일괄 해소한 뒤 26개 파일에서 코드가 조용히 삭제된 것이다. 런타임에서야 SyntaxError: does not provide an export named...가 터진다.

올바른 접근은 마이그레이션된 베이스에서 새 브랜치를 따고, 원본 피처 브랜치의 커밋을 cherry-pick한 뒤 마이그레이션 스크립트의 import 재작성 단계만 다시 실행하는 것이었다. 이 "idempotent한 검증 스텝"은 피그마에서 디자인 QA 체크리스트를 돌리는 것과 같은 맥락이다 — 한 번으로 끝나지 않고, 브랜치가 합쳐질 때마다 반복해야 한다.

시사점: 2025년 프론트엔드 구조 설계의 체크리스트

이 사례들을 종합하면, 2025년형 프론트엔드 프로젝트 구조에는 몇 가지 비-선택적 원칙이 드러난다.

  1. 공유 UI 패키지는 "순수 프레젠테이션"만 담아야 한다. React Query 훅에 의존하는 async-select를 공유 패키지에 넣는 순간, 앱 전체가 패키지의 의존성이 된다. 경계선은 "이 컴포넌트가 비즈니스 로직을 import하는가?"로 긋는다.
  2. 비-JS 에셋의 패키지 간 해상도는 별도 전략이 필요하다. exports 필드를 과신하지 말 것. Vite alias, webpack resolve.alias, 또는 디자인 토큰 빌드 파이프라인(Style Dictionary 등)으로 보완해야 한다.
  3. 서브 경로 앱은 체크리스트 기반으로 운영한다. 침묵하는 실패(silent failure)가 디버깅 시간을 기하급수적으로 늘린다.
  4. Provider/래퍼 순서는 문서화하고 테스트한다. 웹이든 모바일이든, 순서가 바뀌면 런타임에서야 드러나는 버그가 생긴다.

전망: 디자인-개발 갭은 "구조"로 좁혀진다

사실 이 모든 이야기의 본질은 하나다. 피그마 시안의 1px을 사용자 화면에서도 1px로 지키려면, 빌드 인프라와 프로젝트 구조가 그 정밀도를 지원해야 한다. Figma Dev Mode에서 디자인 토큰을 추출하고, 모노레포의 공유 패키지에서 그 토큰을 단일 소스로 소비하며, Turborepo의 캐싱이 빌드 일관성을 보장하는 — 이 파이프라인이 완성되어야 비로소 "디자인 시스템"이라고 부를 수 있다.

pnpm의 엄격한 모듈 격리가 Firebase 싱글톤을 깨뜨리고, 트레일링 슬래시 하나가 앱 전체를 백지로 만드는 현실에서, 프론트엔드 아키텍처는 더 이상 "폴더를 어떻게 나눌까"의 문제가 아니다. 패키지 경계, 에셋 해상도, 브랜치 전략, Provider 순서까지 — 보이지 않는 레이어의 정밀도가 결국 사용자가 보는 화면의 품질을 결정한다. 이제 1px은 CSS만의 문제가 아니라, 인프라의 문제다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요