프로젝트/하루한냥

[하루한냥] 스토리북으로 UI 컴포넌트 문서화하기

알파카털파카 2024. 2. 1. 11:56
[하루한냥]
스토리북으로 UI 컴포넌트 문서화하기

 

 

얼마 전 스토리북이라는 기술을 원티드 프리온보딩 챌린지로 처음 접하게 되었다. 강의를 들었지만 정확히 무엇을 위한 기술인지 이해가 가지 않았고, 당시의 나에게는 굳이 필요하지 않은 기능이라 관심도 적었다. 그 후 하루한냥 프로젝트를 완성했고, 주변에서 스토리북을 사용한다는 얘기가 많이 들려왔다. 개발자가 아니어도, 프로젝트를 배포하지 않아도 UI를 쉽게 확인할 수 있다는 장점에 마침 내 프로젝트에도 적용해보면 좋겠다는 판단을 했다. 이번 포스트는 스토리북을 사용하며 느낀 스토리북의 장점과, 적용할 때 어려웠던 점을 적어보려 한다. 

 

 

📕 스토리북을 도입한 이유
  - 스토리북의 장점
🤔 스토리북을 적용할 때 겪은 어려움
  - GlobalStyle 적용하기
  - useArgs로 스토리북 함수 만들기
  - 캘린더 컴포넌트 개선하기(순수 컴포넌트)
  - 공통 함수 적용하기
🐈 하루한냥 스토리북
📖 참고 레퍼런스

 

 


 

 

스토리북을 도입한 이유

작년 6월, 매달 듣는 원티드 프리온보딩 챌린지에서 스토리북을 주제로 다뤘었다. 컴포넌트에 스토리, 즉 '이야기'를 붙인다고? 스토리란게 대체 뭘까? 의문이 생겼다. 당시에 나는 강의를 열심히 듣지는 않아서, 당장 스토리북을 사용해볼 필요성을 느끼지 못해서 실습도 대강 따라했었다. 그래도 그렇게 배워둔 지식이 이번에 도움이 됐다.

 

강의 후 하루한냥 프로젝트를 완성했다. 추가해볼까 싶은 기능이 여러가지 있었는데, 그중에 스토리북이 떠올랐다. 스토리북을 이용하면 UI를 문서화하고 테스트 해볼 수 있다. 매번 특정 상황을 만들어서 컴포넌트를 확인해야 하는 것이 번거로웠다. 스토리북은 이 문제를 해결해줄 수 있을 것 같았다. 

 

원티드 강의에서 들었던 내용 중 '개발자가 아닌 직군도 쉽게 컴포넌트 UI를 확인할 수 있다'는 장점이 떠올랐다. 개발자가 아니던 시절에 개발자와 소통할 때 개발용어를 알아듣지 못한 적이 많아 협업에 어려웠던 적이 있었다. 이제는 내가 개발자가 되어 다른 직무의 동료와 협업할 일이 생긴다. 스토리북은 그때 원활하게 소통을 도와줄 수 있을 것 같은 도구였다.

 

그래서, 프로젝트에 스토리북을 적용하기로 했다.

 

 

스토리북의 장점

스토리북을 사용하며 느낀 장점을 적어본다. 

 

1. 사용법이 간단하다.

 

한 번 사용법을 익히면 크게 어려운 부분은 없다. 아직 스토리북을 100%까지는 활용해보지 않아서 그럴 수도 있겠지만, 지금까지 사용해본 바로는 난이도가 높지 않았다. 어려운 점이 있어도 공식 문서가 잘 되어있었다.

 

2. UI를 바로 확인할 수 있다.

 

이전까지는 UI를 바로 확인하지 못해서 불편했다. 컴포넌트를 페이지에 직접 반영하면서 개발했어야 했는데, 스토리북을 통해 개발을 진행하면서 반영되는 여러가지 UI를 바로 확인할 수 있어 편리해졌다. UI가 잘 정돈된 문서에 담겨 공통 컴포넌트를 문서화할 수 있다. 덕분에 개발하는 입장에서도, 개발자가 아닌 사람이 보기에도 훨씬 수월하다.

 

3. 크로마틱을 이용하면 스토리북 관리가 편리하다.

 

매번 실행하기 번거로운 스토리북 빌드를 깃허브 액션으로 자동화했다. 지정한 경로의 파일이 변경되면 자동으로 크로마틱 빌드 및 배포 명령어가 실행된다. UI의 변경사항을 추적하고 리뷰가 가능하다는 점에서 팀원과 공유하거나 협업하기 좋은 도구일 것 같다. 크로마틱 내용은 이 글에서는 다루지 않는다. 

 

 

 

 

스토리북을 적용할 때 겪은 어려움

스토리북을 사용하는 난이도는 높지 않다고 했지만, 자잘한 트러블 슈팅이 몇 가지 있었다.

 

 

GlobalStyle 적용하기

