Next.js 15 + Storybook에서 webpack tap 에러 해결하기
문제 상황
디자인 시스템 작업을 위해 SCSS 토큰 파일들을 추가하고 Storybook에서 확인하려는데, npm run storybook 실행 시 빌드가 실패했다.
npm run storybook
에러 메시지
Module not found: TypeError: Cannot read properties of undefined (reading 'tap')
SCSS 관련 import를 모두 주석 처리해봐도 동일한 에러가 발생했다. 스토리 파일들이 .module.scss를 사용하고 있어서, SCSS를 처리하는 과정 자체에서 webpack 내부 에러가 터지고 있었다.
원인 분석
webpack 플러그인 API 변경
Next.js 15.5.12는 내부적으로 webpack 5.101.3+를 번들하고 있다. 문제는 이 버전에서 webpack의 플러그인 API가 변경되었다는 것이다.
Storybook의 @storybook/builder-webpack5는 이전 webpack API에 의존하고 있어서, SCSS 모듈을 처리할 때 NormalModule의 내부 hook에 접근하는 과정에서 tap 메서드가 undefined로 반환되는 것이었다.
이 문제는 이미 Storybook 저장소에 보고되어 있었다.
핵심 원인을 정리하면:
- Next.js 15.5.12가 번들한 webpack 5.101.3+에서 플러그인 API가 변경됨
- Storybook의 webpack 빌더가 이전 API에 의존
- SCSS 처리 시 webpack 내부의
tap메서드 접근 실패
해결 방법
Step 1: webpack 버전 고정
package.json에 npm overrides를 추가하여 webpack을 호환되는 버전으로 고정했다.
{
"overrides": {
"webpack": "5.101.2"
}
}
5.101.2는 플러그인 API 변경 직전 버전이다. overrides를 추가한 후 의존성을 다시 설치한다.
npm install
이것만으로 tap 에러는 해결된다.
Step 2: SCSS @use 경로 해결
webpack 에러를 해결하고 나니 다음 에러가 나타났다.
Can't find stylesheet to import.
SCSS 파일에서 @use "@/styles/tokens/..." 형태로 path alias를 사용하고 있었는데, Next.js dev 서버에서는 내부적으로 이 alias를 처리해주지만, Storybook에서는 자동으로 전달되지 않았다.
.storybook/main.ts에 webpackFinal을 추가하여 @/ alias를 명시적으로 설정했다.
import type { StorybookConfig } from "@storybook/nextjs";
import path from "path";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
{
name: "@storybook/addon-essentials",
options: {
docs: false,
},
},
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
staticDirs: ["../public"],
webpackFinal: async (config) => {
config.resolve = config.resolve || {};
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "../src"),
};
return config;
},
};
export default config;
@storybook/nextjs가 TypeScript의 paths 설정을 webpack resolve alias에 자동으로 적용해주긴 하지만, SCSS의 @use는 sass-loader를 통해 별도로 처리되기 때문에 webpack의 resolve alias가 직접 적용되지 않는 경우가 있다. webpackFinal에서 명시적으로 alias를 설정하면 이 문제가 해결된다.
Step 3: preview.ts에서 글로벌 SCSS import
마지막으로 .storybook/preview.ts에서 디자인 토큰 SCSS 파일을 import한다.
import type { Preview } from "@storybook/react";
import "@/styles/tokens/typography/fonts.scss";
import "@/styles/tokens/colors/_semantic.scss";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
마무리
정리하면 이 문제는 두 가지가 겹친 상황이었다.
- webpack 호환성: Next.js 15.5가 번들한 webpack 5.101.3+와 Storybook의 webpack 빌더 간 API 불일치 →
overrides로 버전 고정 - SCSS path alias: Storybook이 Next.js의 path alias를 SCSS 레벨까지 자동 전달하지 않음 →
webpackFinal에서 resolve alias 명시
Storybook 측에서 webpack 5.101.3+ 호환 패치가 나오면 overrides는 제거할 수 있다. 그때까지는 이 워크어라운드가 가장 안정적인 방법이다.
참고 자료
storybookjs/storybook#32301 - Cannot read properties of undefined (reading 'tap')
