- Published on
storybook 전략 개선
- Authors
- Name
- sulmo
문제 사항
팀에서 더 이상 오픈소스 지원 방향을 제공하지 않는 방향으로 의사결정을 하고, 외부 범용성보다 팀의 사용성이 높은 디자인 시스템을 구축하는 방향으로 전환하게 되었다. 이전에 사용해왔던 개발과 디자인 플로우 및 방식들은 합리적이지 못하고 관습화 되어갔다. 현재 범용 컴포넌트(디자인 시스템)와 콘솔 컴포넌트(디자인 시스템이 될수도 아닐수도 있으며, 콘솔 도메인이 주입되어 범용 컴포넌트로 뺄 수 없지만 전역적으로 재사용되는 컴포넌트)의 개념도 더이상 알맞지 않다.
위와같은 상황으로 콘솔 컴포넌트 관리 이슈가 있다.
- 같은 기능을 하나 디자인이 조금씩 다른 컴포넌트들이 추적이 되지 않아, 중복 생성되어 생산성이 낮아짐
- 관리 프로세스가 개발팀과 디자인팀이 각각 따로 진행되며, 구두로만 논의하여 싱크가 맞지않음
- 테스트와 문서화가 부족하여 UX와 설계 명확성이 떨어짐. context의 의존도가 높은데, 컴포넌트나 훅의 요구사항을 명확히 하지 않고 급히 개발하기 때문으로 추측. 범용성을 가지려면 컴포넌트나 훅의 명확한 목적과 기능에 대한 명세가 있어야 하고, 그것을 바탕으로 소통이 필요
- 콘솔 컴포넌트 및 훅에 대한 정보를 알기 어렵고, 기능이 명확하지 않고, 기능검증도 잘 되지 않음. 특히 훅의 경우 테스트코드가 있더라도, 개발자가 이런 훅이 있는지도 모르고 사용하지 않게됨
- 훅의 경우 UX가 반영된 경우가 많음. 현재는 제대로 관리하고 운영되지 않으므로 이를 스토리북에서 디자인과 개발 두파트가 수면위로 올려 관리하는 프로세스 필요
해결 방안
콘솔 컴포넌트 재정의
- As-Is : 디자인 시스템이 될수도 아닐수도 있으며, 콘솔 도메인이 주입되어 범용 컴포넌트로 뺄 수 없지만 전역적으로 재사용되는 컴포넌트
- To-Be: 콘솔 도메인이 포함된 컴포넌트이거나 콘솔에서 사용하는 스토어나 server state의존이 있는 컴포넌트
위와 같이 콘솔 컴포넌트의 정의를 명확히 하며, 디자이너들도 싱크가 가능하게 스토리북으로 올리기로 결정했다.
Storybook을 기존 스토리북에 올릴것인가? 새로 생성할 것인가?
신규 storybook Case
- 깔끔하다.
- console과의 의존만 존재한다.
- story 대상이 console에만 특정한다. (명확성)
현재 storybook에서 추가 Case
- storybook 버전관리를 한쪽만하면 된다. (일관성 증가, 관리 비용 변화 없음)
- 어차피 vue3 아니라서 storybook7 써야한다.
- 한 앱에서 DS와 console component 둘다 볼 수 있다.
- Open Source접음. mirinae를 외부 공유가 필요 없다.
- 관리 담당자가 서로 다른팀에 존재하는가? NO
결론: 관리 담당자가 서로 한팀이며, 미리네와 동일한 방식으로 편리한 작업을 추구하는게 맞다고 보인다. open source는 접었고, mirinae와 console component를 분리할 니즈는 보이지 않음.
→ 현재 storybook에서 추가 하기로 결정
모노레포에서 서로다른 두 워크스페이스를 한 스토리북에 담으며 나타난 사이드 이팩트가 너무 컸고 이에 따라 이전 논의의 결론이 바뀜
console 컴포넌트를 storybook으로 연결하는 과정에서 컴포넌트를 불러오지 못하는 에러 발생
Failed to fetch dynamically imported module: <http://localhost:6006/@fs/Users/mzc01-sulmo/dev/console/apps/web/src/common/components/badge/auto-sync-state/AutoSyncState.stories.ts>
storybook의 vite alias와 console의 vite alias가 달라서 경로 충돌이 원인
vite.config.js에서 alias에대해
resolve.alias에서 특정 키값을 원하는 기본 path로 변경해주는 옵션을 제공한다.
plugin 으로 파일 경로에 따라 앱별로 다른 alias를 세팅 시도
plugin에서 resolveId라는 콜백으로 넘기는 옵션이 있다. 번들 파일의 source와 importer라는 아규를 가지고 리턴 값이 해당 빌드 파일의 최종 위치가 된다.
둘다 alias설정 인데 여기서 중요한건 순서이다. resolve.alias가 있다면 먼저 적용되고 적용된 정보 기준으로 resolveId 콜백이 반영되어 돈다.
현재 적용되어있는 resolve.alias를 제거하고, vite Plugin의 resolveId 훅으로 importer기준 path 작성 시 문제가 있다.
- resolveId를 사용하면 vite에서 기본적으로 확장자를 자동처리되지 않음. 이부분도 직접 구현되어야 함.
- optimizeDeps에 적용되지 않음. -> cjs를 모두 ems로 바꾸고 해야한다.. 모노레포를 사용 시, 연결된 디펜던시가 ESM 형태로 내보내져야 합니다. 만약 그렇지 않다면, 해당되는 디펜던시들을
optimizeDeps.include
와build.commonjsOptions.include
설정에 추가 OptimaizeDeps? 일반적인 사용 사례는 소스 코드에서 직접 탐색할 수 없는 Import가 있는 경우 플러그인 변환의 결과물에 Import가 사용된 경우를 예로 들 수 있습니다. 이는 Vite가 초기 스캔 시 해당 Import를 발견할 수 없음을 의미 디펜던시가 크거나(내부 모듈이 많은 경우) CommonJS 포맷이라면include
옵션에 명시해야 합니다. 만약 디펜던시가 작고 이미 ESM 스타일로 작성되어 있다면exclude
옵션에 명시해 브라우저에서 바로 불러올 수 있도록 설정할 수 있습니다. node_modules가 아닌 로컬 패키지인 경우 storybook에 web, mirinae를 의존 추가 해줘야 설정 가능하다.
결정사항
- storybook에 web, mirinae를 의존 추가, OptimizeDeps에 cjs들 추가, resolveId 훅으로 분기 및 파일 확장자 자동 등록 구현
- optimizeDeps 대신 mirinae를 esm으로 변경 + storybook도 esm변경
- 파일 확장자 자동 등록의 경우 resolve.alias 활용을 추천, 안되면 fs으로 직접 가져와 달기
- web, mirinae vite alias 변경
- 파일 변경사항이 매우 많으며, 전체 테스트가 필요. git history 전체 갈림.
위 둘중 하나를 선택해야했는데, 이 방안 보다 이전 진행사항 번복이 더 효과적임으로 아래와 같은 결정을 내림.
→ 결론 : console component와 mirinae를 따로 구분하기로 함. + storybook composition이라는 기능 추가
Storybook 전략 개선 (스토리북 신규 생성을 위한)
앞으로 web storybook과 같은 추가 앱이 생길 예정이라 함께 사용할 것을 고려하여Storybook 전략을 개선하기로 했다.
- mirinae-foundation 분리
- Storybook 관련 세팅 분리
- Web Storybook 앱 추가
mirinae-foundation 분리
프로젝트 초기와 현재는 Design System(Mirinae) 정의가 다르다.
초기: 오픈소스 라이브러리 역할이 주된 목적인 범용 UI 라이브러리 현재: 생산성에 맞춰서, 우리 서비스의 Fundamental 부터 전반적인 내용을 디자인시스템
기존 ui 라이브러리인 mirinae만 의존이 있었던 값들을 mirinae-foundation으로 분리하기로 했다. 더이상 mirinae는 단순 UI 라이브러리 스콥의 이름이 아니다. 우리 서비스의 Design System으로서 mirinae로 사용한다. 앞으론 스토리북도 쪼개지고 다양한 앱에 디자인시스템의 기본정책이 적용될 것이며, 그 기준은 mirinae-foundation 모듈을 사용할 예정이다.
screen, color, icon 과 같은 기본 디자인 토큰들 이 있으며, 이와 관련된 md 파일도 함께 해당 패키지에 존재한다.
mirinae와 icon은 컴포넌트까지 의존이 있어 완전히 분리하도록 하였다. 앞으론 프레임워크 또한 변경 될 수 있으며, 프레임워크 별 UI 라이브러리가 따로 생성되더라도 foundation의 기준에 따라 config를 사용하고 완벽하게 관심사를 분리하여 관리한다.
Storybook 관련 세팅 분리
mirinae-foundation으로 기반 정보들의 분리는 끝났다. 이제 Storybook 차례다.
디자이너 팀과 협업을 위해 모든 컴포넌트 문서들은 한 서비스에서 보여야한다. 그래서 Storybook-composition을 사용해 스토리북 마이크로 프론트엔드를 진행하며, 스토리북을 신규 생성 시 공통된 세팅값은 분리한다.
Storybook Composition

