티스토리 뷰

스토리북으로 UI 컴포넌트들을 정리하려고 하는데 어떤식으로 컴포넌트들을 분리해야하고 관리해야하는지에 궁금해서 정리하게 되었습니다. 본인이 알고 있는 좋은 구조가 있다면 댓글로 알려주시면 감사하겠습니다.

 

서론

나는 스토리북을 3년간 사용해왔다. 잘 구조화 하지 않으면 스토리북이 별로 유용하지 않다고 느껴 효율적인 구조를 찾아왔다. 내가 생각하는 스토리북의 best practice를 공유하겠다.

 

이 스토리북을 누가 쓸건가?

스토리북을 팀내에서만 쓸건지, 디자인 시스템 구축용으로 사용하여 외부에서도 같이 사용할건지에 따라서 프로젝트 구성이 달라진다. 외부에서도 사용해야 하는 경우 컴포넌트의 API를 상세히 작성해야 한다.

 

https://github.com/intuit/storybook-addon-sketch

 

이 스토리북 애드온을 사용하게되면 스토리북에 있는 리액트 컴포넌트를 스케치 파일로 변환할 수 있다. 프론트엔드 개발자와 UI디자이너가 같은 리소스를 공유하게 만드는것이다.

 

그외 기획자나, 프로덕트 오너들에겐 스토리북이 에셋의 데모 역할을 하게 된다. 현재 어떤 UI들이 우리 서비스에 있는지를 한곳에서 확인할 수 있다. 누가 스토리북을 읽게 될건지에 따라서 들어가야 하는 정보가 달라지게 된다.

 

 

가장 간단한 방법으로 스토리를 작성하라.

스토리북에 들어있는 코드는 명료하고 깔끔해야한다. 컴포넌트를 사용하는데 조금의 생각도 하지 않게끔 쉽게 만들어라. 스토리북에서만큼은 추상화를 피하고 반복을 늘려서 최대한 직관적으로 만드는것에 초점을 맞춰야한다. 예를들면 다음과 같다.

// ❌ Bad
export const badStory = () => {
  const actors = [{
    name: 'Jonathan Groff',
    role: 'Holden Ford',
    isDetective: true,
  }, {
    name: 'Holt McCallany',
    role: 'Bill Tench',
    isDetective: true,
  }, {
    name: 'Cameron Britton',
    role: 'Edmund Kemper',
    isDetective: false,
  }]

  return (
    <ActorList length={actors.length}>
      {actors.map(actor => (
        <ActorListItem key={actor.name} {...actor}/>
      ))}
    </ActorList>
  )
}

이렇게 데이터를 배열에 넣어서 반복문으로 렌더링 하지말고 반복을 하더라도 아래처럼 심플하게 렌더링해라.

// ✅ Good
export const goodStory = () => (
  <ActorList length={3}>
    <ActorListItem name="Jonathan Groff" role="Holden Ford" isDetective />
    <ActorListItem name="Holt McCallany" role="Bill Tench" isDetective />
    <ActorListItem name="Cameron Britton" role="Edmund Kemper" />
  </ActorList>
)

badStory는 우리가 보여주고자 하는 컴포넌트와 관계없는 로직을 가지고 있다. 오히려 반복을 하더라도 아래처럼 직관적으로 코드를 작성하는것이 좋다. 우리가 실제 서비스를 개발할때는 이러한 로직이 들어가는게 좋지만, 많은 사람들이 공유해서 봐야하는 이러한 Document용 컴포넌트는 최대한 심플하게 작성할수록 좋다.

 

스토리북이 5.2버전으로 업데이트 되면서 스토리북API를 최소화 하게끔 변경되었다. 이것은 스토리북의 .stories파일이 마치 그냥 하나의 리액트 컴포넌트인것처럼 보이게 하기 위해서 의도한것이다. 

 

knobs를 최소화하고 스토리를 늘려라.