TextField 컴포넌트에 스토리북을 추가했다. 프로젝트에서는 CSS에 이상이 전혀 없었는데, 스토리북으로 확인할 때는 TextField 컴포넌트의 width, height 크기가 제대로 적용되지 않았다. 정확히는 box-sizing이 content-box로 설정되어 있었기 때문에 발생한 문제였다. 프로젝트 전역에 적용한 GlobalStyle에서 box-sizing: border-box 스타일을 적용했지만, 스토리북에서는 GlobalStyle이 적용되지 않은 상태였다. 

 

스토리북에 GlobalStyle 적용하기 전과 후

 

공식문서의 decorators를 이용해 스토리를 래핑하는 방법을 참고했다. 스토리북을 설치할 때 생성된 preview.tsx 파일에 GlobalStyle을 넣어주었다. 이렇게하면 모든 스토리북의 .stories. 파일에 글로벌 스타일이 적용된다. box-sizing은 HTML/CSS의 자주 실수하는 문제로 많이 등장하는데, 스토리북에서도 이런 문제를 겪게될 줄은 몰랐다.

 

import type { Preview } from '@storybook/react';
import { Global } from '@emotion/react';
import { globalStyle } from '../src/ui/styles';

const withGlobalStyle = (Story: any) => ( // ✅
  <>
    <Global styles={globalStyle} />
    <Story />
  </>
);

const preview: Preview = {
  decorators: [withGlobalStyle], // ✅
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

 

 

useArgs로 스토리북 함수 만들기

TextField 컴포넌트에 값을 추가하려면 함수가 필요했다. 이 부분도 공식문서에 적혀있는 내용을 따랐다. 스토리북에서는 useState 대신 사용할 수 있는 방법이 있다.

 

사용자가 일으키는 이벤트에 응답하고, 상태를 변경하고, 변경 사항을 UI에 반영하려면 @storybook/preview-apiuseArgs를 이용하면 된다. 기존의 컴포넌트에서 만든 onChange 함수 대신에, useArgs를 활용해 스토리북에서 사용할 수 있는 함수를 만들었다. 스토리북에서 값을 업데이트하고, 변경된 UI를 보여줄 수 있게 되었다. 

 

import styled from '@emotion/styled';
import { Meta, StoryObj } from '@storybook/react';
import { TextField } from '@ui/components/common/TextField';
import { useArgs } from '@storybook/preview-api';
import { ChangeEvent } from 'react';

const meta: Meta<typeof TextField> = {
  title: 'Component/TextField',
  component: TextField,
};

export default meta;

type Story = StoryObj<typeof TextField>;

export const Default: Story = {
  render: function Render() {
    const [{ value }, updateArgs] = useArgs(); // ✅

    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
      updateArgs({ // ✅
        value: e.target.value,
      });
    };

    return (
      <Container>
        <TextField 
          type="input" 
          id="input" 
          name="input" 
          placeholder="Input" 
          value={value} // ✅
          onChange={handleChange} />
      </Container>
    );
  },
};

 

입력값에 따라 변경되는 UI

 

 

캘린더 컴포넌트 개선하기(순수 컴포넌트)

프로젝트의 핵심인 달력을 구성하는 컴포넌트를 스토리북에 추가하면서 문제를 인식하게 됐다. 기존에는 페이지 내 캘린더의 기능과 UI가 혼합된 형태로 나열되어 있었고, 컴포넌트의 원칙과 재사용성 관점을 고려하지 못했다. 캘린더는 다양한 기능을 수행하고 있지만, 순수 컴포넌트로 존재하지 못했기 때문에, 비즈니스 로직 등 공통 컴포넌트로써 활용하기 어려웠다. 이에 따라 순수 컴포넌트로 분리해야겠다는 필요성을 느꼈다.

 

문제를 해결하기 위해 페이지에서 달력 부분을 Calendar 컴포넌트로 분리했다. Calendar 컴포넌트는 Props를 통해 데이터와 기능을 전달받아 구성되도록 개선했다. 덕분에 스토리북으로 컴포넌트를 독립적으로 시각화하고, 테스트할 수 있게 됐다. 

 

// 리팩토링 후 CalendarPage.tsx 코드 
  return (
    <>
      {isFirstSign && (
        <Lottie loop animationData={congratsLottie} play style={{ position: 'absolute', top: '10%', left: '5%' }} />
      )}
      <CalendarHeader page="calendar" />
      <Body>
        <Calendar
          firstDayOfMonth={firstDayOfMonth}
          daysInMonth={daysInMonth}
          monthlyDiary={monthlyDiary}
          currentDate={currentDate}
          selectedYearAndMonthDate={targetDate}
          onDiaryPage={handleClickDiaryPage}
        />
      </Body>
      <Menu />
    </>
  );

 

캘린더 컴포넌트를 공통으로 활용하도록 만들어서 스토리북을 작성하기도 쉬워졌다.

 

// Calendar.stories.tsx

export const Default: Story = {
  render: () => (
    <Container>
      <Calendar
        firstDayOfMonth={1}
        daysInMonth={31}
        monthlyDiary={dummyDiary}
        currentDate={dummyCurrentDate}
        selectedYearAndMonthDate={dummyCurrentDate}
      />
    </Container>
  ),
};