unified-storybook
├ ui-storybook
├ app-storybook
├ ...
- unified-storybook : 외부 스토리북을 어떤형태로 연결할지 정의하고 관리하며 통합 스토리북에대한 스토리파일을 관장한다.
- ui-storybook : 공통 컴포넌트에대한 스토리북 (프레임워크 별 추가될 수 있을 것 같다.)
- app-storybook : 앱 도메인이 포함된 컴포넌트이거나 앱에서 사용하는 스토어나 server state 의존이 있는 컴포넌트
공통된 스토리북 세팅값들
- storybook관련된 plugin
- peerDependencies인 react, react-dom
Web Storybook 추가하기
- 분리된 모듈을 가져와 기본 세팅
- 콘솔 컴포넌트를 위한 디펜던시 설치
- mirinae 컴포넌트
- store 관련 라이브러리
- api 대신 사용할 목데이터 연동
- 배포
전체 스토리북 개발환경 및 배포 현황 안내
💡 [unified-storybook, mirinae-storybook, web-storybook] 각 스토리북의 개발환경 세팅과 배포 방식을 정리합니다.
개발 환경 구동법
mirinae와 web storybook을 동시에 개발해야하는 케이스가 아니라면 각 스토리북에서만 작업하시는 걸 추천합니다.
Mirinae Storybook
(기존과 동일)
Web Storybook

