스토리북을 쓰면서 nextjs mock module을 간단하게 구현했던 부분을 공유하려고 합니다. 이번 글은 nextjs에 관련된 내용이지만, 조금만 응용하면 storybook에서 발생하는 대부분의 외부 모듈 의존성 에러를 처리할 수 있지 않을까 생각합니다.
문제 발생
Storybook으로 컴포넌트의 UI를 테스트하다 보면, 외부모듈이 적절히 주입되지 않아서 에러가 발생할 때가 있습니다. 예를 들어 next/router
, next/link
, next/image
를 사용하면 storybook에서는 다음과 같은 에러가 발생합니다.
// next/router: Uncaught TypeError: Cannot read property 'pathname' of null
const isRoot = router.pathname === '/';
// next/link: Uncaught TypeError: Cannot read property 'prefetch' of null
<Link href="/signup">Sign Up</Link>
// next/image: http://localhost:6006/_next/image?url={src}&w=640&q=75 (404 Not Found)
<Image src={src} alt="logo" width={250} height={50} />
문제의 원인
nextjs
는 redux, i18n, react-router처럼 개발자가 <Provider />
를 명시적으로 주입하지 않기 때문에, 적절한 mock module
을 만들어줘야 테스트 러너가 코드를 실행할 수 있습니다.
nextjs
는 zero-configuration framework를 목표로 하다보니, 렌더링 하는 부분에서 nextjs Provider 주입을 자동으로 처리해주기 때문입니다.
// next/next-server/server/render.tsx
const AppContainer = ({ children }: any) => (
<RouterContext.Provider value={router}>
<AmpStateContext.Provider value={ampState}>
<HeadManagerContext.Provider value={headValue}>
<LoadableContext.Provider value={(moduleName) => reactLoadableModules.push(moduleName)}>
{children}
</LoadableContext.Provider>
</HeadManagerContext.Provider>
</AmpStateContext.Provider>
</RouterContext.Provider>
)
반면 storybook
에서는 (당연히) 그런 nextjs의 Provider를 처리해주지 않죠.
// storybook/app/react/src/client/preview/render.tsx
const render = (node: ReactElement, el: Element) =>
new Promise((resolve) => {
ReactDOM.render(node, el, resolve);
});
해결방법
Storybook의 decorator를 활용해 Provider를 주입 해주는 방식도 있지만, 이번에는 조금 더 간단하게 webpack의 module.alias api 를 이용해 mock module을 만들어 해결해보겠습니다.
// __mocks__/next/router.js
export const useRouter = () => ({
route: '/',
pathname: '',
query: '',
asPath: '',
prefetch: () => {},
push: () => {},
});
export default { useRouter };
// __mocks__/next/link.js
import React from 'react';
export default function ({ children }) {
return <a>{children}</a>;
}
// __mocks__/next/image.js
import React from 'react';
export default function (props) {
return <img {...props} />
}
실제로 nextjs의 모듈 코드가 리턴하는 값과 유사한 type을 리턴해주시면 됩니다.
폴더와 파일이름은 편한대로 설정해도 상관 없습니다. 저는 테스트에 필요한 mock code를 모아놓는 __mocks__
폴더에 next 모듈과 동일한 방식을 차용했습니다.
각각의 mock module을 만들어준 뒤에 storybook webpack 설정에 적용해주시면 됩니다. 컴포넌트를 렌더링 할 때, import 하는 모듈의 경로를 위에서 만든 mock module로 바꿔줍니다.
// .storybook/main.js
module.exports = {
// ...your config
webpackFinal: (config) => {
config.resolve.alias['next/router'] = require.resolve('../__mocks__/next/router.js');
config.resolve.alias['next/link'] = require.resolve('../__mocks__/next/link.js');
config.resolve.alias['next/image'] = require.resolve('../__mocks__/next/image.js');
return config;
},
};
이제 storybook을 다시 시작하면 위에서 발생했던 에러는 다시 나타나지 않습니다. 해당 모듈이 제공하는 기능은 동작하지 않겠지만, storybook의 UI 테스팅에는 필요한 기능이 아니기 때문에 테스트에는 영향이 없을 것 입니다.
필요하다면 로깅같은 동작을 덧붙이거나, 다른 모듈 에러가 발생했을 때도 응용할 수 있다고 생각합니다.
출처
https://stackoverflow.com/questions/63536822/how-to-mock-modules-in-storybooks-stories
https://github.com/vercel/next.js
https://github.com/storybookjs/storybook
Top comments (2)
좋은 내용 잘 읽었습니다.
읽어주셔서 감사합니다^^