리액트 컴포넌트 패턴 연구 - Page & Container Pattern

Johny Kim·2021년 11월 23일
1
post-thumbnail

리액트와 Atomic Design System에 대하여 라는 글을 작성한 적이 있다. 그 글에서는 Atomic Design System 에 초점을 맞추었다. 하지만 이번에는, View를 담당하는 Atomic Design Component 에 도달하기 전까지의 컴포넌트 구조에 대하여 이야기 해보려고 한다. 위에 언급한 글에도 비슷한 내용이 있다. 이 글은 거기에서 좀 더 발전 된 구조 패턴이라고 생각한다.

👩🏻‍🏫 Page & Container 패턴 알아보기

일단 이 패턴의 이름을 다들 처음 들어 봤을 이유는, 내가 고안해냈기 때문이다.
여전히 컴포넌트 구조를 연구중이라서, 완성형이라고 할 수도 없을 지 모른다.
실망스럽겠지만 뒤로가기는 쪼금 있다가 누르고,
뭐라고 하는지 조금만 귀(눈?) 기울여 보자. 😢

Page & Container 패턴에서는 컴포넌트 종류를 크게 3가지로 본다. View Components 영역은 자유로우니, Atomic Design / VAC 패턴과 함께 사용해도 무방하다.

  1. Page Components
  2. Container Components
  3. View Components

컴포넌트 흐름은 아래와 같다.

PAGECONTAINERSVIEWPAGECONTAINERSVIEWPAGEVIEWVIEW
  1. Page는 한개 또는 여러개의 자식 Container를 가질 수 있다.
  2. Page는 다른 Page를 가질 수 있는데, 페이지 내의 서브페이지의 경우이다.
  3. Page가 View를 가질 수 있는 경우는, View 컴포넌트가 단순히 정보를 그려주는 역할만 하는 경우이다.
  4. Container/View는 Page를 자식으로 가질 수 없다.
  5. 마지막은 언제나 View 로 끝난다.

그렇다면 각각의 역할에 대해서 알아보자.

Pages Components

  • 라우팅으로 처음 도착하는 곳이다.
  • 데이터와 레이아웃을 담당한다.
  • 페이지에서 필요한 데이터들을 Fetching 한다.
  • 불러온 데이터는 스토어에 붙이거나, State로 가지고 있는다.
  • 약간의 데이터 가공도 가능하지만, 가급적 여기서는 가공하지 않는다.
  • 각 Containers or Subpages 에 보내 줄 공용 State 가 필요할 수 있다.
  • 레이아웃과 관련 된 HTML/CSS를 담당한다. (화면 분할)
  • 공용 커스텀 훅이 사용될 수 있다.
  • Subpages 가 있다면 라우팅 해준다.

Container Components

  • 기능을 담당한다.
  • 부모 Pages Component또는 Store에서 데이터를 받아서 알맞게 가공한다.
  • 비즈니스 로직을 설계한다.
  • 비즈니스 커스텀 훅이 사용될 수 있다.
  • 뷰 컴포넌트를 리턴한다.

View Components

  • 뷰 컴포넌트는 자유롭게 사용하자.
  • 재활용성은 꼭 챙겨야 한다.
  • Atomic Design System 또는 VAC 패턴을 적용할 수 있고, 둘다 적용할 수도 있겠다.
  • UI 관련 상태 관리를 위한 커스텀 훅을 사용할 수 있다.

👨🏻‍🎨 적용해보기

여기까지 설명만 듣고는 이해가 잘 가지 않으리라 생각 된다. 간단한 예시를 통해 접근해보자. 아래와 같은 화면의 구조를 짠다고 가정해보자. (실제로 저런 구조의 서비스를 운영중이다.)
그림에 글로벌 메뉴 1번만 3개 있는건 애교로 봐주자.

AdminPage.tsx

위 화면 구조에서 Page & Container 패턴을 적용시켜보자. 가장 부모 컴포넌트는 AdminPage 이다. 서브페이지의 내용은 신경쓰지 말고 부모 컴포넌트 입장에서 크게 봤을 때, 위 화면을 3등분 할 수 있다. 아래 그림과 같이 2개의 Container와 Router 영역으로 구분할 수 있다.

