[Nextjs+Storybook] ✈️ 적용기

Pyotato·2024년 4월 17일
1

🍀 들어가기에 앞서..

💖 해당 글은 nextjs(14.1.0) + storybook(^8.0.8) 버전을 바탕으로 한 프로젝트입니다.
😄 학습과 병행하여 작성한 내용이므로 오류가 있을 수 있습니다. (댓글 피드백 환영합니다!)
🔗 해당 프로젝트의 리포지터리 : 깃헙
🗣️ 내용 전달의 간결함을 위해 아래의 내용부터는 비격식체를 사용하도록 하겠습니다.

📑 공식 문서

간단하게 말해서 스토리북이란, UI 컴포넌트와 페이지를 독립적으로 생성하여, 직접 접근하기 어려운 state나 엣지 케이스를 애플리케이션 전체를 살피지 않고도 쉽게 개발하고 공유하도록 해주는 도구다.
핵심 개념은 아래와 같다.

  • Stories: UI 컴포넌트의 랜더링된 상태를 스냅샷처럼 캡쳐한 것.
    각 컴포넌트는 여러개의 스토리가 존재할 수 잇고, 각 스토리는 다른 컴포넌트 state를 나타낸다.

    위의 게시물 카드 컴포넌트에서 Default, No Image 각각 stories에 해당된다.
  • Docs: 스토리북은 stories와 함께 컴포넌트를 분석할 수 있는 문서를 자동적으로 생성할 수 있도록 한다. Docs 문서생성 자동화는 UI 라이브러리 가이드라인, 디자인 시스템 사이트 등을 생성하기 용이하게 한다.
  • Testing: UI 테스팅의 실용적인 진입점이다. UI 개발을 하면서 자연스럽게 스토리를 작성하기 때문에 UI버그를 방지할 수 있는 저비용 테스팅 방법이다.
  • Sharing: 스토리북을 출간하여 동료들과 함께 공유할 수 있으며, notion과 figma 등에 embed가능하다.

기본 설치 및 세팅

$ npx storybook@latest init

설치를 하면 터미널에 아래와 같이http://localhost:6006/에 접속하면

아래와 같이 예시 스토리가 생성된다.

그리고 프로젝트 최상단에 .storybook 폴더가 생성된다.

이 파일들은 스토리북을 위한 설정을 담은 부분이다.

// main.ts

import type { StorybookConfig } from '@storybook/nextjs';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-onboarding',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@chromatic-com/storybook',
    '@storybook/addon-interactions',
    '@storybook/addon-styling-webpack',
    'storybook-dark-mode',
    '@storybook/addon-styling',
    'storybook-addon-mantine',
    '@tomfreudenberg/next-auth-mock/storybook',
  ],
  framework: {
    name: '@storybook/nextjs',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
  staticDirs: ['../public'],
};
export default config;
  • 중요한 부분은 addons에 라이브러리 설정을 추가해줘야한다.
    - 해당 프로젝트에서는 styled-components, nextAuth, mantine 라이브러리를 사용하므로 추가해준다.
    • npm i 도 해줘야 한다.
      $ npm i -D storybook-addon-mantine
    • mantine addon 문서에서 자세한 설정을 해줄 수 있다.
    • 해당 프로젝트는 mantine theme은 사용하지 않고 단순히 컴포넌트만 썼기 때문에 간단한 설정만 해줬다.
  • docs 에 해당 부분이 docs 자동생성 여부를 나타내는 설정이다.
// preview.ts
import '@mantine/core/styles.css';
import '@mantine/tiptap/styles.css';
import 'reactflow/dist/style.css';

import type { Preview } from '@storybook/react';

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    nextjs: {
      appDirectory: true,
    },
  },
};

export default preview;
  • 해당 프로젝트는 app directory를 사용하기 때문에 해당 설정에 true 값을 지정한다.
  • 스타일이 적용되기 위해 맨 위에 각 스타일을 import 해준다.
// preview.tsx
import { MantineProvider } from '@mantine/core';

import { withThemeFromJSXProvider } from '@storybook/addon-themes';

import React from 'react';
import { GlobalStyle } from '../src/styles/globalStyle';

import { ModalsProvider } from '@mantine/modals';
import '@mantine/tiptap/styles.css';
import 'reactflow/dist/style.css';
import { ThemeProvider } from 'styled-components';
import SessionWrapper from '../src/providers/SessionProvider';
import StyledComponentsProvider from '../src/providers/StyledComponentsProvider';
import { theme as colortheme } from '../src/styles/theme';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
};

const GlobalStyles = GlobalStyle;

export const decorators = [
  (renderStory: any) => (
    <ThemeProvider theme={colortheme}>
      <MantineProvider>
        <SessionWrapper>
          <ModalsProvider>
            <StyledComponentsProvider>{renderStory()}</StyledComponentsProvider>
          </ModalsProvider>
        </SessionWrapper>
      </MantineProvider>
    </ThemeProvider>
  ),
  withThemeFromJSXProvider({
    Provider: ThemeProvider,
    GlobalStyles,
  }),
];

첫 스토리 만들기

// app/stories/ItemCard.stores.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import ItemCard from '@/components/shared/ItemCard';

import { POST } from '@/constants';
import { omit } from '@/utils/shared';

const queryClient = new QueryClient();

export const decorators = [
  (story: any) => (
    <QueryClientProvider client={queryClient}>{story()}</QueryClientProvider>
  ),
];

export default {
  title: '홈/게시물 카드',
  component: ItemCard,
  args: { article: POST },
  argTypes: {
    product: 'object',
    description: '로드맵 정보',
  },
  layout: 'fullscreen',
  tags: ['autodocs'],
  decorators,
};

export const Default = {};
export const NoImage = {
  args: { article: { ...omit(POST, 'thumbnailUrl'), thumbnailUrl: null } },
};

  • 해당 프로젝트는 tanstackquery로 데이터 fetching을 하고 있었기 때문에, QueryClientProvider로 감싸줘야 에러가 발생하지 않는다.
export default {
  title: '홈/게시물 카드',
  component: ItemCard,
  args: { article: POST },
  argTypes: {
    product: 'object',
    description: '로드맵 정보',
  },
  layout: 'fullscreen',
  tags: ['autodocs'],
  decorators,
};
  • component에는 렌더링하고자하는 컴포넌트를, title은 스토리북 상에서 폴더와 파일이 되며, args 에는 argTypes에 들어갈 object을 지정한다.
  • Default, NoImage 등으로 컴포넌트의 state를 조작하여 랜더링된 모습을 확인할 수 있다.

  • default 포스트

  • 이미지가 없는 포스트

  • Docs

✨ 이번에는 간단하게 설치와 세팅, 스토리 생성을 해봤습니다.
🤔 스토리 폴더를 컴포넌트 폴더 내에 위치하고 싶은데 현재 프로젝트의 구조는 정돈이 안된 상태라 프로젝트 구조를 정리해야겠다는 생각이 들었습니다.
🙋 다음 포스팅은 프로젝트 구조 변경(아키텍쳐 적용) 및 router mock + auth 스토리북 생성으로 돌아오겠습니다!

profile
https://pyotato-dev.tistory.com/ 로 이사중 🚚💨🚛💨🚚💨

0개의 댓글

관련 채용 정보