개발자의 하루

[Vite] Storybook HMR 이슈 해결

db2 2025. 2. 24. 18:13

1. 문제

컴포넌트 소스 변경 시 스토리북에서 즉시 반영되지 않는 이슈 (HMR 동작하지 않음)

기존 설정

  • 모노레포 구조
apps/docs              # 스토리북으로 구성된 디자인 시스템 문서
├── .storybook
├── src
└── package.json       # 스토리북 실행 (dev: storybook dev -p 6006)

packages/ds-components # 디자인 시스템 컴포넌트
├── src
├── dist               
└── package.json       # 라이브러리 빌드 (build: vite build)
  • 스토리 파일(apps/docs/src/stories/button.stories.jsx)
import { Button } from '@ds/components';

export default {
  title: 'Components/Button',
  component: Button,
}

2. 원인

스토리북이 컴포넌트 패키지의 dist 폴더를 참조하고 있어서 컴포넌트 소스 변경 시, 다시 빌드해야만 변경사항이 반영됨.

3. 해결

컴포넌트 패키지에 dev script 추가

컴포넌트 패키지를 개발 모드에서 실행할 수 있도록 설정하기. Vite는 라이브러리 모드에서도 내부적으로 server.watch 기능을 지원하기 때문에 실제 UI를 띄우지 않더라도 파일 변경을 감지하고 HMR을 트리거 할 수 있음.

// packages/ds-components/package.json 

"scripts": {
    "dev": "vite dev"
}

스토리북 패키지의 alias 설정 추가

/dist 대신 /src를 직접 참조할 수 있도록 설정하기.

// apps/docs/.storybook/main.js

async viteFinal(config) {
    return {
      ...config,
       resolve: {
        alias: {
          '@ds/components': path.resolve(__dirname, '../../../packages/components/src/components'),
          '@ds/hooks': path.resolve(__dirname, '../../../packages/components/src/hooks'),
          ...
        },
      },
    }
}

이 때 주의해야할 점은 내 경우, 컴포넌트 패키지에서 아래와 같이 alias 설정을 해놨기 때문에 스토리북 설정에서도 동일한 구조의 alias 설정을 해야 모듈을 제대로 찾을 수 있다는 것이다. 즉, '../../../packages/components/src' 대신 '../../../packages/components/src/components'와 같이 하위 경로를 명확하게 지정해줘야 함!

// packages/ds-components/vite.config.js

resolve: {
  alias: {
    '@ds/components': path.resolve(__dirname, 'src/components'),
    '@ds/hooks': path.resolve(__dirname, 'src/hooks'),
    ...
  },
},

뽀나스 삽질 후기

터보레포에서 제공하는 design-system 예제가 있다. 구조는 완전히 동일하지만, 예제에서는 tsup --watch를 사용하고 변경사항이 스토리북에 즉시 반영되고 있었다. 그래서 나도 "파일이 변경될 때마다 빌드를 다시 해야 한다"는 생각에 꽂혀서 처음엔 vite build --watch 를 사용해서 빌드 파일을 업데이트하려고 했다. 하지만 이건 프로덕션 빌드이기 때문에 HMR이 동작하지 않았다.

모노레포 구조를 바꿔야하나 번들러를 바꿔야하나 고민을 하던 찰나, vite에선 HMR을 시키려면 vite dev를 사용해야한다는 걸 알았다. 그렇지만 저 명령어는 페이지를 띄울 때만 사용하는 거라 생각해서 답을 코앞에 두고 점점 삼천포로 빠지기 시작했다. 결론은 vite와 tsup 번들링 방식의 차이를 모르고 삽질을 했던 것...ㅎ 허무하지만 아차 싶었다! 다음에는 이 부분에 대해 더 깊이 파악해보면 좋을 것 같다.

++ 여기서 vite의 server.watch 옵션과 chokidar 같은 파일 감시 기능은 불필요하므로 혹시나 눈에 띄더라도 과감히 패스할 것.