레이아웃을 잡기 위한 jsx의 예시는 아래와 같다.

return (
  <div class='admin-page'>
    <GlobalHeaderContainer /> // Container 1
    <main class='admin-page__main'>
      <section class='admin-page__aside'>
        <AsideContainer /> // Container 2
      </section>
      <section class='admin-page__content'>
        <Router> // Router
          <Route path='...' component={페이지1} />
          <Route path='...' component={페이지2} />
        </Router>
      </section>
    </main>
  </div>
)

AdminPage 에 포함 된 두개의 Container의 역할은 아래와 같다.

GlobalHeaderContainer

  • 프로필을 렌더하기 위해 User 데이터를 useSelector 또는 props로 받는다.
    (User데이터는 AdminPage가 렌더 되기도 전에 Store에 이미 존재한다고 가정)
  • 로고와 프로필을 렌더한다.

AsideContainer

  • 글로벌 메뉴 리스트 데이터를 가지고 있다.
  • 글로벌 메뉴 리스트는 Fetched Data 가 아니기 때문에 Container에서 가지고 있을 수 있다.
  • <SideMenus list={list} /> 뷰 컴포넌트를 렌더한다.

이제 AdminPage에서 Router 안에 있는 Page 컴포넌트인 페이지 1 컴포넌트를 보자. 파일명은 Page1.tsx 로 부르도록 하자.

Page1.tsx

AdminPage는 레이아웃 나누는 것 외에는 하는 일이 별로 없었지만 Page1은 할 일이 있다. 필요한 데이터들을 Fetching 하는 일이다. 제목각종 정보들은 DB에 저장되어있기 때문에 원하는 데이터를 얻기 위해 GET API 를 찔러주고 로딩처리를 해준다. 데이터를 받아왔다면 State 또는 Store에 저장한다.

자 이제 이 녀석의 레이아웃은 어떻게 분할하면 좋을까 생각해보자. 일단 특별한게 없이 위 아래로 나열되어있기 때문에 코드는 간단해 보인다. 제목, 정보들서브메뉴들은 같은 Container 안에 있어도 되고 따로 나눠도 될 것으로 보인다. 지금은 따로 나눠보기로 한다.

jsx의 예시는 아래와 같다.

<div class='page-1' />
  <Informations data={fetchedData} /> // 로직 없이 단순히 정보를 그려주는 역할의 View Component
  <Page1SubMenuBarContainer />
  <Router>
    <Route path='...' component={서브페이지1} />
    <Route path='...' component={서브페이지2} />
  </Router>
</div>

Page1 에 포함 된 View와 Container의 역할은 아래와 같다.

Informations

  • 기능이나 로직이 없는 뷰 컴포넌트를 Page 컴포넌트에서 불러왔다.
  • 서브메뉴 리스트를 가지고 있다.
  • 글로벌 메뉴 리스트는 Fetched Data 가 아니기 때문에 Container에서 가지고 있을 수 있다.
  • <MenuBar list={list} /> 뷰 컴포넌트를 렌더한다.

SubPage1.tsx

이제 서브페이지도 같은 패턴으로 진행하면 된다. 서브페이지에서만 필요한 새로운 정보가 있다면 Fetch 하고, 그게 아니라면 부모로부터 내려받은 데이터를 이용하면 된다. 그런것도 필요 없을 수도 있다. 초라하게 단 하나의 자식 컴포넌트만 보여주게 될 지도 모른다. 굳이 필요없다고 느껴질지 모르지만 그래도 Page 컴포넌트를 꼭 작성하도록 하자. Router로 들어오는 첫 페이지의 역할을 잘 해낼 수 있게 말이다.


이 완벽하지 않은 글을 읽어주셔서 감사합니다. 리액트가 아니더라도 앞으로 더 좋은 컴포넌트 구조를 찾기 위한 노력은 계속 될 것 같습니다.
마지막으로 위 예시의 최종 트리 사진으로 마무리 하겠습니다.

profile
작고 단단한 컴포넌트를 만들자.

0개의 댓글