문제
컴포넌트를 적게 써도 전체 스타일이 따라오는 구조
사내 디자인 시스템은 여러 제품 앱에서 사용하는 공통 패키지입니다. 제 담당은 기존 라이브러리의 배포 구조, 토큰과 오버레이 동작을 유지보수하고, 변경을 소비하는 앱에 적용하는 일이었습니다.
예를 들어 버튼만 필요한 화면에서도 패키지 진입점이 전체 CSS를 가져오면 사용하지 않는 모달, 드롭다운과 다른 컴포넌트의 스타일이 함께 포함됩니다. JavaScript의 사용하지 않는 코드를 제거하는 것만으로는 이 CSS import를 없앨 수 없었습니다. 컴포넌트 API를 그대로 유지하면서, 실제 사용 범위에 맞게 스타일이 따라오도록 배포 단위를 바꿔야 했습니다.
모션도 별도의 과제였습니다. 오버레이의 열고 닫는 효과에 사용하던 런타임 의존성을 제거하려면 보이는 애니메이션뿐 아니라 DOM을 언제 붙이고 떼는지까지 유지해야 했습니다. 닫기를 누르자마자 요소를 지우면 퇴출 모션이 사라지고, 닫는 도중 다시 열었는데 이전 완료 처리가 실행되면 열린 창이 사라질 수 있습니다.
선택한 방법
1. CSS 파일 분할과 배럴의 부작용을 함께 정리
컴포넌트별 ESM 모듈과 CSS를 나란히 출력하고, 각 컴포넌트가 자신의 스타일을 import하도록 빌드를 변경했습니다. DatePicker가 Popover를 사용하면 의존 관계를 따라 Popover의 스타일도 가져오게 했습니다. 필요한 스타일을 소비 앱이 직접 조합하도록 넘기지 않고, 컴포넌트의 의존성과 일치시킨 구조입니다.
파일을 나누는 것만으로 끝나지는 않았습니다. 공통 CSS import가 배럴에 남으면 번들러가 그 진입점을 부작용이 있는 모듈로 취급할 수 있습니다. 테마, autofill과 링크 스타일은 필요한 모듈로 옮기고, 배럴에는 재수출만 남겼습니다. CSS는 부작용이 있는 파일로 표시하되, 사용하지 않는 JavaScript 모듈까지 모두 유지하지 않도록 패키지 설정을 맞췄습니다.
빌드 후처리는 확장자 없는 상대 import를 실제 ESM 출력 경로로 바꾸고, SCSS를 같은 CSS layer로 컴파일합니다. 배럴로 옮길 수 없는 export가 추가되면 조용히 누락하는 대신 빌드를 실패시키도록 했습니다. 개발 중 watch 재빌드에서도 같은 후처리를 실행해 개발 결과와 배포 결과가 달라지지 않도록 했습니다.
2. 화면의 모션과 요소의 수명을 분리
모션 자체는 CSS transition으로 옮기고, 작은 usePresence 훅은 마운트 수명만 관리하도록 했습니다. 열리는 렌더에서 바로 요소를 마운트해 포커스 처리가 한 렌더 늦어지지 않게 하고, 닫힐 때는 해당 요소의 애니메이션이 끝난 후 해제합니다.
다시 열리면 이전 해제 대기를 취소합니다. 모션 감소 설정 등으로 진행 중인 애니메이션이 없으면 즉시 해제합니다. 자식 요소의 애니메이션까지 모두 기다리면 본문의 무한 Spinner 때문에 창이 닫히지 않을 수 있어, 기다릴 대상도 오버레이 요소로 제한했습니다. @react-spring/web과 공개 spring 훅을 제거하면서 이런 동작 경계를 함께 유지했습니다.
3. 패키지 변경을 소비 앱의 마이그레이션까지 연결
기존 전체 style.css 진입점은 호환성을 위해 유지했습니다. 다만 새 컴포넌트 CSS와 함께 가져오면 중복되므로, 앱의 전체 스타일 import를 제거하는 절차와 테마 변수만 필요한 경우를 마이그레이션 문서에 구분했습니다. Next.js의 webpack 빌드에서는 패키지 import 최적화 설정도 필요하다고 안내했습니다.
토큰은 tokens.json에서 SCSS, TypeScript와 테마 변수를 생성하도록 통합했습니다. 생성 결과와 커밋된 파일이 다르면 검사가 실패하게 해, 한쪽 출력만 수동 수정하면서 테마가 어긋나는 문제를 드러내도록 했습니다. 소비하는 Notiiv 앱에서는 패키지 버전과 스타일 import를 함께 갱신했습니다.
확인한 결과
검증한 동작
- 배포 경계: 컴포넌트별 ESM·CSS 출력, 실제 파일을 가리키는 import, 재수출만 남은 배럴을 변경 코드로 확인할 수 있습니다. 지원하지 않는 export와 watch 후처리 누락도 후속 커밋에서 방어했습니다.
- 오버레이 수명: 열리는 렌더의 마운트, 모션이 없을 때 즉시 해제, 퇴출 완료 대기, 퇴출 도중 재열기, 처음부터 닫혀 있던 요소의 콜백 처리를 단위 테스트로 남겼습니다.
- 소비 앱 적용: 마이그레이션 문서에는 전체 CSS import 제거, 테마 변수와 번들러별 설정을 기록했습니다. Notiiv의 네 앱은 같은 디자인 시스템 패키지를 참조합니다.
디자인 시스템은 공개 NPM 패키지와 Storybook으로 배포되며, Notiiv 네 앱이 같은 패키지를 참조합니다. 필요한 컴포넌트와 스타일이 같은 의존 관계로 배포되고, 별도 모션 런타임 없이 오버레이 수명을 관리하도록 바뀐 것입니다. 제 기여는 배포 후처리와 설정, presence 훅, 토큰 생성과 소비 앱 변경까지 이어지는 범위입니다.
근거
- ESM + CSS
- 컴포넌트별 배포
- 4 apps
- Notiiv 모노레포의 패키지 의존성 기준
- CSS motion
- react-spring 런타임 의존성 제거