diff --git a/content/intro-to-storybook/react/ko/accessibility-testing.md b/content/intro-to-storybook/react/ko/accessibility-testing.md
new file mode 100644
index 000000000..4a16e4746
--- /dev/null
+++ b/content/intro-to-storybook/react/ko/accessibility-testing.md
@@ -0,0 +1,75 @@
+---
+title: '접근성 테스트'
+tocTitle: '접근성 테스트'
+description: '워크플로우에 접근성 테스트를 통합하는 방법을 배워보세요'
+---
+
+지금까지 우리는 시각적 테스트와 그 기능에 중점을 두고, 점차 복잡도를 높여가며 UI 컴포넌트를 구축하는 것에 집중해 왔습니다. 하지만 아직 UI 개발에 중요한 한 측면을 다루지 않았습니다. 바로 접근성입니다.
+
+## 왜 접근성(A11y)일까요?
+
+접근성은 모든 사용자가 신체적 능력에 관계없이 컴포넌트와 효과적으로 상호작용할 수 있도록 보장합니다. 여기에는 시각, 청각, 운동 또는 인지 장애가 있는 사용자가 포함됩니다. 접근성은 올바른 일일 뿐만 아니라 법적 요구 사항과 업계 표준에 따라 점점 더 의무화되고 있습니다. 이러한 요구 사항을 고려할 때, 우리는 UI 컴포넌트의 접근성을 개발 초기부터 자주 테스트해야 합니다.
+
+## Storybook으로 접근성 문제 해결하기
+
+Storybook은 컴포넌트의 접근성을 테스트하는 데 도움을 주는 [접근성 애드온](https://storybook.js.org/addons/@storybook/addon-a11y) (A11y)을 제공합니다. [axe-core](https://github.com/dequelabs/axe-core)를 기반으로 하며, [WCAG 문제의 최대 57%](https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/)를 포착할 수 있습니다.
+
+어떻게 작동하는지 봅시다! 애드온을 설치하려면 다음 명령을 실행하세요:
+
+```shell
+yarn exec storybook add @storybook/addon-a11y
+```
+
+
+
+💡 Storybook의 `add` 명령은 애드온의 설치와 설정을 자동화합니다. 사용 가능한 다른 명령어에 대해 자세히 알아보려면 [공식 문서](https://storybook.js.org/docs/api/cli-options)를 참조하세요.
+
+
+
+스토리북을 재시작하여 UI에서 새로운 애드온이 활성화되었는지 확인하세요.
+
+
+
+스토리들을 순환하면서, 애드온이 테스트 상태 중 하나에서 접근성 문제를 발견한 것을 볼 수 있습니다.[**"색상 대비"**](https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=axeAPI)위반은 기본적으로 작업 제목 색과 배경 색 간의 대비가 충분하지 않다는 의미입니다. 애플리케이션의 CSS(`src/index.css`)에서 텍스트 색상을 더 어두운 회색으로 변경하여 간단히 해결할 수 있습니다.
+
+```diff:title=src/index.css
+.list-item.TASK_ARCHIVED input[type="text"] {
+- color: #a0aec0;
++ color: #4a5568;
+ text-decoration: line-through;
+}
+```
+
+됐습니다! 우리는 UI 접근성 확보의 첫 걸음을 뗐습니다. 하지만 이게 우리가 해야할 일의 끝은 아닙니다. 접근성을 유지하는것은 지속적인 과정이며 우리는 애플리케이션의 발전하고 UI가 복잡해짐에 따라 새로운 접근성 문제나 회귀 현상이 발생하지 않도록 모니터링해야 합니다.
+
+## Chromatic을 이용한 접근성 테스트
+
+스토리북의 접근성 애드온을 사용하면 개발 중에 접근성 문제를 테스트하고 즉각적인 피드백을 받을 수 있습니다. 하지만 접근성 문제를 추적하는 것은 쉽지 않은 일이며, 어떤 문제를 먼저 해결할지 우선순위를 정하는 데에도 노력이 필요할 수 있습니다. 바로 이 때 Chromatic이 도움을 줄 수 있습니다. 이미 보았듯이, Chromatic은 [시각적 테스트](/intro-to-storybook/react/ko/test/)를 통해 회귀를 방지하는 데 도움을 주었습니다. 이제 [접근성 테스트 기능](https://www.chromatic.com/docs/accessibility)을 사용하여 UI가 접근성을 유지하도록 하고, 실수로 새로운 위반 사항이 발생하지 않도록 할 것입니다.
+
+### 접근성 테스트 활성화하기
+
+Chromatic 프로젝트로 이동하여 **Manage** 페이지로 이동합니다. **Enable** 버튼을 클릭하여 해당 프로젝트의 접근성 테스트를 활성화합니다.
+
+
+
+### 접근성 테스트 실행하기
+
+이제 접근성 테스트를 활성화 했고 CSS의 색상 대비 문제를 해결했으니, 변경 사항을 푸시하여 새로운 Chromatic 빌드를 실행해 봅시다.
+
+```shell:clipboard=false
+git add .
+git commit -m "Fix color contrast accessibility violation"
+git push
+```
+
+Chromatic이 실행되면, 향후 진행할 테스트들의 기준점이 될 [접근성 기준선](https://www.chromatic.com/docs/accessibility/#what-is-an-accessibility-baseline)을 설정합니다. 이를 통해 우리는 새로운 회귀 현상을 일으키지 않으면서 효과적으로 우선순위에 따라 해결할 수 있습니다.
+
+
+
+
+
+이제 우리는 성공적으로 개발의 모든 단계에서 UI가 접근성을 유지할 수 있는 워크플로우를 구축했습니다. Storybook이 개발 과정에서 발생하는 접근성 문제를 발견하는데 도움을 준다면, Chromatic은 접근성 회귀 현상을 추적하고 점진적으로 해결해나가기 쉽게 만들어줍니다.
diff --git a/content/intro-to-storybook/react/ko/composite-component.md b/content/intro-to-storybook/react/ko/composite-component.md
index e553e5d93..670d65eec 100644
--- a/content/intro-to-storybook/react/ko/composite-component.md
+++ b/content/intro-to-storybook/react/ko/composite-component.md
@@ -19,14 +19,32 @@ Taskbox는 핀으로 고정된 task를 일반 task 위에 배치하여 강조합
## 설정하기
-복합 컴포넌트는 기본 컴포넌트와 크게 다르지 않습니다. `TaskList` 컴포넌트와 그에 해당하는 스토리 파일을 만들어보겠습니다. `src/components/TaskList.jsx` 와 `src/components/TaskList.stories.jsx`를 생성해 주세요.
+복합 컴포넌트는 기본 컴포넌트와 크게 다르지 않습니다. `TaskList` 컴포넌트와 그에 해당하는 스토리 파일을 만들어보겠습니다. `src/components/TaskList.tsx` 와 `src/components/TaskList.stories.tsx`를 생성해 주세요.
우선 `TaskList`의 대략적인 구현부터 시작하겠습니다. 이전의 `Task` 컴포넌트를 가져온 후, 속성과 액션을 입력값으로 전달해 주세요.
-```jsx:title=src/components/TaskList.jsx
+```tsx:title=src/components/TaskList.tsx
+import type { TaskData } from '../types';
+
import Task from './Task';
-export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
+type TaskListProps = {
+ /** Checks if it's in loading state */
+ loading?: boolean;
+ /** The list of tasks */
+ tasks: TaskData[];
+ /** Event to change the task to pinned */
+ onPinTask: (id: string) => void;
+ /** Event to change the task to archived */
+ onArchiveTask: (id: string) => void;
+};
+
+export default function TaskList({
+ loading = false,
+ tasks,
+ onPinTask,
+ onArchiveTask,
+}: TaskListProps) {
const events = {
onPinTask,
onArchiveTask,
@@ -42,7 +60,7 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
return (
@@ -52,25 +70,30 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
그리고, 스토리 파일 안에 `TaskList`의 테스트 상태값들을 만들어 보세요.
-```jsx:title=src/components/TaskList.stories.jsx
+```tsx:title=src/components/TaskList.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
import TaskList from './TaskList';
import * as TaskStories from './Task.stories';
-export default {
+const meta = {
component: TaskList,
title: 'TaskList',
decorators: [(story) =>
{story()}
],
- tags: ['autodocs'],
+ tags: ["autodocs"],
args: {
...TaskStories.ActionsData,
},
-};
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
-export const Default = {
+export const Default: Story = {
args: {
// Shaping the stories through args composition.
- // The data was inherited from the Default story in Task.stories.jsx.
+ // The data was inherited from the Default story in Task.stories.tsx.
tasks: [
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
@@ -82,7 +105,7 @@ export const Default = {
},
};
-export const WithPinnedTasks = {
+export const WithPinnedTasks: Story = {
args: {
tasks: [
...Default.args.tasks.slice(0, 5),
@@ -91,14 +114,14 @@ export const WithPinnedTasks = {
},
};
-export const Loading = {
+export const Loading: Story = {
args: {
tasks: [],
loading: true,
},
};
-export const Empty = {
+export const Empty: Story = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
@@ -122,7 +145,7 @@ export const Empty = {
@@ -131,10 +154,28 @@ export const Empty = {
우리의 컴포넌트는 아직 기본 뼈대만을 갖추었지만, 앞으로 작업하게 될 스토리에 대한 아이디어를 얻었습니다. `.list-items` 래퍼(wrapper)가 지나치게 단순하다고 생각할 수도 있습니다. 맞습니다! 대부분의 경우에 우리는 단지 래퍼(wrapper)를 추가하기 위해서 새로운 컴포넌트를 만들지 않습니다. 하지만 `TaskList` 컴포넌트의 **진정한 복잡성**은 `withPinnedTasks`, `loading` 그리고 `empty`에서 드러날 것입니다.
-```jsx:title=src/components/TaskList.jsx
+```tsx:title=src/components/TaskList.tsx
+import type { TaskData } from '../types';
+
import Task from './Task';
-export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
+type TaskListProps = {
+ /** Checks if it's in loading state */
+ loading?: boolean;
+ /** The list of tasks */
+ tasks: TaskData[];
+ /** Event to change the task to pinned */
+ onPinTask: (id: string) => void;
+ /** Event to change the task to archived */
+ onArchiveTask: (id: string) => void;
+};
+
+export default function TaskList({
+ loading = false,
+ tasks,
+ onPinTask,
+ onArchiveTask,
+}: TaskListProps) {
const events = {
onPinTask,
onArchiveTask,
@@ -172,8 +213,8 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
}
const tasksInOrder = [
- ...tasks.filter((t) => t.state === 'TASK_PINNED'),
- ...tasks.filter((t) => t.state !== 'TASK_PINNED'),
+ ...tasks.filter((t) => t.state === "TASK_PINNED"),
+ ...tasks.filter((t) => t.state !== "TASK_PINNED"),
];
return (
@@ -189,87 +230,13 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
목록에서 핀으로 고정된 task의 위치를 확인해 주세요. 핀으로 고정된 task를 사용자를 위해 목록의 맨 위에 위치하도록 우선순위를 부여합니다.
-## 데이터 요구사항 및 props
-
-컴포넌트가 커질수록 입력에 필요한 데이터 요구사항도 함께 커집니다. `TaskList`에서 prop의 요구사항을 정의해봅시다. `Task`는 하위 컴포넌트이기 때문에 렌더링에 필요한 적합한 형태의 데이터를 제공해야 합니다. 시간 절약을 위해서 `Task`에서 사용한 `propTypes`를 재사용하겠습니다.
-
-```diff:title=src/components/TaskList.jsx
-+ import PropTypes from 'prop-types';
-
-import Task from './Task';
-
-export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
- const events = {
- onPinTask,
- onArchiveTask,
- };
- const LoadingRow = (
-
@@ -162,7 +177,7 @@ export default function TaskList() {
}
```
-이제 컴포넌트를 생성할 실제 데이터를 리덕스 스토어에서 받았으므로, 이를 `src/App.jsx`에 연결하여 컴포넌트를 렌더링 할 수 있습니다. 그러나 지금은 먼저 컴포넌트 중심의 여정을 계속해나가도록 하겠습니다.
+이제 컴포넌트를 생성할 실제 데이터를 리덕스 스토어에서 받았으므로, 이를 `src/App.tsx`에 연결하여 컴포넌트를 렌더링 할 수 있습니다. 그러나 지금은 먼저 컴포넌트 중심의 여정을 계속해나가도록 하겠습니다.
그에 대한 내용은 다음 챕터에서 다룰 것이므로 걱정하지 않으셔도 됩니다.
@@ -170,19 +185,27 @@ export default function TaskList() {
이 변경으로 인해 스토리북 스토리가 작동을 멈추게 되었습니다. 왜냐하면 `TaskList`는 이제 리덕스 스토어에 의존하여 task를 가져오고 업데이트하는 연결된 컴포넌트이기 때문에 스토리북 테스트는 작동을 멈추었을 것입니다.
-
+
+
+
이 문제를 해결하기 위해 다양한 접근 방식을 사용할 수 있습니다. 우리 앱은 매우 간단하기 때문에 [이전 장](/intro-to-storybook/react/ko/composite-component/)에서 했던 것처럼 데코레이터에 의존하여 스토리북 스토리에서 모의(mocked) 스토어를 제공할 수 있습니다:
-```jsx:title=src/components/TaskList.stories.jsx
-import TaskList from './TaskList';
+```tsx:title=src/components/TaskList.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
-import * as TaskStories from './Task.stories';
+import type { TaskData } from '../types';
import { Provider } from 'react-redux';
import { configureStore, createSlice } from '@reduxjs/toolkit';
+import TaskList from './TaskList';
+
+import * as TaskStories from './Task.stories';
+
// A super-simple mock of the state of the store
export const MockedState = {
tasks: [
@@ -192,18 +215,24 @@ export const MockedState = {
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
- ],
+ ] as TaskData[],
status: 'idle',
error: null,
};
// A super-simple mock of a redux store
-const Mockstore = ({ taskboxState, children }) => (
+const Mockstore = ({
+ taskboxState,
+ children,
+}: {
+ taskboxState: typeof MockedState;
+ children: React.ReactNode;
+}) => (
{
@@ -222,24 +251,27 @@ const Mockstore = ({ taskboxState, children }) => (
);
-export default {
+const meta = {
component: TaskList,
title: 'TaskList',
decorators: [(story) =>
{story()}
],
tags: ['autodocs'],
excludeStories: /.*MockedState$/,
-};
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
-export const Default = {
+export const Default: Story = {
decorators: [
(story) => {story()},
],
};
-export const WithPinnedTasks = {
+export const WithPinnedTasks: Story = {
decorators: [
(story) => {
- const pinnedtasks = [
+ const pinnedtasks: TaskData[] = [
...MockedState.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];
@@ -258,7 +290,7 @@ export const WithPinnedTasks = {
],
};
-export const Loading = {
+export const Loading: Story = {
decorators: [
(story) => (
(
diff --git a/content/intro-to-storybook/react/ko/deploy.md b/content/intro-to-storybook/react/ko/deploy.md
index 5f2f4c430..f92b5a869 100644
--- a/content/intro-to-storybook/react/ko/deploy.md
+++ b/content/intro-to-storybook/react/ko/deploy.md
@@ -66,7 +66,12 @@ yarn chromatic --project-token=
완료되면 배포된 스토리북의 `https://random-uuid.chromatic.com` 링크를 받으실 것입니다. 해당 링크를 팀과 공유하여 피드백을 받으세요.
-
+
+
+
오예! 우리는 하나의 명령어를 사용하여 스토리북을 배포해보았습니다. 하지만 UI 구현 후 피드백을 받을 때마다 매번 명령어를 수동적으로 실행하는 것은 반복적인 일입니다. 코드를 push할 때마다 최신 버전의 컴포넌트를 배포하는 것이 더 이상적입니다. 따라서 스토리북을 지속적으로 배포할 필요가 있습니다.
diff --git a/content/intro-to-storybook/react/ko/get-started.md b/content/intro-to-storybook/react/ko/get-started.md
index 545fdac0c..6f5d0b81e 100644
--- a/content/intro-to-storybook/react/ko/get-started.md
+++ b/content/intro-to-storybook/react/ko/get-started.md
@@ -41,6 +41,12 @@ yarn dev
우리의 주요 프론트엔드 애플리케이션 방식: 컴포넌트 개발(Storybook)과 애플리케이션 자체 개발이 있습니다.
+
+

앱의 어느 부분을 작업하느냐에 따라, 이 중 하나 또는 여러 개를 동시에 실행하고 싶을 수 있습니다. 현재 우리의 초점은 단일 UI 컴포넌트를 만드는 것이므로, Storybook만 실행할 것입니다.
diff --git a/content/intro-to-storybook/react/ko/screen.md b/content/intro-to-storybook/react/ko/screen.md
index 26f76b0f3..925c71ae7 100644
--- a/content/intro-to-storybook/react/ko/screen.md
+++ b/content/intro-to-storybook/react/ko/screen.md
@@ -11,48 +11,57 @@ commit: '12a7932'
## 화면에 연결하기
-우리 앱은 간단하기 때문에 만들 화면도 매우 간단합니다. 원격 API에서 데이터를 가져와 `TaskList` 컴포넌트(리덕스(Redux)를 통해 자체적으로 데이터를 제공함)를 감싸고, 최상위 레벨의 `error` 필드를 리덕스에서 가져오는 것입니다.
+우리 애플리케이션은 간단하기 때문에 만들 화면도 매우 간단합니다. 원격 API에서 데이터를 가져와 `TaskList` 컴포넌트(리덕스(Redux)를 통해 자체적으로 데이터를 제공함)를 레이아웃으로 감싸고, 최상위 레벨의 `error` 필드를 스토어에서 가져오는 것입니다. (서버 연결에 문제가 발생할 경우를 대비해 해당 필드를 설정한다고 가정해 봅시다.)
-우리는 먼저 Redux 스토어(src/lib/store.js)를 업데이트하여 원격 API에 연결하고, 애플리케이션의 다양한 상태(예: `error`, `succeeded`)를 처리하도록 하겠습니다.
+우리는 먼저 Redux 스토어(`src/lib/store.ts`)를 업데이트하여 원격 API에 연결하고, 애플리케이션의 다양한 상태(예: `error`, `succeeded`)를 처리하도록 하겠습니다.
-```diff:title=src/lib/store.js
+```ts:title=src/lib/store.ts
/* A simple redux store/actions/reducer implementation.
* A true app would be more complex and separated into different files.
*/
+import type { TaskData } from '../types';
+
import {
configureStore,
createSlice,
-+ createAsyncThunk,
+ createAsyncThunk,
+ type PayloadAction,
} from '@reduxjs/toolkit';
+interface TaskBoxState {
+ tasks: TaskData[];
+ status: 'idle' | 'loading' | 'failed' | 'succeeded';
+ error: string | null;
+}
+
/*
* The initial state of our store when the app loads.
* Usually, you would fetch this from a server. Let's not worry about that now
*/
-
-const TaskBoxData = {
+const TaskBoxData: TaskBoxState = {
tasks: [],
status: 'idle',
error: null,
};
-
/*
* Creates an asyncThunk to fetch tasks from a remote endpoint.
* You can read more about Redux Toolkit's thunks in the docs:
* https://redux-toolkit.js.org/api/createAsyncThunk
*/
-+ export const fetchTasks = createAsyncThunk('todos/fetchTodos', async () => {
-+ const response = await fetch(
-+ 'https://jsonplaceholder.typicode.com/todos?userId=1'
-+ );
-+ const data = await response.json();
-+ const result = data.map((task) => ({
-+ id: `${task.id}`,
-+ title: task.title,
-+ state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX',
-+ }));
-+ return result;
-+ });
+export const fetchTasks = createAsyncThunk('taskbox/fetchTasks', async () => {
+ const response = await fetch(
+ 'https://jsonplaceholder.typicode.com/todos?userId=1'
+ );
+ const data = await response.json();
+ const result = data.map(
+ (task: { id: number; title: string; completed: boolean }) => ({
+ id: `${task.id}`,
+ title: task.title,
+ state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX',
+ })
+ );
+ return result;
+});
/*
* The store is created here.
@@ -63,11 +72,13 @@ const TasksSlice = createSlice({
name: 'taskbox',
initialState: TaskBoxData,
reducers: {
- updateTaskState: (state, action) => {
- const { id, newTaskState } = action.payload;
- const task = state.tasks.findIndex((task) => task.id === id);
- if (task >= 0) {
- state.tasks[task].state = newTaskState;
+ updateTaskState: (
+ state,
+ action: PayloadAction<{ id: string; newTaskState: TaskData['state'] }>
+ ) => {
+ const task = state.tasks.find((task) => task.id === action.payload.id);
+ if (task) {
+ task.state = action.payload.newTaskState;
}
},
},
@@ -75,25 +86,25 @@ const TasksSlice = createSlice({
* Extends the reducer for the async actions
* You can read more about it at https://redux-toolkit.js.org/api/createAsyncThunk
*/
-+ extraReducers(builder) {
-+ builder
-+ .addCase(fetchTasks.pending, (state) => {
-+ state.status = 'loading';
-+ state.error = null;
-+ state.tasks = [];
-+ })
-+ .addCase(fetchTasks.fulfilled, (state, action) => {
-+ state.status = 'succeeded';
-+ state.error = null;
-+ // Add any fetched tasks to the array
-+ state.tasks = action.payload;
-+ })
-+ .addCase(fetchTasks.rejected, (state) => {
-+ state.status = 'failed';
-+ state.error = "Something went wrong";
-+ state.tasks = [];
-+ });
-+ },
+ extraReducers(builder) {
+ builder
+ .addCase(fetchTasks.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ state.tasks = [];
+ })
+ .addCase(fetchTasks.fulfilled, (state, action) => {
+ state.status = 'succeeded';
+ state.error = null;
+ // Add any fetched tasks to the array
+ state.tasks = action.payload;
+ })
+ .addCase(fetchTasks.rejected, (state) => {
+ state.status = 'failed';
+ state.error = 'Something went wrong';
+ state.tasks = [];
+ });
+ },
});
// The actions contained in the slice are exported for usage in our components
@@ -110,12 +121,18 @@ const store = configureStore({
},
});
+// Define RootState and AppDispatch types
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
+
export default store;
```
-이제 원격 API 엔드포인트에서 데이터를 검색하여 스토어를 새롭게 업데이트 하고 앱의 다양한 상태를 처리하도록 준비했습니다. 이제 `src/components` 폴더에 `InboxScreen.jsx` 파일을 만들어봅시다:
+이제 원격 API 엔드포인트에서 데이터를 검색하여 스토어를 새롭게 업데이트 하고 앱의 다양한 상태를 처리하도록 준비했습니다. 이제 `src/components` 폴더에 `InboxScreen.tsx` 파일을 만들어봅시다:
+
+```tsx:title=src/components/InboxScreen.tsx
+import type { RootState, AppDispatch } from '../lib/store';
-```jsx:title=src/components/InboxScreen.jsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -125,9 +142,9 @@ import { fetchTasks } from '../lib/store';
import TaskList from './TaskList';
export default function InboxScreen() {
- const dispatch = useDispatch();
+ const dispatch = useDispatch();
// We're retrieving the error field from our updated store
- const { error } = useSelector((state) => state.taskbox);
+ const { error } = useSelector((state: RootState) => state.taskbox);
// The useEffect triggers the data fetching when the component is mounted
useEffect(() => {
dispatch(fetchTasks());
@@ -153,11 +170,12 @@ export default function InboxScreen() {
);
}
+
```
또한 `App` 컴포넌트를 변경하여 `InboxScreen`을 렌더링 합니다. (올바른 화면 선택을 위하여 router를 사용해도 되지만 여기서는 고려하지 않도록 하겠습니다.)
-```diff:title=src/App.jsx
+```diff:title=src/App.tsx
- import { useState } from 'react'
- import reactLogo from './assets/react.svg'
- import viteLogo from '/vite.svg'
@@ -204,29 +222,39 @@ export default App;
그러나 여기서 흥미로운 점은 스토리북에서 스토리를 렌더링 할 때입니다.
-앞에서 살펴보았듯이 `TaskList` 컴포넌트는 이제 **연결된** 컴포넌트가 되었습니다. 그리고 Redux 저장소에 의존하여 작업을 렌더링하고 있습니다.`InboxScreen` 또한 연결된 컴포넌트이므로 비슷한 작업을 수행하고 따라서 `InboxScreen.stories.jsx`에서 스토리를 설정할 때에도 스토어를 제공할 수 있습니다:
+앞에서 살펴보았듯이 `TaskList` 컴포넌트는 이제 **연결된** 컴포넌트가 되었습니다. 그리고 Redux 저장소에 의존하여 작업을 렌더링하고 있습니다.`InboxScreen` 또한 연결된 컴포넌트이므로 비슷한 작업을 수행하고 따라서 `InboxScreen.stories.tsx`에서 스토리를 설정할 때에도 스토어를 제공할 수 있습니다:
-```jsx:title=src/components/InboxScreen.stories.jsx
-import InboxScreen from './InboxScreen';
-import store from '../lib/store';
+```tsx:title=src/components/InboxScreen.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
import { Provider } from 'react-redux';
-export default {
+import InboxScreen from './InboxScreen';
+
+import store from '../lib/store';
+
+const meta = {
component: InboxScreen,
title: 'InboxScreen',
decorators: [(story) => {story()}],
tags: ['autodocs'],
-};
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
-export const Default = {};
+export const Default: Story = {};
-export const Error = {};
+export const Error: Story = {};
```
`error` 스토리에서 문제를 빠르게 찾아 낼 수 있습니다. 올바른 상태를 표시하는 대신 작업 목록을 표시해 줍니다. 이 문제를 피하는 한 가지 방법은 지난 장에서와 유사하게 각 상태에 대해 모의 버전을 제공하는 것이지만, 대신 이 문제를 해결하는데 도움이 되도록 잘 알려진 API mocking 라이브러리를 스토리북 애드온과 함께 사용합니다.
-
+
+
+
## API 서비스 모킹(Mocking)
@@ -240,20 +268,19 @@ export const Error = {};
yarn init-msw
```
-그리고 나서, `.storybook/preview.js` 를 업데이트 하고 초기화해야 합니다:
+그리고 나서, `.storybook/preview.ts` 를 업데이트 하고 초기화해야 합니다:
+
+```diff:title=.storybook/preview.ts
+import type { Preview } from '@storybook/react-vite';
+
+import { initialize, mswLoader } from 'msw-storybook-addon';
-```diff:title=.storybook/preview.js
import '../src/index.css';
// Registers the msw addon
-+ import { initialize, mswLoader } from 'msw-storybook-addon';
-
-// Initialize MSW
-+ initialize();
+initialize();
-//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
-/** @type { import('@storybook/react').Preview } */
-const preview = {
+const preview: Preview = {
parameters: {
controls: {
matchers: {
@@ -262,7 +289,7 @@ const preview = {
},
},
},
-+ loaders: [mswLoader],
+ loaders: [mswLoader],
};
export default preview;
@@ -270,10 +297,8 @@ export default preview;
마지막으로 `InboxScreen` 스토리를 업데이트하고 모의 원격 API 호출 [파라미터(parameter)](https://storybook.js.org/docs/writing-stories/parameters)를 포함합니다.
-```diff:title=src/components/InboxScreen.stories.jsx
-import InboxScreen from './InboxScreen';
-
-import store from '../lib/store';
+```diff:title=src/components/InboxScreen.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
+ import { http, HttpResponse } from 'msw';
@@ -281,14 +306,21 @@ import store from '../lib/store';
import { Provider } from 'react-redux';
-export default {
+import InboxScreen from './InboxScreen';
+
+import store from '../lib/store';
+
+const meta = {
component: InboxScreen,
title: 'InboxScreen',
decorators: [(story) => {story()}],
tags: ['autodocs'],
-};
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
-export const Default = {
+export const Default: Story = {
+ parameters: {
+ msw: {
+ handlers: [
@@ -300,7 +332,7 @@ export const Default = {
+ },
};
-export const Error = {
+export const Error: Story = {
+ parameters: {
+ msw: {
+ handlers: [
@@ -325,29 +357,27 @@ export const Error = {
-## 컴포넌트 테스트
+## 상호작용 테스트
지금까지 간단한 구성 요소에서 시작하여 화면에 이르기까지 완전히 작동하는 응용 프로그램을 처음부터 구축하고 스토리를 사용하여 각 변경 사항을 지속적으로 테스트할 수 있었습니다. 그러나 각각의 새로운 스토리는 UI가 깨지지 않도록 다른 모든 스토리를 수동으로 확인해야 합니다. 그것은 많은 추가 작업입니다.
이 워크플로우를 자동화하고 컴포넌트 상호작용을 자동으로 테스트할 수 없을까요?
-### play 함수를 사용하여 컴포넌트 테스트 작성하기
+### play 함수를 사용하여 상호작용 테스트 작성하기
-스토리북의 [`play`](https://storybook.js.org/docs/react/writing-stories/play-function) 함수와 [`@storybook/addon-interactions`](https://storybook.js.org/docs/writing-tests/component-testing)가 이를 돕습니다. play 함수는 스토리가 렌더링된 후 실행되는 작은 코드 스니펫이 포함됩니다.
+스토리북의 [`play`](https://storybook.js.org/docs/react/writing-stories/play-function) 함수가 이를 돕습니다. play 함수는 스토리가 렌더링된 후 실행되는 작은 코드 스니펫이 포함됩니다.이 함수는 프레임워크에 구애받지 않는 DOM API를 사용하므로, 프론트엔드 프레임워크의 종류와 상관없이 play 함수를 작성하여 UI와 상호작용하고 사용자의 동작을 시뮬레이션하는 스토리를 작성할 수 있습니다. 우리는 이 함수를 사용하여 태스크를 업데이트할 때 UI가 예상대로 작동하는지 확인할 것입니다.
-play 함수는 작업이 업데이트 될 때 UI에 어떤 일이 발생하는지 확인하는 데 도움이 됩니다. 이 기능은 프레임워크에 구애받지 않는 DOM API를 사용합니다. 따라서 play 함수를 사용하면 프레임워크에 구애받지 않고 UI와 상호작용하고 사용자의 동작을 시뮬레이션하는 스토리를 작성할 수 있습니다.
+새로 만든 `InboxScreen` 스토리를 업데이트하고 다음을 추가하여 컴포넌트 상호작용을 추가해 봅시다:
-이제 실제로 살펴보겠습니다! 새로 만든 `InboxScreen` 스토리를 업데이트하고 다음을 추가하여 컴포넌트 상호작용을 추가해 봅시다:
+```diff:title=src/components/InboxScreen.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
-```diff:title=src/components/InboxScreen.stories.jsx
-import InboxScreen from './InboxScreen';
-
-import store from '../lib/store';
++ import { waitFor, waitForElementToBeRemoved } from 'storybook/test';
import { http, HttpResponse } from 'msw';
@@ -355,21 +385,21 @@ import { MockedState } from './TaskList.stories';
import { Provider } from 'react-redux';
-+ import {
-+ fireEvent,
-+ waitFor,
-+ within,
-+ waitForElementToBeRemoved
-+ } from '@storybook/test';
+import InboxScreen from './InboxScreen';
-export default {
+import store from '../lib/store';
+
+const meta = {
component: InboxScreen,
title: 'InboxScreen',
decorators: [(story) => {story()}],
tags: ['autodocs'],
-};
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
-export const Default = {
+export const Default: Story = {
parameters: {
msw: {
handlers: [
@@ -379,21 +409,20 @@ export const Default = {
],
},
},
-+ play: async ({ canvasElement }) => {
-+ const canvas = within(canvasElement);
++ play: async ({ canvas, userEvent }) => {
+ // Waits for the component to transition from the loading state
+ await waitForElementToBeRemoved(await canvas.findByTestId('loading'));
+ // Waits for the component to be updated based on the store
+ await waitFor(async () => {
+ // Simulates pinning the first task
-+ await fireEvent.click(canvas.getByLabelText('pinTask-1'));
++ await userEvent.click(canvas.getByLabelText('pinTask-1'));
+ // Simulates pinning the third task
-+ await fireEvent.click(canvas.getByLabelText('pinTask-3'));
++ await userEvent.click(canvas.getByLabelText('pinTask-3'));
+ });
+ },
};
-export const Error = {
+export const Error: Story = {
parameters: {
msw: {
handlers: [
@@ -410,7 +439,7 @@ export const Error = {
-💡 `@storybook/test` 패키지는 `@storybook/jest`와 `@storybook/testing-library` 테스트 패키지를 대체하며, [Vitest](https://vitest.dev/) 패키지를 기반으로 더 작은 번들 크기와 더 간단한 API를 제공합니다.
+💡 `Interactions` 패널은 각 단계별 흐름을 제공해 스토리북 테스트를 시각화하는데 도움을 줍니다. 또한 상호작용을 일시 정지하고, 재개하고, 되감기 하거나 각 단계별로 실행할 수 있는 유용한 UI 제어 기능을 제공합니다.
@@ -418,52 +447,36 @@ export const Error = {
-## 테스트 러너를 사용한 테스트 자동화
-
-스토리북의 play function을 통해 UI와 상호작용하면서 작업을 업데이트할 때 UI가 어떻게 반응하는지 빠르게 확인할 수 있었고, 추가적인 수작업 없이 UI 일관성을 유지할 수 있었습니다.
-
-하지만 스토리북을 자세히 살펴보면, 상호작용 테스트는 스토리를 볼 때만 실행되는 것을 알 수 있습니다. 따라서 변경 사항이 있을 경우 모든 검사를 위해 각 스토리를 살펴봐야 합니다. 자동화할 수 없을까요?
-
-좋은 소식은 가능하다는 것입니다! 스토리북의 [test runner](https://storybook.js.org/docs/writing-tests/test-runner)가 바로 그 역할을 수행할 수 있습니다. 이는 [Playwright](https://playwright.dev/)로 구동되는 독립형 유틸리티로, 모든 상호작용 테스트를 실행하고 오류가 있는 스토리를 포착합니다.
+## Vitest 애드온을 사용한 테스트 자동화
-작동 방식을 살펴보겠습니다! 아래 명령어를 실행하여 설치하세요:
+play function을 통해 우리는 빠르게 컴포넌트와 사용자 의 상호작용을 시뮬레이션 하고, 작업을 업데이트할 때 컴포넌트가 어떻게 동작하는지 검증하여 UI 일관성을 유지할 수 있었습니다.
+하지만 스토리북을 자세히 살펴보면, 상호작용 테스트는 스토리를 볼 때만 실행되는 것을 알 수 있습니다. 이는 변경사항이 있을때마다 모든 스토리를 직접 살펴봐야 한다는 뜻입니다. 자동화할 수 없을까요?
-```shell
-yarn add --dev @storybook/test-runner
-```
-
-그 다음, `package.json`의 `scripts`를 업데이트하고 새로운 테스트 작업을 추가합니다:
-
-```json:clipboard=false
-{
- "scripts": {
- "test-storybook": "test-storybook"
- }
-}
-```
+가능합니다! 스토리북의 [Vitest 애드온](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon)은 상호작용 테스트를 더 자동화된 방식으로 실행할 수 있도록 해주고 Vitest의 강력한 기능을 활용하여 효율적이고 빠른 테스트를 경험할 수 있게 합니다. 작동 방식을 살펴보겠습니다!
-마지막으로, 스토리북을 실행한 상태에서 새 터미널 창을 열고 다음 명령을 실행합니다:
+스토리북을 실행한 상태에서 사이드바에서 "Run tests" 버튼을 클릭합니다. 그러면 스토리의 렌더링 방식, 동작 방식, play function에 정의되어있는 상호작용(`InboxScreen`스토리에 방금 추가한 것을 포함)에 대한 테스트가 실행됩니다.
-```shell
-yarn test-storybook --watch
-```
+
-💡 play 함수를 이용한 컴포넌트 테스트는 UI 컴포넌트를 테스트하는 훌륭한 방법입니다. 여기에서 본 것보다 훨씬 더 많은 기능을 수행할 수 있으며, 이에 대한 더 많은 정보를 얻으려면 [공식 문서](https://storybook.js.org/docs/writing-tests/component-testing)를 읽어보는 것이 좋습니다.
+💡 Vitest 애드온은 여기에서 본 것 외에도 다른 종류의 테스트를 포함하여 훨씬 더 많은 기능을 제공합니다. 이에 대한 더 많은 정보를 얻으려면 [공식 문서](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon)를 읽어보는 것이 좋습니다.
더 깊이 있는 테스트를 원하신다면, [테스트 핸드북](/ui-testing-handbook/)을 확인해보세요. 이 핸드북은 규모가 큰 프론트엔드 팀이 사용하는 테스트 전략을 다루고 있어 개발 워크플로우를 향상시키는 데 도움을 줄 것입니다.
-
-
-성공입니다! 이제 모든 스토리가 오류 없이 렌더링되고 자동으로 모든 검증이 통과되는지 확인할 수 있는 도구를 갖게 되었습니다. 더군다나, 만약 테스트가 실패하면, 실패한 스토리를 브라우저에서 열 수 있는 링크를 제공합니다.
+이제 우리는 일일히 확인할 필요 없이 UI 테스트를 자동화하는 도구를 갖게 되었습니다. 이는 우리가 애플리케이션을 만들어가면서 UI의 일관성과 기능성을 유지할 수 있게 해주는 훌륭한 방식입니다. 무엇보다도 테스트가 실패할 경우에 즉시 알 수 있어 해결하지 못한 문제들을 쉽고 빠르게 해결할 수 있습니다.
## 컴포넌트 주도 개발
diff --git a/content/intro-to-storybook/react/ko/simple-component.md b/content/intro-to-storybook/react/ko/simple-component.md
index 9eac0a0bd..107b4e875 100644
--- a/content/intro-to-storybook/react/ko/simple-component.md
+++ b/content/intro-to-storybook/react/ko/simple-component.md
@@ -16,20 +16,42 @@ commit: '1e576c5'
- `title` – task를 설명해주는 문자열
- `state` - 현재 어떤 task가 목록에 있으며, 선택되어 있는지의 여부
-`Task` 컴포넌트를 만들기 위해, 위에서 살펴본 여러 유형의 task에 해당하는 테스트 상태를 작성합니다. 그런 다음 모의 데이터를 사용하여 독립적 환경에서 컴포넌트를 구현하기 위해 스토리북(Storybook)을 사용합니다. 각각의 상태에 따라 컴포넌트의 모습을 수동으로 테스트하면서 진행할 것입니다.
+`Task` 컴포넌트를 만들기 위해, 위에서 살펴본 여러 유형의 task에 해당하는 테스트 상태를 작성합니다. 그런 다음 모의 데이터를 사용하여 독립적 환경에서 컴포넌트를 구축하기 위해 스토리북(Storybook)을 사용합니다. 각각의 상태에 따른 컴포넌트의 모습을 "시각적으로 테스트" 하며 진행할 것입니다.
## 설정하기
-먼저 `Task` 컴포넌트와 그에 해당하는 스토리 파일을 만들어 봅시다. `src/components/Task.jsx`와 `src/components/Task.stories.jsx`을 생성해 주세요.
+먼저 `Task` 컴포넌트와 그에 해당하는 스토리 파일을 만들어 봅시다. `src/components/Task.tsx`와 `src/components/Task.stories.tsx`을 생성해 주세요.
`Task`의 기본 구현부터 시작하겠습니다. 우리가 필요로 하는 속성들과 여러분이 task에 대해 취할 수 있는 두 가지 액션(목록 간 이동하는 것)을 간단히 살펴보도록 하겠습니다.
-```jsx:title=src/components/Task.jsx
-export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
+```tsx:title=src/components/Task.tsx
+type TaskData = {
+ id: string;
+ title: string;
+ state: 'TASK_ARCHIVED' | 'TASK_INBOX' | 'TASK_PINNED';
+};
+
+type TaskProps = {
+ task: TaskData;
+ onArchiveTask: (id: string) => void;
+ onPinTask: (id: string) => void;
+};
+
+export default function Task({
+ task: { id, title, state },
+ onArchiveTask,
+ onPinTask,
+}: TaskProps) {
return (
);
@@ -40,9 +62,10 @@ export default function Task({ task: { id, title, state }, onArchiveTask, onPinT
아래의 코드는 `Task`의 세 가지 테스트 상태를 스토리 파일에 작성한 것입니다.
-```jsx:title=src/components/Task.stories.jsx
+```tsx:title=src/components/Task.stories.tsx
+import type { Meta, StoryObj } from '@storybook/react-vite';
-import { fn } from "@storybook/test";
+import { fn } from 'storybook/test';
import Task from './Task';
@@ -51,18 +74,21 @@ export const ActionsData = {
onPinTask: fn(),
};
-export default {
+const meta = {
component: Task,
title: 'Task',
tags: ['autodocs'],
- //👇 "Data"로 끝나는 export들은 스토리가 아닙니다.
+ //👇 Our exports that end in "Data" are not stories.
excludeStories: /.*Data$/,
args: {
...ActionsData,
},
-};
+} satisfies Meta;
-export const Default = {
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
args: {
task: {
id: '1',
@@ -72,7 +98,7 @@ export const Default = {
},
};
-export const Pinned = {
+export const Pinned: Story = {
args: {
task: {
...Default.args.task,
@@ -81,7 +107,7 @@ export const Pinned = {
},
};
-export const Archived = {
+export const Archived: Story = {
args: {
task: {
...Default.args.task,
@@ -104,7 +130,7 @@ export const Archived = {
- 스토리(story)
- 스토리(story)
-스토리북에게 우리가 문서화하고 테스트하고 있는 컴포넌트에 대해 알려주기 위해, 아래 사항들을 포함하는 `default` export를 생성합니다:
+스토리북에게 우리가 테스트하고 있는 컴포넌트에 대해 알려주기 위해, 아래 사항들을 포함하는 `default` export를 생성합니다:
- `component` -- 컴포넌트 자체
- `title` -- 스토리북 사이드바에서 컴포넌트를 그룹화하거나 분류하는 방법
@@ -120,41 +146,42 @@ Arguments(인수) 혹은 줄여서 [`args`](https://storybook.js.org/docs/writin
모든 컴포넌트의 모든 조합에 동일한 액션 세트를 전달해야 하므로, 이를 하나의 `ActionsData` 변수로 묶어 매번 스토리 정의에 전달하는 것이 편리합니다. 컴포넌트가 필요로 하는 `ActionsData`를 묶는 또 다른 장점은, 이를 `export`하고 나중에 이 컴포넌트를 재사용하는 컴포넌트의 스토리에서 사용할 수 있다는 것입니다. 나중에 살펴보겠습니다.
-스토리를 만들 때, 컴포넌트가 기대하는 작업의 형태를 만들기 위해 우리는 기본 `task` 인수를 사용합니다. 이 인수는 일반적으로 실제 데이터의 형태를 기반으로 모델링됩니다. 다시 말해, 이 형태를 `export` 하면 나중에 다른 스토리에서 재사용할 수 있습니다.
-
## 환경설정
최근에 생성한 스토리를 인식하고 애플리케이션의 CSS 파일(`src/index.css`에 위치한)을 사용하기 위해 스토리북 구성 파일에 몇 가지 변경 사항이 필요합니다.
-먼저 스토리북 구성 파일(`.storybook/main.js`)을 다음과 같이 변경합니다:
+먼저 스토리북 구성 파일(`.storybook/main.ts`)을 다음과 같이 변경합니다:
+
+```diff:title=.storybook/main.ts
+import type { StorybookConfig } from '@storybook/react-vite';
-```diff:title=.storybook/main.js
-/** @type { import('@storybook/react-vite').StorybookConfig } */
-const config = {
+const config: StorybookConfig = {
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
-+ stories: ['../src/components/**/*.stories.@(js|jsx)'],
++ stories: ['../src/components/**/*.stories.@(ts|tsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-links',
- '@storybook/addon-essentials',
- '@storybook/addon-interactions',
+ '@storybook/addon-docs',
+ '@storybook/addon-vitest',
+ '@chromatic-com/storybook',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
+
export default config;
```
-위와 같이 변경을 마치셨다면, `.storybook` 폴더 내의 `preview.js`를 다음과 같이 변경합니다:
+위와 같이 변경을 마치셨다면, `.storybook` 폴더 내의 `preview.ts`를 다음과 같이 변경합니다:
+
+```diff:title=.storybook/preview.ts
+import type { Preview } from '@storybook/react-vite';
-```diff:title=.storybook/preview.js
+ import '../src/index.css';
-//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
-/** @type { import('@storybook/react').Preview } */
-const preview = {
+const preview: Preview = {
parameters: {
controls: {
matchers: {
@@ -176,7 +203,7 @@ export default preview;
@@ -187,8 +214,27 @@ export default preview;
컴포넌트는 아직 기본만 갖춘 상태입니다. 우선, 자세한 사항은 생략하고 디자인 코드를 작성해보겠습니다.
-```jsx:title=src/components/Task.jsx
-export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
+```tsx:title=src/components/Task.tsx
+type TaskData = {
+ id: string;
+ title: string;
+ state: 'TASK_ARCHIVED' | 'TASK_INBOX' | 'TASK_PINNED';
+};
+
+type TaskProps = {
+ /** Composition of the task */
+ task: TaskData;
+ /** Event to change the task to archived */
+ onArchiveTask: (id: string) => void;
+ /** Event to change the task to pinned */
+ onPinTask: (id: string) => void;
+};
+
+export default function Task({
+ task: { id, title, state },
+ onArchiveTask,
+ onPinTask,
+}: TaskProps) {
return (