코드잇 프론트엔드 부트캠프 23기 파이널 프로젝트를 시작했다.
오늘은 초기 공통컴포넌트 설계 중, Next.js 프로젝트에서 디자인 아이콘(svg)을 깔금하게 관리하기 위해 SVGR을 설정하여 React
컴포넌트처럼 임포트해서 사용하고 있었다.
import ImgEmpty from '@/assets/icons/img-empty.svg';
// JSX에서 일반 컴포넌트처럼 사용
<ImgEmpty width={122} height={122} />
Next.js환경(웹 브라우저)에서는 정상적으로 렌더링되던 컴포넌트였다. 하지만 이번 프로젝트는 cdd개발을 도입하여 storybook으로 모든 컴포넌트를 테스트 하는 방식을 택했기 때문에 stories.tsx파일을 따로 만들어 테스트들 돌리던 와중...!
실행하자마자 다음과 같은 런타임 에러를 뱉었다.
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at createFiberFromElement (/node_modules/.cache/storybook/.../client-DW84vZ63.js:2945:11)
at reconcileChildFibersImpl (/node_modules/.cache/storybook/.../client-DW84vZ63.js:3947:401)
아니 이게 무슨일이람? 어쨌든 에러 메세지가 말하길 "React 컴포넌트나 내장 태그(String)이 오길 원했는데, Object가 들어와서 오류가 발생한 것이었다.
1. 원인 분석
원인을 파악해보니 Next.js의 빌드 환경(Webpack/Turbopack)과 스토리북의 빌드 환경(vite)간의 설정이 차이가 있었고, 프레임워크 플러그인 간의 간섭이 얽혀있는 것 같았다.
이에 대해 자세하게 설명하면,
해당 프로젝트는 Next.js가 메인 프레임워크이면서, Storybook과 Vitest의 빌더로는 Vite를 사용하는 하이브리드 환경이었다.
따라서 SVGR을 적용할때 Next.js(webpack/turbopack)과 Storybook/Vitest(Vite) 양쪽 모두에 독립적인 설정을 맞춰줘야 했다.
주요 원인을 파악했고
해결하기 위해 시도한 방법을 아래에 정리했다.
2. Try
기존 코드는 다음과 같다.
// 기존 오류가 있던 코드
const config: StorybookConfig = {
"stories": [...],
"addons": [...],
"framework": "@storybook/nextjs-vite" // 단순 문자열로 지정됨
};
우선 vite-plugin-svgr을 최우선으로 install 했다.
npm install -D vite-plugin-svgr
다음으로는 @storybook/nextjs-vite가 SVG파일을 가로채지 못하도록 excludeFiles설정을 적용하여 이미지 플러그인 대상에서 제외시켰다. 아래의 코드처럼 따로 저런 설정을 하지 않으면 기본 이미지 로더가 SVG를 미리 가로채기 때문에 SVGR 플러그인이 적용되지 않는다!
(Vite 빌더가 미리 가로채서 이것저것 하는것을 미리 하는 것을 방지)
framework: {
name: '@storybook/nextjs-vite',
options: {
image: {
excludeFiles: ['**/*.svg'], // SVG 파일 처리 제외
},
},
}
마지막으로는 viteFinal 훅을 추가하여
storybook의 빌더인 vite에 vite-plugin-svgr 플러그인을 넣었고, 스토리북 실행 시 모든 SVG파일을 React컴포넌트로 변환하도록 설정했다.
async viteFinal(config) { // 변경점 ②: Vite 빌드 환경설정 확장
config.plugins = [
svgr({ include: '**/*.svg' }), // SVGR 플러그인을 Vite 플러그인 목록에 추가
...(config.plugins ?? []),
];
return config;
},
};
최종 완성된 .storybook/main.ts의 모습은 다음과 같다.
import type { StorybookConfig } from '@storybook/nextjs-vite';
import svgr from 'vite-plugin-svgr';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@chromatic-com/storybook',
'@storybook/addon-vitest',
'@storybook/addon-a11y',
'@storybook/addon-docs',
'@storybook/addon-mcp',
],
framework: {
name: '@storybook/nextjs-vite',
options: {
image: {
excludeFiles: ['**/*.svg'], // Next.js 이미지 모킹 간섭 방지
},
},
},
async viteFinal(config) {
// svgr을 최우선 순위로 설정하여 기본 에셋 로더보다 먼저 가로채도록 함
config.plugins = [
svgr({
exportAsDefault: true,
include: /\.svg/i, // 쿼리 스트링(?import) 대응용 정규표현식
}),
...(config.plugins ?? []),
];
return config;
},
};
export default config;
3. 오류 정리
위에서 svgr설정을 하지 않았기 때문에 결국 import Left 이런식으로 svgr을 쓰려 했을때 Left변수에 컴포넌트 함수가 아니라, 자바스크립트 객체가 할당이 되었고, 그 상태에서 쓰려고 했기 때문에 React가 뚱딴지 같은 객체를 보고 오류를 발생시킨 것이었다.
4. 마치며 배운점
마치며 배운점은 cdd개발에서의 storybook과 해당 프로젝트의 빌더의 특성을 파악하는 것이 굉장히 중요하단걸 느꼈다. Webpack기반의 Next.js의 설정 방식을 빡빡하게 이해하는 것이 중요하고 Vite 기반의 스토리북 설정을 혼용할 때 파일 확장자에 따른 변환 룰이 충돌하지 않는지, 아니면 빼먹은게 없는지에 관한 지식이 필요하다고 느꼈다.
솔직히 어려운 문제였고 이곳에서 어려움을 겪어서 몇시간동안 어제 팀원들과 많은 시간을 소모했던 것 같다. 라이브러리나 프레임워크를 쓸 때, 좀 더 디테일한 오류를 개선하는 실력을 기르고 싶다는 생각이 들었다.