Unified Storybook
데브환경이다보니 task 프로세스의 종료가 존재하지 않아 두번 실행하게 되었습니다.
sb-all:dev 실행 : 내부 모듈 storybook run dev(mirinae, web …)
- sb-unified:dev실행 : unified storybook run dev
배포 방식
storybook의 composition 기능은 부분적인 마이크로프론트엔드 방식으로 여러 storybook을 한 스토리북에서 볼 수 있도록 제공하는 기능입니다. 이번 기획/디자인팀과 개발팀 간의 소통 및 작업 효율을 높이기위해 스토리북을 분류하고 확인 가능하도록 composition기능을 적용하였습니다.
저희의 경우 총 2가지의 스토리북이 관심사에 따라 분리되었고, 범용 컴포넌트(common 컴포넌트)와 web(App 컴포넌트) 앞으로 프로젝트에 따라 더 추가될 수 있습니다.
Mirinae Storybook
cloudforet docs와 같은 공식 문서들에 링크되어있는 케이스가 있어. 배포 프로세스를 그대로 유지합니다.
- github action → build → s3 + cloudfront 배포
- 위 액션이 직접 구동 가능하며, console 릴리즈 액션에 포함되어 콘솔과 함께 자동배포되고 있습니다.
Web Storybook
현재 개발 중인 모든 컴포넌트는 버전이나 config와 상관 없이 다 보여야 함으로, web-storybook (vercel project setting)의 경우 develop 브랜치가 update되면 배포되도록 설정하였습니다.
Unified-storybook
따로 컴포넌트가 존재하지 않아 추가 개발이나 배포될 일이 거의 없습니다. vercel에 git repo연동을 하지 않고 수동 배포되도록 하였습니다. (예상 작업: introduce(README) 파일 수정)
- unified-storybook 배포 Github Action: 조건 필요없이 워크플로우 실행만 하시면 됩니다.
배포 현황
- mirinae-storybook: 기존 s3 버킷 배포형태 그대로 유지 (링크)
- web-storybook: vercel 배포 (링크)
- unified-storybook: vercel 배포 (링크)
추가 Notice
unified-storybook의 경우 배포된 하위 storybook에 데이터를 요청하는 방식입니다.
요청하는 주체인 unified-storybook가 각 module storybook에 요청을 하게 되어 만약 도메인이 다르면 cors에러를 발생 시킵니다.
unified-storybook의 경우 vercel이라 하위 모듈이 vercel에서 배포될 경우 무관하지만, 만약 mirinae와 같이 다른 도메인에 배포된다면 unified-storybook 도메인을 허용해 주셔야합니다.