-
Storybook 화면 구성하기프론트엔드 2023. 4. 20. 15:07728x90반응형SMALL
현재까지 만든 컴포넌트는 Storybook 앱에서만 사용하고 있었습니다.
이번 포스팅에서는 실제 화면 구성을 Storybook을 이용해보도록 하겠습니다.
1. redux api 호출
//src/redux/tasks.ts 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: any) => ({ id: task.id, title: task.title, state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX', updateAt: new Date(2021, 0, 1, 9, 0), })); return result; }); const tasksSlice = createSlice({ ... extraReducers(builder) { builder .addCase(fetchTasks.pending, (state) => { state.status = 'loading'; state.error = ''; state.tasks = []; }) .addCase(fetchTasks.fulfilled, (state, action) => { state.status = 'succeeded'; state.error = ''; state.tasks = action.payload; }) .addCase(fetchTasks.rejected, (state) => { state.status = 'failed'; state.error = 'something went wrong'; state.tasks = []; }); }, });
redux toolkit의 createAsyncThunk를 이용해 비동기 api를 처리해주도록 하겠습니다.
이후 extraReducer를 통해 추가적인 액션 타입에 대한 설정을 해줍니다. (비동기 api의 pending, fulfilled, rejected와 같은 상태에 대한 처리를 해줍니다.)
(redux 비동기 처리와 관련된 내용을 처음 다뤄보는데, 이에 대한 디테일은 다음에 포스트로 남겨야는 것도 좋을 것 같습니다.)
2. InBoxScreen 컴포넌트 제작
function InBoxScreen() { const dispatch = useAppDispatch(); const error = useAppSelector(selectError); useEffect(() => { dispatch(fetchTasks()); }, []); if (!!error) { return ( <div className={style['wrapper']}> <div className={style['error-wrapper']}> <HiOutlineEmojiSad size={32} /> <div className={style['title-message']}>Oh no!</div> <div className={style['subtitle-message']}>Something went wrong</div> </div> </div> ); } return ( <div className={style['wrapper']}> <nav> <h1 className={style['title-message']}>Taskbox</h1> </nav> <TaskList /> </div> ); } export default InBoxScreen;
InBoxScreen 컴포넌트는 실제 페이지에서 TaskList를 반환해주는 컴포넌트입니다.
페이지가 렌더링된 후, TaskList에서 사용될 Tasks를 비동기적으로 받아와 전역상태로 보관하게 됩니다.
InBoxScreen에서 전역 상태를 가져오거나 업데이트할 때 훅을 자세히 보면 기존 redux에서 사용하는 훅과 다릅니다. 이에 대한 디테일은 redux docs를 참고하면 될 것 같습니다.
이후 App 컴포넌트에 InBoxScreen 컴포넌트를 추가해줍니다.
function App() { return ( <Provider store={store}> <InBoxScreen /> </Provider> ); }
InBoxScreen 내부에서 사용할 전역상태에 대한 Provider를 감싸줍니다.
3. InBoxScreen story
이제 TaskList 컴포넌트가 실제 사이트에 연결됐습니다. InBoxScreen에 대한 스토리를 작성하여 여러 시나리오를 작성해보도록 하겠습니다.
export default { component: InBoxScreen, title: 'InBoxScreen', decorators: [ (Story: StoryFn) => ( <Provider store={store}> <Story /> </Provider> ), ], }; const Template: StoryFn = () => <InBoxScreen />; export const Default = Template.bind({}); export const Error = Template.bind({});
default export의 경우 InBoxScreen 컴포넌트와 title, decorators를 반환해주고, decorators를 통해 전역상태에 대한 Provider를 감싸주도록 했습니다.
InBoxScreen의 story는 두 가지로 Default와 Error가 있습니다.
4. Mock Service Worker
위와 같은 경우 Default와 Error가 같은 데이터를 가져오기 때문에 두 스토리에 차이가 없습니다.
이를 해결하기 위해 msw를 도입하여 현재 연결된 api 주소에 대해 storybook에서는 항상 mockData를 반환해줄 수 있도록 해줍니다.
설치
npm install --save-dev msw msw-storybook-addon
서비스 워커 생성
npx msw init public/
해당 명려어를 실행할 경우 public 디렉토리에 서비스 워커를 생성해줍니다.
addon 관련 설정
import { initialize, mswDecorator } from 'msw-storybook-addon'; initialize();
위의 내용을 storybook/preview.js에 추가해줍니다.
이후 storybook을 실행하면 msw의 서비스 워커를 통해 mock 데이터를 받아올 수 있습니다.
msw api 설정
현재 데이터를 불러오는 url에 대해 msw가 반환해줄 값을 정해줍니다.
export const Default = Template.bind({}); Default.parameters = { msw: { handlers: [ rest.get( 'https://jsonplaceholder.typicode.com/todos?userId=1', (req, res, ctx) => { return res(ctx.json(mockedState.tasks)); } ), ], }, }; export const Error = Template.bind({}); Error.parameters = { msw: { handlers: [ rest.get( 'https://jsonplaceholder.typicode.com/todos?userId=1', (req, res, ctx) => { return res(ctx.status(403)); } ), ], }, };
Default 스토리의 경우 api 호출할 경우 이전 포스트에서 사용한 mockedState의 tasks데이터를 반환하도록 해줍니다.
Error 스토리의 경우 api 호출할 경우 403 status를 반환하도록 해줍니다.
스토리북의 Default, Error 스토리가 의도한 대로 렌더링되는지 확인해줍니다.
storybook app 5. 인터랙티브 스토리
현재까지는 컴포넌트의 정적인 상태의 컴포넌트 렌더링 상태만을 통해 테스트를 했습니다.
이번에는 컴포넌트가 처음 렌더링된 후 사용자와의 상호작용으로 인해 UI가 의도한대로 동작하는지 확인해보도록 하겠습니다.
스토리북의 play기능을 통해 스토리가 렌더링된 후 실행될 여러 상호작용들을 포함시킬 수 있습니다.
Default.play = async ({ canvasElement }) => { const canvas = within(canvasElement); await waitFor(async () => { const pinButton3 = canvas .getByDisplayValue('Task 3') .parentElement?.parentElement?.getElementsByTagName( 'button' )[0] as Element; const pinButton5 = canvas .getByDisplayValue('Task 5') .parentElement?.parentElement?.getElementsByTagName( 'button' )[0] as Element; fireEvent.click(pinButton3); fireEvent.click(pinButton5); }); };
canvasElement는 스토리의 루트 컴포넌트로 여기서는 InBoxScreen 컴포넌트를 가리킵니다. 이를 within에 넘겨주면 getByLabelText와 같이 컴포넌트 내부의 구성요소를 찾는 메소드를 가진 element를 반환해줍니다.
이후 Task3과 Task 5의 pin을 누르는 액션을 추가해줍니다.
play를 추가해주면 밑의 interactions탭에서 추가해준 상호 작용에 대한 내용이 출력됩니다. 또한 상호작용으로 바뀐 uI의 모습도 렌더링됩니다.
상호작용 추가된 모습 이번 포스팅에서 실제 구성된 컴포넌트를 통해 화면을 구성해보기도하고 사용자의 상호작용으로 UI가 변동되는 모습까지 테스트해봤습니다. 스토리의 플레이를 작성하는 과정에서 testing library의 여러 모듈들에 대한 사용 이해도가 떨어진 것을 확인했고 이를 보충해야될 것 같습니다. 또한 Redux의 비동기 상태 관리를 살짝 사용해봤는데, 다른 thunk나 saga와 같은 미들웨어도 공부해보면 좋을 것 같다고 생각했습니다.
반응형LIST'프론트엔드' 카테고리의 다른 글
Storybook Addons (2) 2023.04.26 Storybook 스냅샷 테스트 (0) 2023.04.24 Storybook 데이터 연결하기 (0) 2023.04.19 Storybook 컴포넌트 만들기 2 (0) 2023.04.18 Storybook 컴포넌트 만들기 1 (0) 2023.04.17