본문 바로가기

FrontEnd

스토리북 개발팀이 알려주는 컨테이너 / 프리젠터 패턴 - Context API를 이용해 의존성 주입하기

반응형

스토리북 공식 문서에서 container / presenter 패턴의 사용 사례에 대해 배워봅니다.

스토리북을 전혀 몰라도 패턴에 대해 배우실 수 있습니다.

스토리 북


페이지(화면)을 만드는 방법

BBC, Guardian 및 Storybook 유지보수 팀은 순수한 프레젠테이션 페이지를 만듭니다.
이 방법을 사용하면 Storybook에서 페이지를 렌더링하기 위해 특별한 작업을 수행할 필요가 없습니다.

여기서 특별한 작업이라 함은 모킹, 몽키 패치를 이용한 의존성 주입을 의미합니다.

 

화면 단계까지 프리젠테이셔널 컴포넌트로 작성하는 것은 간단합니다.
스토리북 외부에 있는 앱의 단일 래퍼 컴포넌트에서 모든 지저분한 "connected" 논리를 수행한다는 아이디어입니다.
스토리북 소개 튜토리얼의 Data 장에서 이 접근 방식의 예를 볼 수 있습니다.
  • 장점:
    • 스토리를 작성하기 쉽습니다.
    • 스토리의 모든 데이터는 스토리의 파라미터로 인코딩되어 스토리북 도구(예: 컨트롤)의 다른 부분과 잘 작동합니다.
  • 단점:
    • 기존 앱은 이러한 방식으로 구성되지 않았을 수 있으며 변경하기 어려울 수 있습니다.
    • 한 곳에서 데이터를 가져오는 것은 데이터를 사용하는 컴포넌트로 프롭스를 드릴다운해야 함을 의미합니다. 이것은 하나의 큰 GraphQL 쿼리를 구성하는 페이지에서 자연스러울 수 있지만(예를 들어) 다른 데이터 가져오기 접근 방식은 적절하지 않을 수 있습니다.
    • 화면의 다른 위치에서 데이터를 점진적으로 로드하려는 경우 유연성이 떨어집니다.

프리젠테이셔널 컴포넌트를 위한 Args 구성

이러한 방식으로 화면을 구축할 때 복합 컴포넌트의 입력은 렌더링하는 다양한 컴포넌트의 입력(props) 조합인 것이 일반적입니다.
예를 들어, 화면이 페이지 레이아웃(현재 사용자의 세부 정보 포함), 헤더(보고 있는 문서 설명) 및 목록(하위 문서의)을 렌더링하는 경우
화면의 입력은 user, document, subdocuments로 구성될 수 있습니다.
// 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에서 연결된 컴포넌트를 렌더링해야 하는 경우 네트워크 요청을 모킹하여 데이터를 가져올 수 있습니다.
이를 수행할 수 있는 다양한 계층이 있습니다.


의존성 모킹 피하기

대안 : 컨테이너 컴포넌트를 모킹하기

props 또는 React 컨텍스트를 통해 데이터를 전달하여 연결된 "컨테이너" 컴포넌트의 의존성 전체를 모킹하는 것을 피할 수 있습니다.
그러나 컨테이너와 프리젠테이션 컴포넌트 로직의 엄격한 분할이 필요합니다.
예를 들어 데이터 가져오기 및 DOM 렌더링을 같이 담당하는 컴포넌트가 있는 경우 이전에 설명한 대로 모킹해야 합니다.

프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 사용하는 경우, 서로 사이에 서로가 끼어있는 경우가 일반적입니다.
이 경우 대부분 의존성을 모킹합니다. 

그런데 이미 각 프리젠테이셔널 컴포넌트에 대한 스토리가 있는데 컨테이너 컴포넌트 각각의 의존성 전체를 모킹할 필요가 있을까요?

 

이 문제에 대한 해결책은 컨테이너 컴포넌트를 제공하는 React 컨텍스트를 만드는 것입니다.

이를 통해 이후에 종속성을 조롱하는 것에 대해 걱정하지 않고 컴포넌트 계층의 모든 수준에서 평소와 같이 컨테이너 컴포펀트를 자유롭게 포함할 수 있습니다.
컨테이너 자체를 모킹한 프레젠테이션 컴포넌트에 해당하는 컴포넌트(카운터파트)와 교체할 수 있기 때문입니다.
 
컨텍스트 컨테이너를 앱의 특정 페이지 또는 스크린으로 나누는 것을 추천합니다.
예를 들어 ProfilePage 컴포넌트 있는 경우 다음과 같이 파일 구조를 설정할 수 있습니다.

ProfilePage.js
ProfilePage.stories.js
ProfilePageContainer.js
ProfilePageContext.js​
또한 앱의 모든 페이지에서 렌더링될 수 있는 컨테이너 컴포넌트를 "전역" 컨테이너 컨텍스트(GlobalContainerContext로 명명됨)를 설정하고 이를 애플리케이션의 최상위 수준에 추가하는 것이 종종 도움이 됩니다.
이 전역 컨텍스트 내에 모든 컨테이너를 배치하는 것이 가능하지만 전역적으로 필요한 컨테이너만 제공해야 합니다.

실제로 구현하기

이제 위에 설명한 패턴을 구현해 봅시다.

 

먼저 화면 단위의 폴더 구조입니다.

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>
  );
};

스토리북에서 컨테이너 컴포넌트를 모킹하기

Storybook의 컨텍스트에서 컨텍스트를 통해 실제로 운영 환경에서 사용하는 컨테이너 컴포넌트를 제공하는 대신
스토리북에서 사용하는 모의 컴포넌트를 제공합니다.
대부분의 경우 이러한 컴포넌트의 모의 버전은 종종 관련 스토리에서 직접 빌려올 수 있습니다.
// 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(프리젠테이셔널 컴포넌트)에 제공합니다.

 

예를 들어, Next.js에서 이것은 pages/profile.js 컴포넌트가 됩니다.
// 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/

 

Storybook Tutorials

Learn how to develop UIs with components and design systems. Our in-depth frontend guides are created by Storybook maintainers and peer-reviewed by the open source community.

storybook.js.org

https://storybook.js.org/docs/react/writing-stories/build-pages-with-storybook

 

Building pages with Storybook

Storybook is an open source tool for developing UI components in isolation for React, Vue, and Angular

storybook.js.org

 

반응형