Visual TDD (feat. Storybook TDD)

Visual TDD (feat. Storybook TDD)

Visual Test Driven Development with Chromatic

8 min read
StorybookTDD

개요

Visual TDD(시각적 TDD) 라고, 보여지는 UI 개념 단계에서 외관의 변경 사항을 포착하는 방법을 이야기 합니다. 해당 방법을 사용하기 위해서 Storybook 팀에서 만든 Chromatic이라는 도구가 존재합니다.

사실 개발단계에서 UI가 변경되는 일은 되게 잦고 당연하지 않나? 시각적 변경 사항을 찾아야 할 이유가 있나? 싶었는데요. 막상 써보니 무엇이 변경되었는지 포착하기 쉽고 특히 데이터를 도출해내는 UI 라면 더 유용하게 쓰일 것 같다는 생각이 듭니다. 개인적으로 변경사항이 포착되었을때 나오는 리뷰 과정이 확실히 재미는 있습니다.

다만 이 Visual TDD를 과연 일반적인 TDD 라고 볼 수 있을지.. 라고 개인적으로 의문이 들긴합니다. 따지고 보면 흔히 아는 TDD 라는 개념이 테스트 케이스와 검증 후 리팩토링을 반복하는 방법인데 시각적인 변화를 감지해야만 하는 에러 테스트 케이스를 만들 큰 이유나 방법이(Red 과정이) 저로서는 조금 모호해보입니다. 엄밀히 따지면 실수나 고의로 인한 변경사항을 확인하는 것에 더 큰 의의를 두는 것 처럼 보이거든요. 물론 방법론인 만큼 어떻게 쓰냐에 의미를 두는 것이고 TDD에 대한 경험이 부족한 탓일지도 모르겠습니다.


Story 만들기

프로젝트에 Storybook이 이미 적용 되어있다는 전제하에 시작합니다.

제 개인적으로 Visual TDD 가 확실하게 도움될 만한 경우가 무엇이 있을까 고민해보면 아마 다음과 같은 경우가 아닐까 생각해서 작성해본 코드들이에요.

먼저 Profile 컴포넌트를 다음과 같이 간단히 만들어 봅니다.

components/profile.tsx
import styled from '@emotion/styled';
import Button from './common/button';

export interface ProfileProps {
  following?: boolean;
  followers_cnt?: number;
}

export default function Profile({ following, followers_cnt = 0 }: ProfileProps) {
  return (
    <Card>
      <img src="..." />
      <p>팔로워: {followers_cnt}</p>
      {following ? <Button>구독중</Button> : <Button variant="primary">구독</Button>}
    </Card>
  );
}

const Card = styled.div`
  padding: 1rem;
  border-radius: 0.25rem;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  box-shadow: 0px 0px 4px 2px #00000026;
  > img {
    border-radius: 0.25rem;
  }
`;

Story는 다음과 같이요.

stories/Profile.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import Profile, { ProfileProps } from '@/components/profile';

const meta = {
  title: 'Profile',
  component: Profile,
  tags: ['autodocs'],
  parameters: {
    layout: 'centered',
    componentSubtitle: 'Base Profile Component',
  },
} satisfies Meta<ProfileProps>;
export default meta;

type Story = StoryObj<ProfileProps>;

export const UnFollowing: Story = {
  args: {
    followers_cnt: 1234,
  },
};

export const Following: Story = {
  args: {
    ...UnFollowing.args,
    following: true,
  },
};

Storybook 에서 확인해 본다면 다음과 같겠습니다.

1


chromatic

chromatic.com 에서 먼저 회원가입을 진행한 뒤에, GitHub에 올라가 있는 Repo 나 혹은 그렇지 않은 프로젝트를 상황에 맞게 선택하여 작업을 생성합니다.

2

이후에는 다음과 같은 명령어 들을 작성하라고 고지합니다.

이는 chromatic을 프로젝트내에 설치하며 방금 만든 chromatic 작업 공간을 프로젝트와 연결 시키는 작업이에요. token의 값은 초기 세팅시에 반드시 필요하며 CHROMATIC_PROJECT_TOKEN 환경변수 설정을 해두고 명령어 만으로 실행시키면 될 것 같습니다. token 값은 나중에 설정창에서 찾을 수 있으니 백업까지 해둘 필요는 없어 보입니다. 덤으로 다른 리뷰어, collaborator 를 초대하기 위한 초대 링크가 따로 존재하니 이를 공유할 필요도 없어보입니다.

3

npx chromatic 을 사용하고 나면 chromatic 에서 현재까지 작성된 스토리들을 기준으로 스냅샷을 캡처합니다. 이후에도 스냅샷을 캡처하면서 이를 비교합니다.


Diff, Review

이제 이후에는 변경사항을 확인하고 리뷰하는 일만 남았습니다.

components/profile.tsx
//...
import { digitK } from '@/utils/format';

export default function Profile({ following, followers_cnt = 0 }: ProfileProps) {
  return (
    <Card>
      <img src="..." />
      <p>팔로워: {digitK(followers_cnt)}</p>
      {following ? <Button>구독중</Button> : <Button variant="primary">구독</Button>}
    </Card>
  );
}
//...

간단한 함수를 추가했어요. 1000 자리수가 넘어가면 K 단위로 변경해서 표기하도록 기능을 추가했습니다. 이를 적용하고 다시 npx chromatic 을 작동시킵니다.

6

변경사항이 형광색으로 체크되었습니다.

단순히 보여지는 화면이 바뀐 것이지만 다음 세 가지 과정으로 생각해 볼 수 있는것 같아요.

  1. 어떤 기능이 추가되었는지.
  2. 기능이 추가되었다면 왜 추가되었고 어떤 화면으로 나타나는지
  3. 만약 숫자나 기능이 정상으로 작동하지 않을 경우에 대한 에러 확인된

1 단계는 Diff 를 확인 하는 단계에서, 2 단계는 다음 코드 리뷰 단계에서 확인 할 수 있습니다.

7

3 단계는 현재 스냅샷이 인증 된 이후 다음 스냅샷에 Diff 가 발견될 시 진행 되겠죠.

UI 디자인 단계에서는 Diff 가 당연히 발생하니 이런 검증 단계가 의미가 있나 싶은데요. Util 기능이나 데이터를 UI로 출력하는 디자인 비교에서는 확실히 의미를 찾을 수 있어보입니다.


마무리

만약 GitHub 과 연결하여 사용했다면 커밋 푸쉬때 변경사항이 있는지, 확인해야할 리뷰를 알림해줍니다. 8

Storybook으로 TDD를 한다는 이야기를 들어서 한번 적용해 보았는데 Jest나 cypress 같은 도구들과는 꽤나 다르긴 합니다. 직접 사용해볼지, 어떻게 사용해볼지는 고민이 필요해보입니다. chromatic 도 완전 무료는 아니고 크롬 브라우저 환경에서의 결과물 비교만 제공하니 다른 브라우저 환경을 위해선 비용이 필요하기도 합니다. (머리를 잘썼...)

확실히 재미는 있습니다.

틀린 내용이 있다면 지적해 주시고,
더 좋은 방법이나 생각을 공유해주세요.

banner
  • 개요
  • Story 만들기
  • chromatic
  • Diff, Review
  • 마무리
Streaming과 Lazy LoadingImage Lazy Loading