DEV Community

Dylan
Dylan

Posted on

Storybook에서 nextjs 에러가 발생할 때

스토리북을 쓰면서 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} /> 
Enter fullscreen mode Exit fullscreen mode

문제의 원인

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>
)
Enter fullscreen mode Exit fullscreen mode

반면 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);
  });
Enter fullscreen mode Exit fullscreen mode

해결방법

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} />
}

Enter fullscreen mode Exit fullscreen mode

실제로 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;
  },
};

Enter fullscreen mode Exit fullscreen mode

이제 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)

Collapse
 
dance2die profile image
Sung M. Kim

좋은 내용 잘 읽었습니다.

Collapse
 
dylanju profile image
Dylan

읽어주셔서 감사합니다^^