export const Past: Story = {
  render: () => (
    <Container>
      <Calendar
        firstDayOfMonth={1}
        daysInMonth={31}
        monthlyDiary={dummyDiary}
        currentDate={dummyCurrentDate}
        selectedYearAndMonthDate={dummyPastDate}
      />
    </Container>
  ),
};

 

캘린더 외에도 이런 문제를 가진 컴포넌트가 두 세곳 더 있었다. 해당 컴포넌트도 캘린더처럼 공통 컴포넌트가 될 수 있도록 리팩토링했다. 스토리북을 활용하면서 리액트 컴포넌트 개발에 신경써야 할 부분을 되새기게 됐다. 기존의 컴포넌트에 있던 문제를 발견하고 개선할 수 있던 기회였다. 

 

 

공통 함수 적용하기

여러가지 상황에 따른 컴포넌트 UI를 보여주기 위해서는 로직이 겹치는 부분이 발생한다. 매번 동일한 코드를 반복하자니 비효율적이라, 컴포넌트 전체에 적용하는 방법이 있는지 찾아봤다. 이 부분은 Component args를 이용해서 해결했다. 

 

컴포넌트 레벨에서 args를 정의하면, 덮어쓰지 않는 한 모든 컴포넌트 요소 스토리에 적용된다. 중복 코드를 줄이고 간편하게 활용할 수 있었다.

 

const meta: Meta<typeof Calendar> = {
  title: 'Component/Calendar',
  component: Calendar,
  args: { // ✅
    onDiaryPage: (type: DateType, date: number) => {
      alert(`type: ${type}, date: ${date}`);
    },
  },
};

export default meta;

type Story = StoryObj<typeof Calendar>;

export const Default: Story = {
  render: (args) => (
    <Container>
      <Calendar
        firstDayOfMonth={1}
        daysInMonth={31}
        monthlyDiary={dummyDiary}
        currentDate={dummyCurrentDate}
        selectedYearAndMonthDate={dummyCurrentDate}
        onDiaryPage={args.onDiaryPage} // ✅
      />
    </Container>
  ),
};

export const Past: Story = {
  render: (args) => (
    <Container>
      <Calendar
        firstDayOfMonth={1}
        daysInMonth={31}
        monthlyDiary={dummyDiary}
        currentDate={dummyCurrentDate}
        selectedYearAndMonthDate={dummyPastDate}
        onDiaryPage={args.onDiaryPage} // ✅
      />
    </Container>
  ),
};

 

Calendar.stories 컴포넌트에서 날짜 클릭 시 alert창

 

 

 

 

내용 요약

1. 스토리북을 이용하면 UI를 개발하고, 테스트하고, 문서화할 때 유용하다.
2. 스토리북은 개발자 비개발자 모두에게 이해하기 쉽고 편리해서 협업하기 좋다.
3. 스토리북에 전역 스타일을 추가하고 싶은 경우 decorators를 활용하면 된다.
4. useArgs를 사용하면 스토리북에서 사용자의 입력을 반영하는 UI를 보여줄 수 있다. 
5. React 개발 시 순수 컴포넌트로 만들면 공통으로 이용할 수 있고 스토리북 활용에도 편리하다.
6. 반복되는 코드는 component-args로 중복을 줄일 수 있다.

 

 

 

 

하루한냥 스토리북

완성한 프로젝트 스토리북 링크는 다음과 같다. 피그마의 디자인 가이드와 얼라인시켰다. 상황에 따라 다른 UI를 테스트하고, 프로젝트 전반의 스타일을 쉽게 확인할 수 있다. 

 

 

@storybook/cli - Storybook

 

65ad0ebb79ef0ac5d280a2f7-lotaaylsbc.chromatic.com

 

 

 

 

참고 레퍼런스

https://storybook.js.org/docs/get-started/why-storybook

https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/

https://storybook.js.org/docs/configure/styling-and-css

https://storybook.js.org/docs/writing-stories/decorators

https://storybook.js.org/docs/writing-stories/args

https://www.npmjs.com/package/@storybook/preview-api

https://storybook.js.org/docs/writing-stories/args#component-args

https://storybook.js.org/docs/api/csf

 

 

 

 

마치며

블로그에 글을 쓰면서 학습한 내용과 어려웠던 부분이 깔끔하게 정리되었다. 문제 해결에 급급해 사용법만 훑고 지나갔는데 공식 문서를 보면서 학습하니 이해가 더 잘 됐다. 필요할 때 학습하는 내용이 가장 효율적이고 재미있다. 당장 필요하지 않더라도 인덱싱을 해두면 언젠간 쓸모가 있다. 스토리북도 마침 나에게 필요한 순간이 찾아왔고, 덕분에 알차게 즐겼다. 특히 순수 컴포넌트로 존재하지 못했던 컴포넌트를 리팩토링한 것이 가장 기억에 남는다. 리액트에서 개발을 바라보는 원칙을 다시 새기게 됐다.