일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- javascript
- springboot
- ReactHooks
- Redux
- designpatterns
- java
- css
- react-hook-form
- component
- backend
- RTK
- golang
- JPA
- hook
- Chakra
- JavaSpring
- go
- satisfiles
- React
- test
- typescript
- storybook
- 티스토리챌린지
- Spring
- Gin
- 웹애플리케이션서버
- tanstackquery
- frontend
- 오블완
- Today
- Total
bkdragon's log
Storybook 으로 협업하기 본문
Storybook 은 페이지와 UI 컴포넌트를 독립적으로 테스트 할 수 있는 라이브러리이다. 개발중인 컴포넌트의 모습을 전체 앱을 실행하지 않아도 확인할 수 있다. 디자이너와 개발자, 개발자와 개발자의 협업에 큰 도움이 된다.
최근 회사에서 공통으로 사용될 표준 컴포넌트와 유용한 훅스를 개발중인데 이것을 github packages 에 배포하고 컴포넌트는 Storybook과 Chromatic을 통해 배포해서 공유했다.
주요 개념
Story
Storybook 에서는 Story 라고하는 개념을 사용한다. 이는 컴포넌트의 렌더링된 상태를 캡쳐한 것이다.
컴포넌트는 다양한 Props를 가질 수 있다. Props 에 따라 모양이 변하거나 사용법이 달라질 수 있다. 하나의 컴포넌트에 여러 Story가 존재할 수 있고 이것들을 Storybook 을 통해 비교할 수 있다.
아래는 Storybook 을 처음 설치하면 예제로 들어있는 Button 컴포넌트의 Story 를 보여주는 예시이다.
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>;
export const Primary: Story = {
args: {
primary: true,
label: "Button",
},
};
Primary 가 Story 이다. Storybook 화면에서 Button의 Primary Story를 확인할 수 있다.
actions
Actions는 컴포넌트에서 발생하는 이벤트를 추적하고 기록하는 기능이다. 버튼 클릭, 폼 제출, 입력 변경 등의 이벤트가 발생했을 때 어떤 데이터가 전달되는지 확인할 수 있다.
import { action } from '@storybook/addon-actions';
const meta: Meta<typeof Form> = {
component: Form,
args: {
onSubmit: action('form-submit'),
onChange: action('form-change'),
onReset: action('form-reset')
}
};
Controls
Storybook의 강력한 기능중 하나인 Controls 는 Storybook 페이지에서 Props들의 값을 조정하며 확인할 수 있게 해준다.
Control type을 지정할 수도 있는데 이게 무슨말이냐 하면, 현재는 message를 string 으로 받지만 만약 들어 갈 수 있는 string이 특정 문자열들이 정해져있다고 해보자. 그럼 radio 형태로 선택하는 편이 좋을 것 이다.
const meta = {
title: "Components/Spinner",
tags: ["autodocs"],
component: Spinner,
argTypes: {
message: {
options: ["Loading...", "Please wait..."],
control: {
type: "radio",
},
},
},
} satisfies Meta<typeof Spinner>;
이런 식으로 하면 radio 버튼으로 message 를 변경할 수 있다.
복잡한 스토리를 작성하는 경우 컴포넌트의 Props 이외의 인자를 추가해서 활용할 수 있다. 특정 데이터를 보여주는 Card 라는 컴포넌트가 있고 이 컴포넌트가 특정 레이아웃 상황에서 어떻게 존재하는지 확인하기 위해 아래와 같이 스토리를 작성할 수 있다.
import type { Meta, StoryObj } from "@storybook/react";
import Card from "@/components/Card/Card";
type CardPropsAndLayoutArgs = React.ComponentProps<typeof Card> & {
layout: "grid" | "list" | "masonry";
columns: number;
gap: number;
maxWidth: number;
};
const meta: Meta<CardPropsAndLayoutArgs> = {
component: Card,
render: ({ layout, columns, gap, maxWidth, ...args }) => {
// 레이아웃별 스타일 정의
const layoutStyles = {
grid: {
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: `${gap}px`,
maxWidth: `${maxWidth}px`,
},
list: {
display: "flex",
flexDirection: "column" as const,
gap: `${gap}px`,
maxWidth: `${maxWidth}px`,
},
masonry: {
columnCount: columns,
columnGap: `${gap}px`,
maxWidth: `${maxWidth}px`,
},
};
const sampleCards = Array(6)
.fill(null)
.map((_, index) => ({
title: `${args.title} ${index + 1}`,
content: `${args.content} ${index + 1}`,
}));
return (
<div style={layoutStyles[layout]}>
{sampleCards.map((cardData, index) => (
<Card key={index} {...cardData} />
))}
</div>
);
},
// 레이아웃 관련 컨트롤 정의
argTypes: {
layout: {
control: "select",
options: ["grid", "list", "masonry"],
description: "카드 레이아웃 타입",
},
columns: {
control: { type: "range", min: 1, max: 4, step: 1 },
description: "그리드 열 수",
},
gap: {
control: { type: "range", min: 8, max: 48, step: 8 },
description: "카드 간격 (px)",
},
maxWidth: {
control: { type: "range", min: 400, max: 1200, step: 100 },
description: "최대 너비 (px)",
},
},
};
export default meta;
type Story = StoryObj<CardPropsAndLayoutArgs>;
// 다양한 레이아웃 스토리
export const GridLayout: Story = {
args: {
title: "카드",
content: "카드 내용입니다.",
layout: "grid",
columns: 2,
gap: 16,
maxWidth: 800,
},
};
export const ListLayout: Story = {
args: {
title: "카드",
content: "카드 내용입니다.",
layout: "list",
columns: 1,
gap: 16,
maxWidth: 600,
},
};
export const MasonryLayout: Story = {
args: {
title: "카드",
content: "카드 내용입니다.",
layout: "masonry",
columns: 3,
gap: 16,
maxWidth: 1000,
},
};
카드에서 실제로 쓰는건 title과 cotent 뿐이다. 추가적인 인자를 통해 부모 요소의 레이아웃을 바꿔서 카드 컴포넌트가 어떤식으로 렌더링 되는지 확인한다.
추가적인 여러 테크닉이 있지만, 설명은 여기서 마치고 배포에 대한 내용으로 넘어가자. (제목이 'Storybook으로 협업하기'니까.. )
배포
Chromatic
Storybook은 Chormatic 을 통해 무료로 배포할 수 있다. chromatic에 github를 통해 가입하면 레포를 지정할 수 있고 레포를 지정하면 아래와 같은 명령어를 제공해준다.
npx chromatic --project-token={token}
이 명령어를 실행하면 배포가 되고 배포 url을 제공해준다 .
github actions으로 자동화하기
프로젝트가 배포될 때 혹은 특정 상황에 자동으로 Storybook 을 배포하기 위해 github acions을 활용할 수 있다.
아까 얻은 project token을 레포의 secrets 에 추가해놓은 뒤 main.yml 파일을 아래와 같이 구성한다.
name: Storybook Deployment
on:
push:
branches:
- develop
jobs:
storybook:
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: npm install
- name: publish to chromatic
id: chromatic
uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Comment on Commit
uses: peter-evans/commit-comment@v2
with:
body: |
### 🚀 Storybook 배포 알림
- Storybook URL: ${{ steps.chromatic.outputs.storybookUrl }}
CHROMATIC_PROJECT_TOKEN 이 추가한 project token이고 , GITHUB_TOKEN 깃허브에서 사용하는 예약어이다.
배포 이후 commit 메시지로 배포 url 까지 나오게 하고 있다.
후기
Storybook의 가장 큰 장점은 소통이 쉬워지는 부분이라고 생각한다. 특히 배포해서 공유하는 절차가 간단해서 그 강점이 극대화 되는 것 같다. 소통은 개발자의 가장 중요한 덕목중 하나라고 생각하기 때문에 Storybook을 더 잘 활용할 수 있게 꾸준히 사용해보려고 한다. Story 작성도 아직은 기본적인 수준만 가능한데, 다양한 테크닉을 익혀봐야겠다.
'React' 카테고리의 다른 글
Storybook Decorator 로 전역 상태 사용하기 (0) | 2024.11.07 |
---|---|
합성 컴포넌트 패턴과 React Children API 를 활용한 컴포넌트 설계 (0) | 2024.10.31 |
useReducer Action 객체 타입 (1) | 2024.10.08 |
BottomSheet (0) | 2024.10.07 |
렌더링과 커밋 (2) | 2024.09.21 |