스토리북 튜토리얼 with Vue - 시작 및 간단한 컴포넌트 적용

스토리북은 독립적으로 컴포넌트를 개발하기 편하게 해주는 툴, UI의 스웨거같은 것.

Storybook Tutorials Vue

vue를 위한 튜토리얼은 한국어 번역이 없어서, 구글 페이지 번역과 함께 공부해 본다.

시작하기

스토리북은 빈 폴더에 깔 수 있게 만들어진 것이 아니기 때문에 이미 만들어진 프로젝트가 필요하다. 예제는 이미 다 세팅된 프로젝트를 받고 시작하게 하지만 기존의 프로젝트에 스토리북을 도입하는 방법은 기본 문서에서 찾았다.

init만 하면 알아서 어떤 프레임워크 썼는지 파악한다. node16버전 이상이 필요하다.

# Add Storybook: 프로젝트의 루트에서 스토리북 추가
npx storybook init

# 스토리북 시작. 로컬호스트 6006 포트에서 웰컴 화면이 나타남
npm run storybook

기억하면 좋은 개념

  • 컴포넌트 하나에 스토리가 여러개 쓸 수 있다고 보면 된다.
  • 데이터를 가공해서 넘겨주는 컴포넌트(예제의 TaskList.vue)와 렌더링만 하는 컴포넌트(예제의 PureTaskList.vue)를 분리하는 것이 좋다. 렌더링만 하는 컴포넌트의 스토리를 작성하게 된다.
  • 다른 스토리 js 파일에 작성한 스토리를 가져와서 사용할 수 있다.

[컴포넌트이름].stories.js

예제의 Task.stories.js

import 컴포넌트이름 from './컴포넌트이름.vue' // 스토리를 매칭할 컴포넌트 불러오기
import { action } from '@storybook/addon-actions' // 액션 쓸 거라서 불러오기

// 기본 설정
export default {
  // 컴포넌트
  component: 컴포넌트이름,
  // 스토리에서 쓰이지만 스토리북 앱에서 렌더링하면 안되는 정보 
  excludeStories: /.&Data$/,
  // 스토리북 앱에서 컴포넌트를 참조하는 이름
  title: '스토리북앱 사이드 바에서 사용할 컴포넌트이름',
  // 각 스토리에서 사용할 액션을 지정, 예제에서는 pin 했을 때, 완료했을 때를 이벤트로 잡았다. 
  argTypes: {
    액션이름1: {},
    액션이름2: {},
  },
}

// 액션 데이터
export const actionsData = {
  // 여기서 지정한 액션의 이름으로 컴포넌트 내부에서 emit하게 된다. 
  액션이름1: action('액션-이름-1'),
  액션이름2: action('액션-이름-2'),
}

// 템플릿 생성 - 스토리를 여러개 작성할 것이기 때문에 반복을 피하기 위해.
const Template = args => ({
  components: { 컴포넌트이름 },
  setup() {
    return { args, ...actionData }	
  },
  template: '<컴포넌트이름 v-bind="args" />',
})

// Default 스토리
export const Default = Template.bind({})
Default.args = {
  // 컴포넌트에 넘겨줄 필요한 거 넣어주면 됨. 일단 예제 그대로..
  task: {
    id: '1',
    title: 'Test Task',
    state: 'TASK_INBOX',
  },
}

// Pinned 스토리
export const Pinned = Template.bind({})
Pinned.args = {
  // 기본 스토리의 인수에서 state만 다른 값으로 넣음
  task: {
    ...Default.arge.task,
    state: 'TASK_PINNED',
  }
}

// Archived 스토리
export const Archived = Template.bind({})
Archived.arge = {
  task: {
    ...Default.args.task,
    state: 'TAST_ARCHIVED',
  }
}

[컴포넌트이름].vue

예제에서 Task.vue 기능적인 내용 위주로 쓰느라 클래스 이름 등은 빼고 썼을 수 있다. (아직 vue3 익숙하지 않아서 설명에서 버벅거릴 수 있다..)

