React Server Component에 Storybook 세팅하기
서버 컴포넌트를 사용하면 Storybook을 적용하는 경우에 몇가지 추가 설정이 필요하게 되었다. Next.js App Router를 통해 사용하는 경우 기본으로 서버 컴포넌트를 지원하기에 Storybook에 추가 설정이 필요해질 수 있다.
Storybook v8 부터 서버 컴포넌트 지원을 시작했는데, 아직 정식 버전은 아니고 rc 버전이 존재하는 상태다. (확인한 날짜 '2024-03-03')
기본적인 서버 컴포넌트를 위한 Storybook config 변경과, 서버 컴포넌트에서 직접 DB에 요청하는 경우는 또 다른 설정이 필요하다. 하나씩 설정해보자.
Storybook v8 추가 및 config
현재 작성하는 기준으로 최신 버전은 v8.0.0-rc.1
이라 이 버전을 사용하자.
pnpx storybook@v8.0.0-rc.1 upgrade
로 적용한다. 혹은 pnpx storybook@next upgrade --prerelease
로 최신 버전을 적용해주자.
이후에 서버 컴포넌트 기능을 켜주면 기본 설정은 완료된다.
// .storybook/main.ts export default { features: { experimentalRSC: true, }, }; // .storybook/preview.ts const preview: Preview = { parameters: { ... nextjs: { appDirectory: true, }, }, };
이후 Storybook으로 서버 컴포넌트의 스토리를 추가 할 수 있게 되는데, 만약 서버 컴포넌트에서 직접 DB에 쿼리를 해서 가져온다던지 하는 경우 문제가 생긴다.
왜냐하면 현재 Storybook은 클라이언트를 대상으로 만들어져있기 때문에 서버 컴포넌트같이 서버에서 동작하지 않는다. (그래서 @storybook/server
같이 PHP와 같은 서버에서 UI를 그리는 경우도 지원하기 위해 개발중인 것 같다.)
만약 서버 컴포에서 직접 DB로 쿼리하는 경우 스토리가 보일 수 있게 고쳐보자.
DB 쿼리 모킹
우선 모킹을 위해 추가 라이브러리가 필요하다.
pnpm add -D storybook-addon-module-mock @storybook/jest
storybook-addon-module-mock
은 jest
의 jest.fn()
함수를 스토리에서 사용할 수 있게 해준다.
import { createMock } from "storybook-addon-module-mock"; import * as model from "~/app/activities/model"; ... export const Page: Story = { args: { searchParams: { date: "2024-03-01" } }, parameters: { moduleMock: { mock: () => { const mock = createMock(model, "getActivities"); const activities: model.Activity[] = [ { id: "1", userId: "1", name: "스토리북 설정하기", description: "추가 설정 정리", created_at: "2024-03-01", status: "idle", }, ]; mock.mockReturnValue(Promise.resolve(activities)); return [mock]; }, }, }, };
위의 코드는 getActivities
라는 DB에 쿼리해서 데이터를 가져오는 함수를 모킹하고 있다.
import * as model from "~/app/activities/model";
처럼 import 해와서 특정 함수를 모킹하고 리턴값을 제공하면 스토리에 사용할 컴포넌트가 해당 값을 사용할 수 있게 된다.
여기까지는 Storybook의 공식 블로그 내용이지만 실제 적용시에는 추가로 설정해야할게 있었다.
webpack 설정
프로젝트의 DB로 PostgreSQL을 사용하고 있었다. 쿼리하는 함수를 모킹하더라도 빌드하는 과정에 pool을 생성하면서 특정 모듈들 net
, tls
, dns
, pg-native
가 필요하다는 에러가 나오게 된다.
// .storybook/main.ts const config: StorybookConfig = { ... webpackFinal: async (config) => { config.resolve = { ...config.resolve, fallback: { ...(config.resolve || {}).fallback, // for pg module net: false, tls: false, dns: false, "pg-native": false, }, }; return config; }, };
Storybook은 서버로 동작하는 것이 아니고 스토리에서 실제 DB를 요청이 필요하지 않기 때문에 특정 모듈들을 fallback 설정해주는게 필요하다.
서버 컴포넌트와 Storybook
위의 스토리에서 쿼리를 하는 함수를 모킹하는 방식으로 진행하였다. 만약 함수가 분리가 되어있지 않다면 분리하는 것이 좋다.
데이터를 로드하고 가공하는 레이어를 분리하였다면 위와 같이 모킹을 할 때 쉽게 적용할 수 있다.
관련해서 서버 컴포넌트에서 데이터를 핸들하는 경우에 데이터 접근하는 계층을 나누어서 관리하는 것을 Next.js 공식 블로그에서도 추천하고 있다.
코드의 관리, 테스트, 보안적인 면에서도 이러한 레이어 분리는 적절한 계층 분리이기 때문에 시도해보자.
How to Think About Security in Next.js | Data Access Layer