스토리북의 플러그인중 knobs가 있다. 이것은 컴포넌트가 props에 따라 어떻게 달라질 수 있는지를 볼 수 있는 플러그인인데, 이 knobs는 최소화하고 오히려 props에 따라 달라지는 상태 하나하나를 스토리로 정의하는것이 좋다. 하나의 스토리에서 사용하는 props의 개수를 적게 만드는것이 중요하다. 그렇지 않으면 특정 상황에서의 컴포넌트가 어떤 모습인지 보기 위해 많은 수의 props를 조작해야 하기 때문이다. 그리고 props의 조합수가 많아지면 발견되지 않는 엣지 케이스의 컴포넌트가 발생할수있고 그로인해, 똑같은 컴포넌트를 중복해서 스토리로 만드는 불상사가 벌어질 수도 있다.

 

차라리 해당 컴포넌트가 가진 특징을 많은 스토리로 만들어라. 모든 경우의수가 왼쪽 메뉴에 나타나있으면 해당 컴포넌트를 놓치는일은 생기지 않을것이다. 예를들어, 버튼 컴포넌트가 theme와 size라는 props를 가진다고 해보자. 이때 이 버튼에 대한 스토리 하나를 정의해서 knobs로 props를 조작하는게 아니라 두개의 스토리를 만들라는것이다. 그럼 이 문서를 읽는 사람은 Button이 두 props에 따라 어떻게 변하는지 빠르게 확인할 수 있다. 

 

props의 조합에 따라 컴포넌트가 어떻게 바뀌는지도 문서화 하고 싶을 수 있다.

// ❌ Bad
stories.add('default', () => {
  const themes = ['default', 'primary', 'success', 'danger', 'warning'];
  const sizes = ['sm', 'md', 'lg'];

  return (
    <Button
      theme={select('theme', themes)}
      size={select('size', sizes)}
    >
      Button
    </Button>
  );
});

테마와 사이즈에 따라 컴포넌트가 어떻게 바뀌는지를 default 스토리에서 knobs를 통해 조작하는 예제인데, 이렇게 하면 좋지 않다. 아래처럼 해야 한다.

// ✅ Good
const themes = ['default', 'primary', 'success', 'danger', 'warning'];
const sizes = ['sm', 'md', 'lg'];

stories.add('default', () => {
  return (
    <Button>default button</Button>
  );
});

stories.add('theme', () => {
  const theme = select('theme', themes);

  return (
    <Button theme={theme}>{theme} button</Button>
  );
});

stories.add('size', () => {
  const size = select('size', sizes);

  return (
    <Button size={size}>{size} button</Button>
  );
});

stories.add('playground', () => {
  const theme = select('theme', themes);
  const size = select('size', sizes);
  const children = text('children', 'hello world !')


  return (
    <Button theme={theme} size={size}>{children}</Button>
  );
});

위에서는 default라는 스토리 하나에서 테마와 사이즈를 변경할수 있었지만 위 예제에서는 default, theme, size, playground 총 4개의 스토리를 생성했다.

 

1. 버튼의 기본 형태는 이렇고,

2. 테마를 변경하면 이렇고,

3. 사이즈를 변경하면 이렇고,

4. 테마와 사이즈를 동시에 변경하면 이렇다.(playground)

 

라는식으로 문서화를 해야 한다는것이다.

 

너무 많은 스토리가 만들어지는게 아닌가라고 생각할수도 있지만 컴포넌트가 많아졌을떄 스토리북을 더 빠르게 사용하기 위해서는 오히려 이렇게 중복을 많이 만드는게 더 효율적이다. knobs를 사용하지 말라는게 아니라 너무 의존하지 말라는 얘기다. 이렇게 중복으로 작성된 스토리들 덕분에 내가 보여주고자 하는 컴포넌트의 모든 모습을 쉽고 효율적으로 문서화 할 수 있다.

 

프론트엔드 개발을 하다보면 작은 크기의 컴포넌트들을 모아서 하나의 큰 컴포넌트를 완성시킨다. 그것과 똑같은 방식으로 스토리북을 구조화하면 된다.

 

스토리북 프로젝트를 다음과 같이 구성하면,

/src
  | /components
    | <Button>
    | /form
      | <Input>
      | <Checkbox>

  | /container
    | <SignUpForm>

  | /view
    | <SignUpPage>

가장 작은 단위의 컴포넌트들을 components폴더에 넣고, component들을 조합하여 container, container를 조합하여 view를 만든다. 그럼 스토리들은 다음 타이틀을 갖는다.

