이번에는 아이콘과 3D 에셋, 로티 파일을 이용해야 할 일이 많아졌다. 앱인 만큼 유저와의 인터렉션을 매우 중요하게 생각하고 20대를 저격한 서비스이기 때문에 아기자기한 요소가 많아야 한다고 생각했기 때문이다. 정적인 파일을 백엔드에서 그때그때 보내주기보다는 정적 파일로 프론트에서 물고 있다가 필요할 때 뿌려주는 방식이 좋을 것이라고 생각하게 되어 정적인 파일을 관리하는 것이 중요하게 되었다.
정적인 파일들을 스토리북에 나타나게끔 하고 싶은데 어떻게 해야 할지 고민이라면, package-json
파일에 아래와 같이 세팅해주자.
{
"scripts": {
"storybook": "start-storybook -s public",
"build-storybook": "build-storybook -s public"
}
}
public
폴더에 대해서 명시해주는 것이다. 그러면 스토리북에서 public 폴더를 정적 폴더로 지정하여 Storybook에서 해당 폴더에 있는 정적 파일에 접근할 수 있도록 경로를 조정하여 설정해준다.
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
위와 같이 컴포넌트에 대해서 메타 데이터를 생성하고, 스토리를 만들어 주는 것은 기본적인 스토리북의 세팅으로서 익숙하게 작성하는 코드일 것이다. 그러나, 버전 7.0이상부터는 스토리의 title
이 빌드 과정에서의 정적인 분석 대상으로 설정된다. 그래서 default export에서는 title
속성이 자동으로 생성되어, 꼭 작성하지 않아도 되는 optional한 코드이다.
다시 한번 개념을 되짚어 보자면, 위와 같이 export 된 메타데이터는 스토리북 상에서의 Addon
플러그인 기능이 스토리를 조작할 수 있는 데이터를 생성해준다.
이제 기본적인 메타데이터를 정의하였으므로, 컴포넌트의 스토리를 정의할 수 있다. 위에서 정의한 Button
컴포넌트들의 스토리를 만들어볼건데, 가장 먼저 Primary
라는 스토리로 만들어 볼까?
// Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/react/api/csf
* to learn how to use render functions.
*/
export const Primary: Story = {
render: () => <Button primary label="Button" />,
};
Prmiary
는 Button
컴포넌트의 스토리로 작성된 것을 알 수 있다.
스토리북에 전역 스타일을 적용하고 싶다면 해당 UI 컴포넌트를 감싸줄 수 있는 decorator
을 정의하면 된다. 그러나, 데코레이터도 컴포넌트 단위로, 전역 단위로도 작성할 수 있으므로, 전역 단위로 적을 수 있는 preview.js
파일에 데코레이터를 작성해주도록 하자.
import React from 'react';
import { Preview } from '@storybook/react';
import { ThemeProvider } from 'styled-components';
const preview: Preview = {
decorators: [
(Story) => (
<ThemeProvider theme="default">
<Story />
</ThemeProvider>
),
],
};
export default preview;
이렇게 작성하게 되면 모든 스토리가 전역 테마 속에서 작성될 수 있다. 데코레이터는 전역 데코레이터부터 적용되어 실행되기 때문에 이 순서를 잘 기억해두자. 이렇게 전체 테마에 대해서 스토리북에서도 자유롭게 사용할 수 있게 되었다.
스토리북을 이용하면 컴포넌트 단위로 작동하는 action에 대하여 매우 편리하게 테스트를 수행할 수 있다.
이는 play
함수와 jest
라이브러리를 이용하여 동작할 수 있는데, 전체 API가 어떻게 동작하는지 테스트하는것도 중요하지만, 이렇게 만들어낸 컴포넌트 조각이 제대로 동작하고 있는지 확인하기 위해서는 이보다 편리한 방법은 없을 것이라는 생각도 든다.
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/testing-library';
import { Page } from './Page';
const meta = {
title: 'Example/Page',
component: Page,
parameters: {
layout: 'fullscreen',
},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoggedOut: Story = {};
export const LoggedIn: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = await canvas.getByRole('button', {
name: /Log in/i,
});
await userEvent.click(loginButton);
},
};
여기서 주목해야 하는 것은 LoggedIn
스토리이다. 기존 헤더 컴포넌트를 within
으로 가져온 이후, 로그인이라는 역할을 주었을 때 일어나는 인터렉션에 대하여 확인할 수 있다.
그럼 Logged in
이라는 새로운 스토리가 생성되게 되고, 로그인 된 화면이 어떻게 보이는지 확인할 수 있다. 여기서 jest
라이브러리를 이용하여 테스트 코드도 작성하게 되면 interactions addon
에 테스트의 결과가 맞는지 틀리는지 확인할 수 있도록 결과가 뜨게 된다.
이번에 스토리북으로 작성해야 하는 컴포넌트의 수가 많지도 않고, 인터렉션을 중요시 여기는 부분들이 많기 때문에 잘 동작하는지 확인하는 것이 매우 중요하다고 할 수 있겠다. 그렇기 때문에 적은 수의 컴포넌트를 만드는 대신 꼼꼼히 테스트를 진행해서 잘 동작하는지 하나하나 확인하면서 개발하고자 한다. 위에서 언급한 테스트는 interaction
을 위주로 한 테스트라면, 추가로 프로젝트에 적용할 수 있는 테스트는 아래와 같다
지난 프로젝트에서는 preview
파일에서 Router
로 감싸서 스토리북 요소를 누르면 라우팅을 진행하는 것만 했었는데, 로더를 이용하면 외부 API도 스토리북 내에서 손쉽게 호출할 수 있다. 하지만 스토리의 데이터는 대부분 args가 관리하기 때문에 되도록이면 args로 데이터를 뿌려주는 방식으로 하고, 로더는 대용량 데이터를 패칭하는데 사용하는 것을 추천한다고 한다.
import type { Meta, StoryObj } from '@storybook/react';
import fetch from 'node-fetch';
import { TodoItem } from './TodoItem';
const meta: Meta<typeof TodoItem> = {
component: TodoItem,
render: (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />,
};
export default meta;
type Story = StoryObj<typeof TodoItem>;
export const Primary: Story = {
loaders: [
async () => ({
todo: await (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
}),
],
};
이렇게 스토리 단위로 외부 API를 호출하여 데이터를 패칭하여 가져올 수 있다. API 호출을 통하여 얻은 결과는 스토리 함수의 두번째 인수로 전달하여, 스토리의 loaded
에 결합하게 된다. (그러니 인수를 꼭 까먹지 말고 넣어주자) 그러나, loaded
인수는 args
인수의 우선순위에서 밀리기 때문에 데이터가 겹쳐서 섞이지 않도록 잘 관리해줄 필요가 있다.
전반적으로 데이터를 뿌리고 싶다면 preview.ts
파일에 작성해주도록 하자. preview
파일에 작성한 loaded 데이터를 사용하고 싶다면 개별 스토리에서 해당 객체를 불러와주도록 하자.
Chromatic을 이용한다면 디자이너, 개발자가 다같이 UI 개발에 대하여 피드백을 쉽게 주고받을 수 있다.
chromatic을 이용하는 이유는 다음과 같다.
이왕 확인하는 스토리북인 만큼 제대로 만들어보자.
github actions를 이용하여 빌드할 수 있다.
# Workflow name
name: 'Chromatic Publish'
# Event for the workflow
on: push
# List of jobs
jobs:
test:
# Operating System
runs-on: ubuntu-latest
# Job steps
steps:
- uses: actions/checkout@v3
- run: yarn
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
이렇게 스토리북의 공식문서를 꼼꼼히 훑어보면서 그동안 스토리북 개발에 있어서 헷갈렸던 개념을 다잡고, 새롭게 도전해보고자 하는 영역도 탐색할 수 있었다.
이번에 꼼꼼한 개발을 통하여 일관된 개발환경을 유지할 수 있도록 하자.