Storybook을 이용한 Component 개발 (번외: vitest)

poyal·2024년 6월 5일
1
post-thumbnail

계기

매번 boilerplate를 만들면서 매번 만들던게 있었다.

개발의 편의성을 위해서 스타일 가이드 페이지를 만들고 같이 협업하는 개발자들에게 가이드 페이지를 확인하여 작업하라고 했었다.

이것이 불편하고 의존성이 높아져서 변경하고 싶어졌다.

매번 공통 컴포넌트들을 변경할때 마다 많은양의 공통 컴포넌트도 다같이 수정했어야 했다.

또 공통 컴포넌트를 수정하면, 비지니스 페이지 뿐만아니라 가이드 페이지의 내용까지 수정해야 정상적으로 빌드가 되었고, 빌드시에 가이드 페이지가 포함되어 번들의 사이즈가 커져서 쓸데없는 크기가 되었다.

예전부터 컴포넌트의 위주의 개발을 진행하고 있었는데 컴포넌트 독립적으로 테스트 위주의 개발을 진행하고 싶었다.

문제점

  • 테스트가 없다.
  • 가이드 페이지가 있으나, 빌드 번들에 포함이 된다.
  • 컴포넌트들이 가이드 페이지에 의존적이다.
  • 컴포넌트를 개발할때 가이드 페이지 or 특정 화면에 붙여서만 개발이 가능하다.

도입

예전부터 Storybook이 비쥬얼 가이드의 역활을 하고는 있다고 알았지만, 제대로 도입해 보니 생각보다 여러가지의 역활이 있었다.

장점

  • 개발 스타일 가이드 페에지의 역활을 한다.
  • 컴포넌트를 개발 할 때, 빈 페이지나 특정 페이지에 개발할 필요 없이 스토리북에 개발하면 된다.
  • 가이드 페이지를 빌드에 포함 시키지 않을 수 있다.
  • 여러 에드온으로 필요한 기능을 많이 추가 시킬수 있다.
  • 스토리북에서 비쥬얼 테스트를 진행할수 있다.
  • 스토리북을 다른포트로 배포할수 있다.

단점

  • 컴포넌트의 가이드 페이지의 의존성이 큰폭으로 떨어지지는 않는다.
  • 생각보다는 작업량이 더 많아진다.
  • devDependencies가 많아진다. 좀 많이 많아진다.

추가된 기능

  • @storybook/addon-a11y: ui 접근성을 테스트 하기위한 에드온
  • @storybook/addon-actions: ui action을 확인하기 위한 에드온
  • @storybook/addon-measure: ui 요소의 간격, 크기와 같은 내용을 시작적으로 표현
  • @storybook/addon-storysource: 스토리북의 원본 소스표시
  • @storybook/addon-viewport: 다양한 viewport를 제공하는 에드온
  • @storybook/test: storybook 내부의 ui 테스트를 진행

결과

기본 스토리 화면

기본 스토리 화면

스토리 화면 도구

스토리 화면 도구
왼쪽부터

  • 새로고침
  • 확대
  • 축소
  • 확대, 축소 초기화
  • 배경 변경
  • 격자 on/off
  • 그리드 프리뷰
  • 시뮬레이션(블러등... 필터기능)
  • viewport 선택
  • 요소의 간격, 크기 표시

화면 컨트롤러

비쥬얼 테스트


웹접근성 테스트



웹 접근성이 문제되는 구간을 표시 할수 있다.

vitest

Storybook을 구성하면서 지역 단위 테스트의 필요성도 생겼다.
Storybook이 비쥬얼 테스트와 컴포넌트 단위의 개발을 담당함으로 다른단위(Service, Store등...)의 테스트 단위도 테스트가 필요해 졌다.

결과

service

export default class SampleService {
  iamTrue(): boolean {
    return true;
  }

  iamFalse(): boolean {
    return false;
  }

  iamTen(): number {
    return 10;
  }
}

service test

import {describe, test, expect} from 'vitest';
import {render, screen}         from '@testing-library/vue';

import NotFound      from '@/app/system/view/not-found.vue';
import SampleService from '@/app/system/service/sample.service';

describe('system/not-found.vue', () => {
  test('success init', () => {
    render(NotFound);

    screen.getByText('NOT FOUND');
  });

  test('sample.service.ts', () => {
    const sampleService: SampleService = new SampleService();
    expect(sampleService.iamFalse()).toBeFalsy();
    expect(sampleService.iamTrue()).toBeTruthy();
    expect(sampleService.iamTen()).toBeTypeOf('number');
    expect(sampleService.iamTen()).toBeGreaterThan(9);
  });
});

store

import type {Ref}           from 'vue';
import {ref}                from 'vue';
import type {AxiosResponse} from 'axios';
import {defineStore}        from 'pinia';

import {Validate}  from '@/core/methods';
import Container   from '@/core/container';
import SampleAxios from '@/core/axios/sample.axios';
import Mapper      from '@/core/service/mapper.service';

import {SampleModel} from '@/app/sample/model/sample.model';

export const useSampleStore = defineStore('album-sample-store', () => {
  const sample: SampleAxios = Container.resolve(SampleAxios);
  const mapper: Mapper = Container.resolve(Mapper);

  const list: Ref<SampleModel.Response.FindAll[]> = ref([]);
  const one: Ref<SampleModel.Response.FindOne | null> = ref(null);

  function setList(id: number, params: SampleModel.Request.Search) {
    if (Validate(params)) {
      return sample.get(
        '/albums',
        {params}
      ).then((response: AxiosResponse<SampleModel.Response.FindAll[]>) => {
          list.value = mapper.toArray(SampleModel.Response.FindAll, response.data);
        }
      );
    }

    return;
  }

  function getList(): SampleModel.Response.FindAll[] {
    return mapper.toArray(SampleModel.Response.FindAll, list.value);
  }

  return {list, one, setList, getList};
});

store test

describe('sample/sample-list.vue', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  test('pinia test', async () => {
    const search: SampleModel.Request.Search = new SampleModel.Request.Search();
    const sampleStore = useSampleStore();
    const setSpy = vi.spyOn(sampleStore, 'setList');
    const getSpy = vi.spyOn(sampleStore, 'getList');

    await sampleStore.setList(1000, search);
    const lists = sampleStore.getList();

    expect(setSpy).toHaveBeenCalled();
    expect(getSpy).toHaveBeenCalled();
    expect.arrayContaining(lists);
  });
});

이후의 개발방향

  1. 컴포넌트 기준의 개발 방향으로 진행
  2. 빈 화면이나 타겟에 바로 개발을 하는게 아니라 스토리 북에서 개발
  3. 스토리북에서 props, emit이 정상동작 하는지 확인
  4. 스토리북에서 비쥬얼 테스트를 작성하여 제대로 동작하는지 확인
  5. 웹 접근성 및 반응형이라면 호환성을 체크하여 진행
  6. 컴포넌트 단계별로 스토리북이 정상동작 하는지 확인
  7. 스토어 및 서비스등 vitest 테스트도 진행
  8. 마지막으로 화면을 띄워서 확인

0개의 댓글