Storybook Preview의 decorators로 Provider component 적용하기

silver·2024년 9월 22일

React

목록 보기
3/3
post-thumbnail

Storybook을 작성하던 중 recoil,react-router-dom,tanstack-query와 같이 provider component로 감싸야하는 컴포넌트의 스토리를 작성하게 됐다.

처음엔 각 스토리의 decorators에서 필요한 프로바이더 컴포넌트로 Story를 감싸도록 설정했다. 하지만 작성하다보니 이러한 작업이 중복되는 컴포넌트들이 존재했다. 그래서 모든 Story에 대해 decorators를 적용할 방법을 찾아봤고, Storybook 폴더의 Preview파일을 통해 설정할 수 있다는 것을 알게 됐다.

Preview.ts를 Preview.tsx로 변경


우선 Preview 파일에서 decorators에서 tsx 문법을 사용하려면 Preview.ts 파일을 Preview.tsx로 파일명을 변경해야 한다.

이 과정에서 위와 같은 오류가 발생했는데, Storybook이 preview파일의 확장자가 tsx로 설정되면 읽지 못하는 건가? 하고 Storybook의 빌드옵션을 변경해보았지만 해결되지 않았다. 알고보니 파일 확장자를 변경한 다음 storybook 개발 서버를 다시 실행하지 않아서 생긴 문제였다.

Webpack이 처음 Storybook 개발 서버를 실행할 당시의 파일명을 읽어서 해당 파일들의 변경사항을 감지해 업데이트한다.
하지만 파일명이 변경되면 개발 서버를 실행할 때와 파일명이 달라지기 때문에 서버를 새로 실행해야하는 것이다.

decorators 설정

decorators필드는 리액트 컴포넌트를 반환하는 콜백함수를 입력받는다. 콜백함수의 파라미터로는 decorators를 통해 감싸고자하는 Story컴포넌트와 args,argTypes,globals,parameters등의 context를 입력받는다.

Preview 파일에선 모든 story에 대한 decorators를 설정할 것이기 때문에 별도의 context는 입력하지 않고 개별 Story에서 입력해준다.

const preview : Preview = {
  ...
decorators : [
  	(Story)=> {
      return (
        <Component>
          <Story/>
        </Component>
      )
	},
  ]
}

하나의 콜백함수를 입력할 수도 있지만 이처럼 배열형태로 입력할 수 있는데, 배열에서 뒤에 있는 요소가 앞에 있는 요소를 감싸는 형태로 이루어진다. 예를들어,


decorators : [
  	(Story)=> {
      return (
        <section>
          <Story/>
        </section>
      )
	},
  (Story)=> {
	  return (
        <main>
          <Story/>
        </main>
      )
  },
  ]

이와 같이 작성하면

<main>
  <section>
    <Story/>
  <section/>
<main/>

이와 같이 main 태그 안에 section 태그가 있고 그 안에 Story 컴포넌트가 감싸지는 형태로 렌더링 된다.

내가 사용한 기술 스택에서 필요로 하는 provider 컴포넌트를 아래와 같이 작성했다.

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
  decorators: [
    (Story) => {
      return (
        <QueryClientProvider client={queryClient}>
          <MemoryRouter initialEntries="{[/]}">
            <RecoilRoot>
              <ThemeProvider theme={defaultTheme}>
                <GlobalStyle />
                <Story />
              </ThemeProvider>
            </RecoilRoot>
          </MemoryRouter>
        </QueryClientProvider>
      );
    },
  ],
};

recoilState의 값이 필요한 경우

Preview.tsx 파일에서 decorators를 통해 reocilRoot를 설정했지만, 실제로 recoilState를 사용해서 렌더링하는 컴포넌트가 있어서 이에 대한 추가 설정이 필요했다.
이는 아래 코드처럼 RecoilRoot로 감쌀 때 initializeState를 통해 컴포넌트에 사용되는 recoilState에 직접 값을 할당할 수 있다.


decorators: [
    (Story) => {
      return (
        <RecoilRoot
          initializeState={({ set }) => {
            set(shoppingCartAtom, shoppingCartProducts);
          }}
        >
          <Container>
            <ShoppingCartContainer>
              <ShoppingCart></ShoppingCart>
              <Story />
            </ShoppingCartContainer>
          </Container>
        </RecoilRoot>
      );
    },
  ],
  

Tanstack-query 인스턴스를 사용하는 경우

Tanstack-query provider로 모든 스토리를 감싸주었지만, query 인스턴스를 통해 렌더링이 진행되는 컴포넌트의 경우 api호출이 발생하는데 실제 애플리케이션 환경과 다르기 때문에 정상적으로 데이터를 불러오지 못한다. 이를 해결하기 위해선 api 호출을 mocking해야하는데 이러한 작업을 도와주는 MSW라는 라이브러리를 사용해야 한다. 이는 E2E테스트를 진행할 때도 사용할 수 있을 거라 예상되서 다음 글은 MSW를 배워서 문제를 해결하고 이를 정리해서 글을 써보려 한다.

react-router-dom memoryRouter

react-router-dom의 useNavigate,link등과 같은 기능을 사용하는 Story를 작성할 땐 라우터 컨텍스트로 감싸져 있어야 한다. 하지만 기존에 RouterProvider는 router를 입력해서 url에 따라 페이지를 보여주는데, Storybook 환경에선 이러한 작업이 제한적이다.

이를 해결하기 위해 memoryRouter로 Story를 감싸줬다. memoryRouter는 브라우저 환경과 무관하게 메모리 내에서 경로를 관리하는 router이다. 주로 테스트와 같이 브라우저가 없거나 주소를 사용할 수 없는 경우에 사용할 수 있다.

1개의 댓글

comment-user-thumbnail
2024년 9월 23일

아 진짜 어렵네요ㅜㅜ 스토리북 recoil도 같이할 수 있군요

답글 달기