<template>
  <!-- 프롭으로 넘어온 task의 state 값에 따라 클래스를 다르게 붙여줌 -->
  <div :class="classes">
    <!-- 왼쪽 체크박스 부분, 체크박스 클릭하면 archiveTask 실행 -->
    <label 
      :for="'checked' + task.id"
      :aria-label="'archiveTask-' + task.id" >
      <input
        type="checkbox"
        :checked="isChecked"
        disabled
        :name="'checked' + task.id"
        :id="'archiveTask-' + task.id" />
      <!-- css로 input은 안보이게 하고, 이 span으로 체크박스 모양을 만들었다. -->
      <span @click="archiveTask"></span>
    </label>

    <!-- 할 일 텍스트 보여지는 부분 title -->
    <label
      :for="'title-' + task.id"
      :aria-label="task.title" >
      <input
        type="text"
        readonly
        :value="task.title"
        :id="'title-' + task.id"
        name="title"
        placeholder="Input title" />
    </label>	

    <!-- pin 할 수 있는 버튼, 클릭하면 pinTask 실행 -->
    <button
      v-if="!isChecked"
      @click="pinTask"
      :id="'pinTask-' + task.id"
      :aria-label="'pinTask-' + task.id">
      <span />
    </button>
  </div>
</template>
<script>
  import { reactive, computed } from 'vue'

  export default {
    name: '컴포넌트이름',
    // 프롭으로 task 오프젝트 하나가 넘어와서 렌더링 하게됨
    props: {
      task: {
        type: Object, 
        required: true, 
        default: () => ({ id: '', state: '', title: '' }),
        validator: task => ['id', 'state', 'title'].every(key => key in task),
      },
    },
    emits: ['액션-이름-1', '액션-이름-2'],

    setup(props, { emit }) {
      props = reactive(props)
      return {
        // 컴포넌트 최상위 div에서 사용할 클래스 이름, list-item이라는 클래스와 상태값에 따른 클래스를 사용하기 위함. 
        classes: computed(() => ({
          'list-item TASK_INBOX': props.task.state === 'TASK_INBOX',
          'list-item TASK_PINNED': props.task.state === 'TASK_PINNED',
          'list-item TASK_ARCHIVED': props.task.state === 'TASK_ARCHIVED',
        })),

        // 체크 상태 확인
        isChecked: computed(() => props.task.state === 'TASK_ARCHIVED'),

        // 체크박스 눌렀을 때(할 일 완료하기) 액션 알리기. task의 아이디 같이 보냄. 
        archiveTask() {
          emit('액션-이름-1', props.task.id)
        },

        // pin 버튼 눌렀을 때 액션 알리기. task의 아이디 같이 보냄. 
        pinTask() {
          emit('액션-이름-2', props.task.id)
        },
      }
    },
  }
</script>

스토리북 장점

  • 데이터 값, 상태에 따른 UI 확인이 쉽다. 특히 empty, loading 상태 등. - Controls에서 데이터 값을 바꿔볼 수 있다.

    스토리북UI

  • @storybook/addon-a11y 애드온으로 접근성 문제 파악이 체계적이고 간결하다.

    스토리북UI

  • 어떤 동작이 트리거 되는 지 확인이 간편하다. 스토리북UI

  • (스토리북을 빌드 & 배포하면)다른 팀과 커뮤니케이션이 용이하다.
  • 스토리북을 도입해서 새로 만드는 컴포넌트인 경우 자연스럽게 재사용성을 고려해서 만들게 된다.
  • UI 테스트도 가능하다.
  • 필요한 컴포넌트에만 도입도 가능하다.

스토리북 단점

  • 처음에 추가하고 작성하고 익혀야할 게 많다. 러닝커브가 높다고 생각한다. (데이터 모킹 준비하는 부분이 아직 잘 이해가 안됨)
  • 기능별로 쪼개어 지지 않은 컴포넌트, 다른 컴포넌트에 의존적인 컴포넌트에는 바로 도입이 어렵다. (도입하려면 강제로 리팩토링 해야됨)

결국 스토리북..

사실 튜토리얼 보고 이 글도 쓰고 했지만 스토리북 자체가 너무 어려워서 실제로 기존 프로젝트에 도입할 수 있을까 하는 고민이 많았다. 기존에 쓰던 사람이 없고 혼자 도입해야 하기 때문이다.

그래서 다른 Vue 문서화 도구들을 찾아보았는데 딱 이렇다 할 게 없었다. 그냥 하지 말까..도 생각했지만 어떤 모양으로든 문서화는 필요하다고 느꼈다. 며칠동안 헤매다가 스토리북으로 다시 돌아왔다. 스토리북 문서에서 데이터값들을 테스트 해볼 수 있는 기능이 강력하기 때문이다. 다시 처음부터 sb init하고 살펴보니 자동으로 문서 생성하는 autodocs 기능이 매력적이었다. vue2(+ vite) 프로젝트에서도 잘 돌아가는 거 같았다.

현재 시점에서 가장 활발하게 서포트 해주고 있고 사용자도 많고 다른 SPA 프레임워크로 갈아탈 때 다시 배우지 않아도 되는 스토리북을 사용해 보기로 마음 먹었다.

Categories:

Updated:

Leave a comment