-
Storybook 데이터 연결하기프론트엔드 2023. 4. 19. 19:30728x90반응형SMALL
이전 포스트까지 독립된 환경에서 상태를 가지지 않는 컴포넌트를 작성해보았습니다.
이번 포스트에서는 실제 데이터를 연결한 컴포넌트에 대한 Story 작성을 살펴보도록 하겠습니다.
1. Redux 설치 및 적용
redux를 이용해 전역상태를 정의해주겠습니다.
redux 관련 패키지 설치
npm install @reduxjs/toolkit react-redux
src/redux/tasks.ts 작성
interface StateType { tasks: TaskType[]; status: string; error: any; } interface ActionType { id: string; newTaskState: string; } const initialState: StateType = { tasks: [], status: 'idle', error: null, }; const tasksSlice = createSlice({ name: 'taskbox', initialState, reducers: { updateTaskState: (state, action: PayloadAction<ActionType>) => { const { id, newTaskState } = action.payload; const task = state.tasks.findIndex((task) => task.id === id); if (task >= 0) { state.tasks[task].state = newTaskState; } }, }, }); export const { updateTaskState } = tasksSlice.actions; export const selectTasks = (state: RootState) => { const tasksInOrder = [ ...state.taskbox.tasks.filter((t) => t.state === 'TASK_PINNED'), ...state.taskbox.tasks.filter((t) => t.state === 'TASK_INBOX'), ]; return tasksInOrder; }; export const selectStatus = (state: RootState) => state.taskbox.status; export { StateType }; export default tasksSlice.reducer;
- 전역 상태 - { tasks: task리스트, status: 로딩 상태 or 보통 상태, error: 에러 발생 여부 }입니다.
- updateTaskState reducer - action.payload로 넘어온 id에 해당하는 task의 상태를 newTaskState로 변경해줍니다.
- selectTasks - 현재 완료되지 않은 Task를 반환하는 selector를 정의합니다.
- selectStatus - 현재 상태를 반환해줍니다.
생성한 reducer를 기반으로 Store 또한 작성해줍니다. (store 작성은 생략하도록 하겠습니다.)
2. TaskList에 전역 상태 적용
이전에 작성한 TaskList 컴포넌트의 props는 전역상태로 대체할 것이기 때문에 모두 없애줍니다.
const dispatch = useDispatch(); const tasks = useSelector(selectTasks); const status = useSelector(selectStatus); const handlePinTask = (newID: string) => { dispatch(updateTaskState({ id: newID, newTaskState: 'TASK_PINNED' })); }; const handleArchiveTask = (newID: string) => { dispatch(updateTaskState({ id: newID, newTaskState: 'TASK_ARCHIVED' })); };
useSelector 훅을 이용해 사용할 tasks와 status 상태를 가져옵니다.
useDispatch 훅을이용해 Task 컴포넌트에서 사용될 핸들러를 정의해줍니다.
3. TaskListWrapper
각 스토리마다 다른 초기 상태를 주입해줘야합니다. 그러기 위해서 TaskListWrapper컴포넌트를 통해 각 스토리에 필요한 초기 상태를 따로따로 주입해줄 수 있도록 해줍니다. 또한 상태 초기화를 위한 reducer를 작성해줍니다.
// TaskList Wrapper function TaskListWrapper({ initialState, children }: Props) { const dispatch = useDispatch(); useEffect(() => { dispatch(initiateTaskState(initialState)); }, [dispatch, initialState]); return <>{children}</>; } export default TaskListWrapper;
// src/redux/tasks.ts ... reducers: { initiateTaskState: (state, action: PayloadAction<StateType>) => { state.tasks = action.payload.tasks; state.status = action.payload.status; state.error = action.payload.error; }, ... },
props로 받은 initialState를 initiateTaskState reducer로 넘겨줌으로써 초기 상태를 적용시켜줍니다.
4. TaskList story 수정
현재 tasks나 status를 props로 받아올 경우에 대한 story가 작성되어 있기 때문에 전역상태를 사용하는 스토리로 바꿔줘야 합니다.
초기 데이터 정의
const mockedState = { tasks: [ { ...TaskStories.Default.args!.task!, id: '1', title: 'Task 1' }, { ...TaskStories.Default.args!.task!, id: '2', title: 'Task 2' }, { ...TaskStories.Default.args!.task!, id: '3', title: 'Task 3' }, { ...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' }, ], status: 'idle', error: null, };
스토리 데코레이터 작성
저번 포스팅에서 decorators가 story 대상 컴포넌트를 wrapper로 감쌀 때 많이 사용된다고 했습니다. redux의 store에 대한 Provider를 TaskList 컴포넌트를 감싸야 하므로 각 스토리에 decorators를 선언해줍니다.
export const Default = Template.bind({}); Default.decorators = [ (Story: StoryFn) => ( <Provider store={store}> <TaskListWrapper initialState={mockedState}> <Story /> </TaskListWrapper> </Provider> ), ]; export const WithPinnedTasks = Template.bind({}); WithPinnedTasks.decorators = [ (Story: StoryFn) => { const pinnedTasks = [ ...mockedState.tasks.slice(0, 5), { id: '6', title: 'Task 6 Pinned', state: 'TASK_PINNED', updateAt: new Date(2021, 0, 1, 9, 0), }, ]; return ( <Provider store={store}> <TaskListWrapper initialState={{ ...mockedState, tasks: pinnedTasks }}> <Story /> </TaskListWrapper> </Provider> ); }, ]; export const Loading = Template.bind({}); Loading.decorators = [ (Story: StoryFn) => ( <Provider store={store}> <TaskListWrapper initialState={{ ...mockedState, status: 'loading' }}> <Story /> </TaskListWrapper> </Provider> ), ]; export const Empty = Template.bind({}); Empty.decorators = [ (Story: StoryFn) => ( <Provider store={store}> <TaskListWrapper initialState={{ ...mockedState, tasks: [] }}> <Story /> </TaskListWrapper> </Provider> ), ];
스토리에서 사용할 초기 상태값들을 따로따로 정의해줌으로써 각 스토리가 다른 상태값을 가지고 실행될 수 있도록 해줍니다.
5. excludesStories
storybook 튜토리얼을 보면 사용할 mockedState를 story 파일에서 named export해준 뒤 default export의 excludesStories속성에 mockedState변수명을 넘겨주면서 export한 mockedState는 스토리가 아니라는 것을 알려주고 있습니다.
storybook tutorial 위와 같은 상황은 storybook은 story 파일에서 named export한 것들은 모두 스토리로 간주하기 때문에 발생합니다. 만약 MockedState를 다른 파일에서도 사용해서 export해야되긴 하지만 story는 아니기 때문에 excludeStories에 변수명을 넘겨줌으로써 storybook에게 named export된 해당 변수명은 스토리가 아님을 명시해주는 것입니다.
-> 관련 페이지(storybook tutorial)
오늘은 실제 구동되는 컴포넌트를 storybook에서 실행하기 위한 방법을 알아봤습니다. 이번 스토리북 튜토리얼의 전역 상태 초기화 부분이 비효율적이라고 생각하여 저와 같은 경우 HOC?와 같은 방법을 통해 전역 상태 초기화를 했는데, 더 나은 방법은 없는지 알아보는 것도 좋을 것 같습니다.
다음 포스트에서는 현재까지 storybook앱에서만 구현했던 컴포넌트를 실제 화면에 연결하여 개발하는 과정에 대해 알아보도록 하겠습니다.
반응형LIST'프론트엔드' 카테고리의 다른 글
Storybook 스냅샷 테스트 (0) 2023.04.24 Storybook 화면 구성하기 (0) 2023.04.20 Storybook 컴포넌트 만들기 2 (0) 2023.04.18 Storybook 컴포넌트 만들기 1 (0) 2023.04.17 Storybook CRA에 적용하기 (0) 2023.04.17