Components|Button
Components|Form/Input
Components|Form/Checkbox
Container|SignUpForm
View|SignUpPage

이 타이틀만보고도 이 스토리가 스토리북에서 대충 어느정도에 위치할것이라는 감이 생기고 덕분에 스토리 탐색 시간을 줄여줄 수 있다.

 

DocsPage를 통해 문서화하기

스토리북에서 새롭게 업데이트된 기능 중 하나이다. 

컴포넌트의 기본값과 예상되는 props 타입들을 문서화 할 수 있다.

import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Badge } from './Badge';

<Meta title="Demo/Badge" component={Badge} />

# Badge

With `MDX` we write longform markdown documentation for our `Badge` component and embed Doc Blocks inline.

<Props of={Badge} />

<Story name="positive">
  <Badge status="positive">Positive</Badge>
</Story>

이렇게 마크다운 문법으로 docs 페이지를 커스텀할 수 있다. 하지만 소규모팀 혹은 개인이 사용하는데 굳이 이정도의 문서화를 제공할 필요는 없는것같다. 이런게 있다 정도는 알아두고 나중에 필요할때 사용하면 된다.

 

스토리북을 컴포넌트 창고처럼 사용하라.

 

각 컴포넌트에 default 스토리를 만들어라.

컴포넌트 기반 라이브러리에서는 필수 props와 옵션 props 라는 개념이 있다. 각 컴포넌트의 필수 props가 채워진 상태를 default 스토리로 정의하라. 그리고 나서 옵션 props에 따라 다른 형태를 또하나의 스토리로 만들면 된다. 필수 props는 가장 간단한 형태의 값으로 채워져야한다.

 

또한, default 스토리는 다른 스토리들과의 비교를 쉽게 만들어준다.

 

action addon을 사용하라.

컴포넌트로 렌더링에 영향을 미치는 props를 내려줄 수도 있지만, 이벤트 핸들러를 내려줄수도 있다. 참고

export const hello = () => {
  // knobs 만들기
  const big = boolean('big', false);
  const name = text('name', 'Storybook');
  return (
    <Hello
      name={name}
      big={big}
      onHello={action('onHello')}
      onBye={action('onBye')}
    />
  );
};
hello.story = {
  name: 'Default'
};

스토리파일에서 Hello 컴포넌트를 렌더링하고 있다. Hello 컴포넌트는 이렇게 생겼다.

const Hello = ({ name, big, onHello, onBye }) => {
  return (
    <div>
      {big ? <h1>안녕하세요, {name}!</h1> : <p>안녕하세요, {name}!</p>}
      <div>
        <button onClick={onHello}>Hello</button>
        <button onClick={onBye}>Bye</button>
      </div>
    </div>
  );
};

 스토리에서 내려준 action('onHello')와 action('onBye')이벤트 핸들러가 Hello 컴포넌트로 내려가고, 버튼을 눌렀을때 해당 핸들러에 어떤 인자들이 전달되는지를 확인할 수 있다.

 

종류에 상관없이 스토리를 만들어라.

스토리북은 dumb component나 presentational component라고 불리우는 단순히 UI를 그리기 위한 컴포넌트에만 해당되는걸로 생각하는데, 사실은 그렇지 않다. redux의 state와 연관되어 있는 컨테이너 컴포넌트에 대해서도 스토리를 작성할 수 있다. 물론 decorators를 커스텀해야해서 세팅이 어렵긴 하지만 모든 컴포넌트는 스토리가 되어야 한다.

 

컨테이너 컴포넌트에 대한 스토리는 비즈니스 로직의 가장 간단한 형태에 대해 문서화를 할 수 있기 떄문에 큰 장점이 있다. 해당 컨테이너 컴포넌트와 연관된 로직만 집중해서 확인할 수 있다.

 

결론

프로젝트와 팀이 커짐에따라 스토리북에 많은 양의 컴포넌트가 들어가게 될것이다. 그것들중 어떤건 매일매일 사용하지만, 거의 사용되지 않는것도 있을수있다. 그러한 컴포넌트에 대해서도 스토리북을 확인하면서 리마인드 할 필요가 있다.

 

 

 

출처

https://dev.to/loicgoyet/how-i-manage-to-make-my-storybook-project-the-most-efficient-possible-2d8o

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함