스토리북 공식 문서에서 container / presenter 패턴의 사용 사례에 대해 배워봅니다.
스토리북을 전혀 몰라도 패턴에 대해 배우실 수 있습니다.
페이지(화면)을 만드는 방법
BBC, Guardian 및 Storybook 유지보수 팀은 순수한 프레젠테이션 페이지를 만듭니다.
이 방법을 사용하면 Storybook에서 페이지를 렌더링하기 위해 특별한 작업을 수행할 필요가 없습니다.
여기서 특별한 작업이라 함은 모킹, 몽키 패치를 이용한 의존성 주입을 의미합니다.
- 장점:
- 스토리를 작성하기 쉽습니다.
- 스토리의 모든 데이터는 스토리의 파라미터로 인코딩되어 스토리북 도구(예: 컨트롤)의 다른 부분과 잘 작동합니다.
- 단점:
- 기존 앱은 이러한 방식으로 구성되지 않았을 수 있으며 변경하기 어려울 수 있습니다.
- 한 곳에서 데이터를 가져오는 것은 데이터를 사용하는 컴포넌트로 프롭스를 드릴다운해야 함을 의미합니다. 이것은 하나의 큰 GraphQL 쿼리를 구성하는 페이지에서 자연스러울 수 있지만(예를 들어) 다른 데이터 가져오기 접근 방식은 적절하지 않을 수 있습니다.
- 화면의 다른 위치에서 데이터를 점진적으로 로드하려는 경우 유연성이 떨어집니다.
프리젠테이셔널 컴포넌트를 위한 Args 구성
// YourPage.ts|tsx
import React from 'react';
import PageLayout from './PageLayout';
import Document from './Document';
import SubDocuments from './SubDocuments';
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
export interface DocumentScreen {
user?: {};
document?: Document;
subdocuments?: SubDocuments[];
}
function DocumentScreen({ user, document, subdocuments }) {
return (
<PageLayout user={user}>
<DocumentHeader document={document} />
<DocumentList documents={subdocuments} />
</PageLayout>
);
}
의존성이 연결된 컨테이너 컴포넌트 모킹하기
Storybook에서 연결된 컴포넌트를 렌더링해야 하는 경우 네트워크 요청을 모킹하여 데이터를 가져올 수 있습니다.
이를 수행할 수 있는 다양한 계층이 있습니다.
의존성 모킹 피하기
대안 : 컨테이너 컴포넌트를 모킹하기
프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 사용하는 경우, 서로 사이에 서로가 끼어있는 경우가 일반적입니다.
이 경우 대부분 의존성을 모킹합니다.
그런데 이미 각 프리젠테이셔널 컴포넌트에 대한 스토리가 있는데 컨테이너 컴포넌트 각각의 의존성 전체를 모킹할 필요가 있을까요?
이 문제에 대한 해결책은 컨테이너 컴포넌트를 제공하는 React 컨텍스트를 만드는 것입니다.
ProfilePage.js
ProfilePage.stories.js
ProfilePageContainer.js
ProfilePageContext.js
실제로 구현하기
이제 위에 설명한 패턴을 구현해 봅시다.
먼저 화면 단위의 폴더 구조입니다.
ProfilePage.js
ProfilePage.stories.js
ProfilePageContainer.js
ProfilePageContext.js
ProfilePage는 프레젠테이셔널 컴포넌트입니다.
ProfilePageContext에서 컨테이너 컴포넌트를 검색하기 위해 useContext 훅을 사용합니다.
// ProfilePage.js|jsx
import { useContext } from 'react';
import ProfilePageContext from './ProfilePageContext';
export const ProfilePage = ({ name, userId }) => {
const { UserPostsContainer, UserFriendsContainer } = useContext(ProfilePageContext);
return (
<div>
<h1>{name}</h1>
<UserPostsContainer userId={userId} />
<UserFriendsContainer userId={userId} />
</div>
);
};
스토리북에서 컨테이너 컴포넌트를 모킹하기
// ProfilePage.stories.js|jsx
import React from 'react';
import { ProfilePage } from './ProfilePage';
import { UserPosts } from './UserPosts';
//👇 Imports a specific story from a story file
import { normal as UserFriendsNormal } from './UserFriends.stories';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'ProfilePage',
component: ProfilePage,
};
const ProfilePageProps = {
name: 'Jimi Hendrix',
userId: '1',
};
// 이 부분을 주목하세요
// 첫번째 처럼 의존성을 모킹할 수도 있지만.
// 두번째 처럼 스토리북의 모킹한 컴포넌트 자체를 사용할 수 있습니다.
const context = {
//👇 We can access the `userId` prop here if required:
UserPostsContainer({ userId }) {
return <UserPosts {...UserPostsProps} />;
},
// Most of the time we can simply pass in a story.
// In this case we're passing in the `normal` story export
// from the `UserFriends` component stories.
UserFriendsContainer: UserFriendsNormal,
};
export const normal = () => {
return (
<ProfilePageContext.Provider value={context}>
<ProfilePage {...ProfilePageProps} />
</ProfilePageContext.Provider>
);
};
모든 ProfilePage 스토리에 동일한 컨텍스트가 적용되는 경우 데코레이터를 사용할 수도 있습니다.
컨테이너를 실제 애플리케이션에서 사용하기
실제 운영 환경에서는
애플리케이션의 컨텍스트에서 ProfilePageContext.Provider로 래핑하여
필요한 모든 컨테이너 컴포넌트를 ProfilePage(프리젠테이셔널 컴포넌트)에 제공합니다.
// pages/profile.js|jsx
import React from 'react';
import ProfilePageContext from './ProfilePageContext';
import { ProfilePageContainer } from './ProfilePageContainer';
import { UserPostsContainer } from './UserPostsContainer';
import { UserFriendsContainer } from './UserFriendsContainer';
//👇 Ensure that your context value remains referentially equal between each render.
const context = {
UserPostsContainer,
UserFriendsContainer,
};
export const AppProfilePage = () => {
return (
<ProfilePageContext.Provider value={context}>
<ProfilePageContainer />
</ProfilePageContext.Provider>
);
};
스토리북에서 글로벌 컨테이너 모킹하기
GlobalContainerContext를 설정했다면
Storybook의 preview.js 내에서 데코레이터를 설정하여 모든 스토리에 컨텍스트를 제공해야 합니다.
우리 애플리케이션에서도 헤더와 같이 모든 화면(페이지)에서 사용하는 컴포넌트에 동일한 로직을 적용할 수 있습니다.
// .storybook/preview.js
import React from 'react';
import { normal as NavigationNormal } from '../components/Navigation.stories';
import GlobalContainerContext from '../components/lib/GlobalContainerContext';
const context = {
NavigationContainer: NavigationNormal,
};
const AppDecorator = (storyFn) => {
return (
<GlobalContainerContext.Provider value={context}>{storyFn()}</GlobalContainerContext.Provider>
);
};
addDecorator(AppDecorator);
참고 :
https://storybook.js.org/tutorials/intro-to-storybook/react/en/data/
https://storybook.js.org/docs/react/writing-stories/build-pages-with-storybook
'FrontEnd' 카테고리의 다른 글
리액트 쿼리 : 리액트 라우터와 연계하기 (6) | 2022.09.27 |
---|---|
javascript 프로젝트에 d.ts를 이용하여 타입스크립트 도입하기 (0) | 2022.09.22 |
소프트웨어 합성 : 트랜스듀서(Transducers) (0) | 2022.09.20 |
소프트웨어 합성 : 리듀서(reducer) (0) | 2022.09.20 |
AST 활용 1편 : ESLint console.log 체크 플러그인 만들기 (0) | 2022.09.19 |