Presentational-Container 패턴의 모든 것: Hooks로 대체할 수 있을까?

ClydeHan·2024년 9월 7일
3

Presentational-Container Pattern

프레젠테이셔널 컨테이너 패턴 이미지

이미지 출처: nextree.io

📌 패턴의 탄생 배경과 역사

Presentational-Container 패턴이 탄생하게 된 이유는 소프트웨어 개발의 복잡성 증가 때문이다. 애플리케이션의 규모가 커지고, 요구사항이 복잡해지면서 자연스럽게 코드의 복잡성도 늘어나게 되었다. 특히, UI와 비즈니스 로직이 뒤섞인 컴포넌트들이 점점 비대해지고 이해하기 어려워지는 문제가 발생했다. 개발자들은 이렇게 커진 컴포넌트들에서 로직과 UI가 섞여 있을 때 유지보수나 코드 수정이 얼마나 힘들어지는지를 경험하게 되었고, 이를 해결하기 위한 방법론으로 Presentational-Container 패턴이 등장했다.


이 패턴은 React와 같이 컴포넌트 기반의 UI 라이브러리에서 특히 많이 사용되었는데, 이유는 다음과 같다.

💡 단방향 데이터 흐름

React는 단방향 데이터 흐름을 권장하며, 상태와 데이터는 상위에서 하위로 전달된다. 이 때문에 상태를 처리하는 로직과 UI가 자연스럽게 분리될 수 있는 가능성이 존재했다.

💡 상태 관리 라이브러리의 등장

특히 Redux와 같은 상태 관리 라이브러리가 등장하면서 비즈니스 로직과 상태 관리를 효과적으로 분리하는 필요성이 커졌다. 단순히 UI만 담당하는 컴포넌트와 상태 로직을 관리하는 컴포넌트를 분리함으로써 복잡성을 관리하고자 했다.


이 패턴을 공식적으로 처음 소개한 사람은 Dan Abramov로, Redux의 창시자이기도 하다. 그는 React 애플리케이션의 복잡성을 해결하기 위해 상태 관리 라이브러리를 만들었고, 동시에 컴포넌트의 역할을 명확하게 분리하는 디자인 패턴을 제안했다. Dan Abramov는 2015년 Medium 글을 통해 이 개념을 소개하면서 많은 개발자들이 이를 따르기 시작했다.

초창기 React는 상태 관리가 매우 기본적이었고, 규모가 커지면서 상태와 UI 로직이 함께 섞이는 경우가 많았다. 이 문제를 해결하기 위해 컴포넌트를 두 가지로 나누자는 아이디어가 등장하게 되었고, Presentational Component와 Container Component로 역할을 분리하게 되었다.


📌 Presentational-Container 패턴의 핵심 개념

Presentational-Container 패턴리액트 컴포넌트 패턴이다. 이 패턴은 리액트에서 컴포넌트의 구조와 책임을 어떻게 나눌지에 대한 방식으로, 컴포넌트 간의 역할 분리에 중점을 둔 패턴이다.

이 패턴의 핵심 개념은 단일 책임 원칙(Single Responsibility Principle, SRP)에 있다. 단일 책임 원칙은 객체지향 프로그래밍에서 중요한 원칙 중 하나로, 각 클래스 또는 객체는 단 하나의 책임만 가져야 한다는 것이다. 이 원칙을 컴포넌트에 적용하게 되면, UI를 그리는 것과 비즈니스 로직을 처리하는 것을 명확하게 나누어야 한다는 생각이 나온다.

SRP에 대한 자세한 설명: 객체 지향 프로그래밍의 5원칙: S.O.L.I.D 원칙 (SOLID Principles)


💡 Presentational Component (프레젠테이셔널 컴포넌트)

Presentational Component는 말 그대로 프레젠테이션, 즉 UI를 담당하는 컴포넌트이다. 이 컴포넌트는 비즈니스 로직에 대해서는 전혀 관여하지 않으며, 순수하게 화면을 어떻게 렌더링할 것인가에만 초점을 맞춘다. 즉, UI의 외형동작을 관리하는 컴포넌트이다. 이런 컴포넌트는 주로 stateless (상태를 가지지 않는)이며, 데이터를 처리하거나 변경하는 기능은 없다.

  • Presentational Component는 props로 데이터를 전달받아, 해당 데이터를 시각적으로 표현하는 역할을 한다.
  • 상태나 라이프사이클 메서드와는 거의 무관하며, 오로지 UI 렌더링에만 집중한다.
  • CSS, 스타일링, 레이아웃을 관리하는 데 매우 적합하다. 즉, UI의 디자인적 요소에만 집중하게 한다.

Presentational Component의 장점 중 하나는 재사용성이다. 상태나 비즈니스 로직과 독립적이기 때문에, 다양한 상황에서 재사용할 수 있다. 만약 한 컴포넌트가 UI를 렌더링하는 것만 담당한다면, 그 컴포넌트를 여러 다른 컨텍스트에서도 사용할 수 있게 된다.


💡 Container Component (컨테이너 컴포넌트)

Container Component는 비즈니스 로직을 관리하는 컴포넌트이다. 이 컴포넌트는 상태를 관리하고, API를 호출하거나, 데이터를 처리하는 등 로직과 관련된 모든 작업을 담당한다. Container Component는 Presentational Component에게 데이터를 전달하고, 사용자가 상호작용할 때 그에 따른 로직을 처리하는 역할을 한다.

  • 상태 관리를 담당한다. React에서 useState, useEffect 같은 훅을 사용하여 상태를 변경하거나 관리한다.
  • API 호출이나 Redux를 통해 데이터를 가져오고, 그 데이터를 Presentational Component에 전달하는 역할을 한다.
  • 비즈니스 로직을 처리하고, 이를 통해 UI가 변화해야 할 때 Presentational Component에 적절한 props를 전달한다.

Container Component는 주로 비즈니스 로직과 데이터 처리에 집중하는데, 이는 상태 관리나 데이터베이스와의 상호작용, 사용자 입력 처리 등의 복잡한 로직을 UI와 분리하기 위함이다.


📌 Presentational-Container 패턴의 장점

💡 책임의 분리

Presentational-Container 패턴의 가장 큰 장점은 책임의 명확한 분리이다. 각 컴포넌트가 하나의 역할만 수행하도록 만들어지기 때문에, 코드의 복잡성을 줄이고 가독성을 높일 수 있다. UI와 로직을 하나의 컴포넌트에 몰아넣는 대신, 그 역할을 두 가지로 나누기 때문에 개발자는 각각의 컴포넌트를 이해하기 더 쉬워진다.

💡 재사용성

Presentational Component는 로직에 의존하지 않고, 순수하게 UI만을 렌더링하기 때문에 재사용성이 높다. 동일한 UI가 여러 곳에서 필요하다면 Presentational Component를 재사용하여 다양한 페이지나 컴포넌트에서 활용할 수 있다. 예를 들어, 여러 곳에서 동일한 버튼 스타일이 필요하다면 Button Presentational Component를 만들어 쉽게 재사용할 수 있다.

💡 테스트 용이성

컴포넌트가 각자의 역할에 집중하게 되면서, 테스트도 쉬워진다. Presentational Component는 단순히 props에 기반하여 UI를 렌더링하기 때문에, 스냅샷 테스트나 단순한 렌더링 테스트로 충분히 커버할 수 있다. 반면, Container Component는 로직을 테스트하면 되기 때문에 유닛 테스트통합 테스트로 충분히 검증 가능하다.

💡 유지보수성 향상

코드가 명확하게 나누어져 있기 때문에, 프로젝트가 커지더라도 유지보수가 쉬워진다. 비즈니스 로직을 변경할 필요가 있으면 Container Component만 수정하면 되고, UI 디자인이 바뀌면 Presentational Component만 수정하면 된다. 즉, 각각의 변경 사항이 서로에게 큰 영향을 주지 않도록 분리되어 있다.


📌 Presentational-Container 패턴의 단점

💡 구조의 복잡성

프로젝트가 작거나 간단한 경우에는 Presentational-Container 패턴을 도입하는 것이 오히려 불필요하게 복잡할 수 있다. 모든 컴포넌트를 분리하다 보면 코드가 비대해지고, 파일과 폴더의 수가 많아지면서 관리해야 할 부담이 커질 수 있다. 이는 프로젝트 규모에 따라 적절히 사용해야 하는 이유가 된다.

💡 데이터 전달 복잡성

Presentational Component는 데이터를 props로만 전달받아야 하므로, 이 사이에 전달해야 할 데이터가 많아지면 컴포넌트 간 데이터 전달 과정이 복잡해질 수 있다. Container Component에서 처리된 상태와 로직을 Presentational Component로 전달하는 과정이 많아지면 컴포넌트 간 결합도가 높아질 수 있다.

💡 컴포넌트 수 증가

로직과 UI를 각각 다른 컴포넌트로 분리하면서 컴포넌트의 수가 기하급수적으로 증가할 수 있다. 이는 대규모 프로젝트에서 팀이 체계적으로 관리할 수 있을 때만 유용하게 적용될 수 있으며, 그렇지 않을 경우에는 오히려 유지보수가 더 어려워질 수 있다.


📌 왜 Presentational-Container 패턴을 사용해야 하는가?

이 패턴을 사용하는 이유는 주로 코드의 유지보수성재사용성 때문이다. 대규모 애플리케이션에서는 컴포넌트가 비대해지고, 이를 제대로 관리하지 않으면 코드가 스파게티처럼 얽히게 된다. 이 때 UI와 비즈니스 로직을 분리하면 컴포넌트의 역할이 명확해지며, 코드 수정이나 확장이 훨씬 쉬워진다.

또한, 재사용성도 중요한 이유 중 하나이다. 동일한 UI가 여러 곳에서 필요할 때 Presentational Component로 만들어두면, 다양한 페이지에서 해당 컴포넌트를 재사용할 수 있다. 이는 개발 속도를 높이고 코드 중복을 줄이는 데 도움을 준다.

💡 언제 사용해야 하는가?

Presentational-Container 패턴은 대규모 애플리케이션에서 특히 유용하다. 애플리케이션이 커질수록 컴포넌트의 역할이 복잡해지고, 유지보수나 테스트가 어려워질 수 있기 때문에 UI와 로직을 분리하는 것이 필수적이다. 또한, 재사용 가능한 UI 컴포넌트를 만들거나 상태 관리가 복잡한 경우에도 이 패턴을 사용하는 것이 좋다.

반면, 작은 프로젝트단순한 UI의 경우에는 Presentational-Container 패턴을 굳이 사용할 필요가 없다. 과도한 컴포넌트 분리는 오히려 프로젝트를 복잡하게 만들 수 있다.


📌 폴더 구조

💡 분리된 폴더 구조

가장 전통적인 방식은 Presentational ComponentContainer Component를 각기 다른 폴더에 나누어 배치하는 방식이다. 이 방식은 두 역할을 물리적으로도 분리하는 것으로, 큰 규모의 프로젝트에서 관리가 용이하다.

/src
  /components
    /UserProfile
      UserProfile.js         (Presentational Component)
  /containers
    UserProfileContainer.js  (Container Component)

✏️ 장점

  • 책임 분리가 명확하다. UI와 비즈니스 로직을 물리적으로도 분리함으로써 코드의 구조가 명확해진다.
  • 대규모 애플리케이션에서 확장성이 뛰어나다. 각 폴더에서 UI 관련 컴포넌트나 비즈니스 로직만 따로 집중해서 관리할 수 있다.

✏️ 단점

  • 파일이 많아질 경우 관리가 복잡해질 수 있다.
  • 서로 관련 있는 컴포넌트가 물리적으로 멀리 떨어져 있어 관련성을 파악하기 어려울 수 있다.

💡 폴더 내 분리 구조

Presentational Component와 Container Component를 같은 폴더 내에 배치하는 방식이다. 이렇게 하면 컴포넌트와 그에 대응하는 Container가 가까이 위치하게 되어 연관성을 더 쉽게 파악할 수 있다.

/src
  /UserProfile
    UserProfile.js           (Presentational Component)
    UserProfileContainer.js  (Container Component)

✏️ 장점

  • 관련된 파일을 같은 폴더에 배치하여, 쉽게 찾을 수 있다.
  • 작은/중간 규모의 프로젝트에서 유용하며, UI와 비즈니스 로직이 가까이 있어 관리가 더 쉽다.

✏️ 단점

  • 큰 규모의 프로젝트에서는 폴더 내 파일이 많아질 수 있다, 특히 동일 폴더 내에 너무 많은 파일이 쌓일 경우 관리가 복잡해질 수 있다.
  • 역할을 완전히 분리하지 않기 때문에, 중복되거나 비슷한 파일들이 늘어날 가능성이 있다.

💡 폴더 내 역할별 구분 구조 (Role-Based)

Container ComponentPresentational Component를 역할에 따라 폴더 내에서 더 세분화하는 방식이다. 이 방식은 프로젝트가 더 커지고 복잡해질수록 컴포넌트를 체계적으로 관리할 수 있게 도와준다.

/src
  /components
    /UserProfile
      /presentational
        UserProfile.js  (Presentational Component)
      /container
        UserProfileContainer.js  (Container Component)

✏️ 장점

  • 역할에 따른 분리가 명확하다. 각 폴더의 목적이 분명해져 유지보수가 용이하다.
  • 대규모 애플리케이션에서 사용하기 좋다. 각 컴포넌트의 역할이 명확하게 나뉘어 관리가 체계적이다.

✏️ 단점

  • 초기 설정이 다소 복잡할 수 있다. 특히, 프로젝트가 작을 경우에는 과도하게 복잡해 보일 수 있다.
  • 파일과 폴더가 계층적으로 깊어질 수 있어 간단한 기능에도 많은 폴더를 열어야 할 수 있다.

💡 하나의 파일에 Presentational-Container 결합 구조

Presentational Component와 Container Component를 하나의 파일에 결합하여 작성하는 방식이다. 이 방식은 소규모 프로젝트나 간단한 컴포넌트에서 사용될 수 있다.

// UserProfile.js (하나의 파일에 Container와 Presentational을 결합)
import React, { useState, useEffect } from 'react';

// Presentational Component
const UserProfile = ({ user, onLogout }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={onLogout}>Logout</button>
    </div>
  );
};

// Container Component
const UserProfileContainer = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData().then(data => setUser(data));
  }, []);

  const handleLogout = () => {
    logoutUser();
    setUser(null);
  };

  return user ? <UserProfile user={user} onLogout={handleLogout} /> : <div>Loading...</div>;
};

export default UserProfileContainer;

✏️ 장점

  • 파일 수를 줄일 수 있다, 특히 작은 프로젝트나 간단한 컴포넌트에서 유용하다.
  • 단순한 관리가 가능하다. 한 파일 내에서 모든 컴포넌트를 정의할 수 있어 폴더 구조를 간단하게 유지할 수 있다.

✏️ 단점

  • 코드가 비대해질 수 있다. 하나의 파일 내에 모든 로직과 UI가 들어가므로, 컴포넌트가 복잡해질 경우 코드의 가독성이 떨어질 수 있다.
  • 역할이 분리되었음에도 물리적으로는 하나의 파일에 위치하기 때문에, 역할 분리의 장점이 줄어든다.

💡 폴더 구조 선택 시 고려사항

폴더 구조를 선택할 때는 프로젝트의 규모, 팀의 협업 방식, 유지보수성을 고려해야 한다.

  • 소규모 프로젝트에서는 Presentational과 Container 컴포넌트를 같은 파일이나 폴더에 두어 관리하는 것이 더 간편하다.
  • 중대규모 프로젝트에서는 Presentational과 Container를 분리된 폴더로 나누고, 역할에 따라 폴더 구조를 체계적으로 설계하는 것이 적합하다.
  • 역할에 따른 더 명확한 분리가 필요하다면, 각 컴포넌트를 역할별로 폴더에 세분화하여 관리할 수 있다.

프로젝트의 복잡도와 팀의 관리 스타일에 따라 적절한 폴더 구조를 선택하는 것이 핵심이다.


📌 Atomic Design 패턴과의 비교

💡패턴의 목적

  • 프레젠테이셔널-컨테이너 패턴은 UI와 비즈니스 로직의 분리에 중점을 둔 패턴이다. 프레젠테이셔널 컴포넌트는 UI를 그리는 역할만 하고, 컨테이너 컴포넌트는 상태 관리와 비즈니스 로직을 담당한다. 이를 통해 컴포넌트 간 책임을 명확하게 구분하고, 가독성과 유지보수성을 높이는 것을 목표로 한다.
  • 아토믹 디자인 패턴은 UI 컴포넌트를 작은 단위에서 큰 단위로 점진적으로 조립하는 방식이다. 원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages) 순으로 컴포넌트를 쌓아 올리며, UI의 재사용성과 확장성을 높이는 데 중점을 둔다.

💡 컴포넌트 구조 및 역할

  • 프레젠테이셔널-컨테이너 패턴은 컴포넌트를 두 가지로 나누는 구조이다. 프레젠테이셔널 컴포넌트는 단순히 UI를 그리는 데 집중하고, 컨테이너 컴포넌트는 데이터를 받아오고 상태를 관리한다. 이로 인해 UI와 로직이 명확히 분리되어, UI나 로직이 변경될 때 서로 영향을 최소화할 수 있다.
  • 아토믹 디자인 패턴에서는 작은 컴포넌트에서 점진적으로 큰 컴포넌트를 만드는 방식을 따른다. 원자는 가장 작은 단위로, 버튼이나 입력 필드 같은 요소를 나타낸다. 그런 원자들이 모여 분자가 되고, 분자가 모여 더 복잡한 유기체가 된다. 이렇게 하여 복잡한 UI를 구성할 수 있으며, UI 구성 요소의 일관성을 유지할 수 있다.

💡 재사용성과 유지보수성

  • 프레젠테이셔널-컨테이너 패턴UI와 로직의 분리를 통해 유지보수성을 높인다. UI 디자인이 변경되더라도 프레젠테이셔널 컴포넌트만 수정하면 되고, 로직이 변경되면 컨테이너 컴포넌트만 수정하면 된다. 그러나 프레젠테이셔널 컴포넌트 자체는 작은 컴포넌트 단위로 재사용하지 않기 때문에, UI 재사용성 측면에서는 상대적으로 덜 유연할 수 있다.
  • 아토믹 디자인 패턴모든 컴포넌트를 작은 단위에서 재사용할 수 있게 하여 UI 재사용성을 극대화한다. 원자와 분자는 애플리케이션 내 여러 곳에서 재사용될 수 있으며, 전체 UI의 일관성을 유지하면서 다양한 페이지나 템플릿을 구성할 수 있다. 이로 인해 UI 컴포넌트의 유지보수성도 높아지며, 새로운 컴포넌트 추가 시에도 기존 컴포넌트를 활용할 수 있다.

💡 적용 사례

  • 프레젠테이셔널-컨테이너 패턴대규모 애플리케이션에서 비즈니스 로직이 복잡한 경우에 유용하다. 특히, 데이터 상태 관리API 호출 등이 중요한 부분에서 UI와 로직을 분리하여 유지보수성을 높이고자 할 때 적합하다. 또한, 리덕스 같은 상태 관리 라이브러리와 결합하여 사용할 때 효과적이다.
  • 아토믹 디자인 패턴UI의 일관성을 유지하면서 확장성을 높여야 하는 디자인 시스템이나 컴포넌트 라이브러리 구축에 적합하다. 다양한 페이지와 템플릿을 구성해야 하는 대규모 프로젝트UI 프레임워크에서 사용되며, 디자인 원칙을 공유하는 팀 내에서 유용하다.

💡 결합 가능성

  • 이 두 패턴은 서로 배타적이지 않으며 결합하여 사용할 수 있다. 예를 들어, 아토믹 디자인 패턴을 활용하여 UI 컴포넌트를 작은 단위로 재사용하면서, 프레젠테이셔널-컨테이너 패턴을 사용하여 로직과 UI의 책임을 분리할 수 있다. 이를 통해 더 유연하고 유지보수 가능한 코드베이스를 만들 수 있다.

📌 커스텀 훅 패턴과의 비교

💡 패턴의 목적

  • 프레젠테이셔널-컨테이너 패턴은 UI와 비즈니스 로직을 컴포넌트 레벨에서 분리하는 데 중점을 둔다. 프레젠테이셔널 컴포넌트는 화면에 UI를 표시하는 역할에 집중하고, 컨테이너 컴포넌트는 상태 관리나 비즈니스 로직을 담당한다. 컴포넌트의 책임을 명확하게 구분함으로써 유지보수와 테스트를 용이하게 만든다.
  • 커스텀 훅 패턴로직의 재사용성을 목표로 한다. 여러 컴포넌트에서 동일한 비즈니스 로직이 반복될 때, 그 로직을 훅으로 추출해 다양한 컴포넌트에서 공유할 수 있도록 한다. 이를 통해 코드 중복을 줄이고, 비즈니스 로직을 모듈화할 수 있다.

💡 컴포넌트 구조와 로직 처리 방식

  • 프레젠테이셔널-컨테이너 패턴은 컴포넌트를 두 개의 역할로 분리한다. 프레젠테이셔널 컴포넌트는 오로지 UI를 그리는 역할만 담당하며, 상태나 비즈니스 로직을 다루지 않는다. 컨테이너 컴포넌트는 필요한 데이터를 가져오고, 상태를 관리하며, 그 데이터를 프레젠테이셔널 컴포넌트로 전달해 화면을 구성한다. 이로써 UI와 로직 간 결합을 느슨하게 하여 변경의 영향을 최소화할 수 있다.
  • 커스텀 훅 패턴로직을 컴포넌트 외부로 분리하여 함수 단위로 재사용하는 방식을 채택한다. API 호출이나 상태 관리 등의 로직을 커스텀 훅으로 캡슐화하여, 여러 컴포넌트에서 이 훅을 호출해 동일한 로직을 처리할 수 있다. 로직의 재사용성을 극대화하면서도, 로직이 컴포넌트 내부에 묶여 있지 않기 때문에 컴포넌트 자체가 간결해지는 효과가 있다.

💡 로직 재사용성

  • 프레젠테이셔널-컨테이너 패턴에서는 컨테이너 컴포넌트가 로직을 담당하지만, 이 로직은 컴포넌트 단위로 나뉘어 재사용된다. 같은 로직이 여러 컴포넌트에서 필요하면 그만큼 여러 컨테이너 컴포넌트를 따로 만들어야 하므로, 로직의 재사용성은 낮은 편이다.
  • 커스텀 훅 패턴비즈니스 로직을 함수형으로 추상화하기 때문에, 해당 로직을 여러 컴포넌트에서 호출하여 재사용할 수 있다. 상태 관리나 데이터 처리 같은 로직이 여러 컴포넌트에서 공통으로 사용된다면, 커스텀 훅을 통해 코드 중복을 방지할 수 있으며, 로직의 유지보수도 중앙 집중화되어 효율적이다.

💡 유연성

  • 프레젠테이셔널-컨테이너 패턴컴포넌트 간의 책임 분리에 중점을 두기 때문에 UI와 비즈니스 로직을 별도로 관리할 수 있어, 유연하게 컴포넌트를 확장할 수 있다. 하지만, 동일한 로직을 여러 컨테이너 컴포넌트에서 다루게 될 경우에는 유연성보다는 복잡도가 높아질 수 있다.
  • 커스텀 훅 패턴다양한 컴포넌트에서 쉽게 로직을 재사용할 수 있어 유연성이 매우 높다. 훅을 통해 로직을 모듈화함으로써 필요한 컴포넌트에서만 로직을 호출하면 되고, 새로운 컴포넌트를 추가할 때도 훅을 이용해 기존 로직을 재사용할 수 있다.

💡 적용 사례

  • 프레젠테이셔널-컨테이너 패턴비즈니스 로직과 UI 간 역할 분리가 중요한 대규모 애플리케이션에서 유용하다. 상태 관리, 데이터 fetching 등 컴포넌트별로 로직과 UI를 명확하게 분리해야 할 때 적합하다.
  • 커스텀 훅 패턴여러 컴포넌트에서 반복되는 비즈니스 로직이 존재할 때 효과적이다. 예를 들어, API 호출, 폼 처리, 상태 관리 등 비슷한 작업을 수행하는 로직을 추상화하여, 다양한 컴포넌트에서 재사용하고자 할 때 커스텀 훅이 유용하다.

💡 결합 가능성

  • 이 두 패턴은 함께 사용할 수 있다. 컨테이너 컴포넌트 내부에서 커스텀 훅을 사용해 비즈니스 로직을 더 모듈화할 수 있다. 즉, 컨테이너 컴포넌트는 상태 관리나 데이터 처리 같은 로직을 담당하면서, 공통 로직을 커스텀 훅으로 분리하여 여러 컨테이너 컴포넌트에서 재사용할 수 있다.

창시자도 버린 패턴?

최근에는 Presentational-Container 패턴의 필요성에 대해 의문이 제기되고 있다. Dan Abramov조차도 훅(Hooks)의 도입으로 이 패턴의 필요성을 재평가하고 있으며, 컴포넌트 설계를 내부 구현이 아닌 컴포넌트 인터페이스(API 디자인) 관점에서 바라보는 것이 더 중요하다는 의견이 많아지고 있다.

따라서, 이 패턴을 맹목적으로 따르는 대신, 컴포넌트 인터페이스 중심의 설계로 전환하는 것이 더욱 유연하고 유지보수에 효과적일 수 있다.


Presentational-Container 패턴의 현황

📌 컨테이너 컴포넌트를 굳이 만들 필요가 없는 이유

💡 간결한 코드 작성

컨테이너 컴포넌트를 따로 만들지 않고도, Hooks를 통해 컴포넌트 내부에서 비즈니스 로직을 바로 처리할 수 있다. 복잡한 로직도 커스텀 Hook으로 분리하여 사용할 수 있기 때문에, 컴포넌트 구조가 훨씬 간결해진다.

Hooks를 사용하여 컨테이너 컴포넌트를 대체하는 예시이다. 컴포넌트 안에서 비즈니스 로직을 처리하면서도 코드가 간결해진다.

import React, { useState, useEffect } from 'react';

const UserProfile = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // API 호출 예시
    fetchUserData().then(data => setUser(data));
  }, []);

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button>Logout</button>
    </div>
  );
};

💡 로직의 재사용성

컨테이너 컴포넌트는 특정 상태나 데이터를 관리하는 역할을 했지만, 이를 커스텀 Hook으로 대체하면 로직의 재사용성이 높아진다. 하나의 커스텀 Hook을 여러 프레젠테이셔널 컴포넌트에서 사용할 수 있어, 코드 관리가 더 수월해진다.

여러 컴포넌트에서 사용할 수 있는 커스텀 Hook을 통해 로직을 재사용하는 예시이다.

// useUser.js (커스텀 Hook)
import { useState, useEffect } from 'react';

const useUser = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData().then(data => setUser(data));
  }, []);

  return user;
};

// UserProfile.js
import React from 'react';
import useUser from './useUser';

const UserProfile = () => {
  const user = useUser();

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button>Logout</button>
    </div>
  );
};

💡 비즈니스 로직과 UI 로직의 명확한 분리

컨테이너 컴포넌트는 UI를 렌더링하지 않지만, 여전히 React 컴포넌트라는 특성상 UI 로직이 섞일 가능성이 있다. 반면에 Hooks는 함수 형태로 존재하므로 UI와 완전히 분리되어 UI와 비즈니스 로직이 혼재하지 않는다.

Hooks를 사용하여 비즈니스 로직과 UI 로직을 분리할 수 있다. 여기서는 useUser Hook으로 비즈니스 로직을 분리하고, UI 로직을 깔끔하게 유지한다.

// UserProfile.js
import React from 'react';
import useUser from './useUser';

const UserProfile = () => {
  const user = useUser();

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button>Logout</button>
    </div>
  );
};

📌 그럼에도 Presentational-Container 패턴을 사용해야 하는 이유

💡 복잡한 상태 관리

여러 프레젠테이셔널 컴포넌트가 동일한 상태를 공유하는 경우, 컨테이너 컴포넌트를 통해 상태를 집중적으로 관리하는 것이 유리하다.

대규모 애플리케이션에서 복잡한 상태를 관리해야 할 경우, 컨테이너 컴포넌트를 사용하는 예시이다.

// UserProfileContainer.js (Container Component)
import React, { useState, useEffect } from 'react';
import UserProfile from './UserProfile';

const UserProfileContainer = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData().then(data => setUser(data));
  }, []);

  const handleLogout = () => {
    // 로그아웃 처리
    setUser(null);
  };

  return <UserProfile user={user} onLogout={handleLogout} />;
};
// UserProfile.js (Presentational Component)
import React from 'react';

const UserProfile = ({ user, onLogout }) => {
  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={onLogout}>Logout</button>
    </div>
  );
};

export default UserProfile;

💡 깊은 컴포넌트 계층 구조

중첩된 컴포넌트에서 비즈니스 로직을 상위에서 하위로 전달할 때, 컨테이너 컴포넌트는 데이터를 한 곳에서 처리하고 하위 컴포넌트에 props로 전달함으로써 구조를 더 명확하게 유지한다.

// App.js (최상위 컨테이너)
import React from 'react';
import UserProfileContainer from './UserProfileContainer';

const App = () => {
  return (
    <div>
      <h1>Application</h1>
      <UserProfileContainer />
    </div>
  );
};

export default App;

💡 분업화된 개발 환경

UI 개발자와 로직 개발자가 분리된 대규모 팀에서는 Presentational-Container 패턴이 협업에 도움이 된다. UI와 로직이 분리되면, 각각의 변경 사항이 서로에게 영향을 주지 않고 작업할 수 있다.


📌 결론: Hooks로 대체할 수 있지만, 상황에 따라 결합이 필요하다.

Hooks는 컨테이너 컴포넌트를 대체할 수 있지만, 상황에 따라 두 패턴을 결합하는 것이 더 적절할 수 있다. 특히, 대규모 애플리케이션에서 상태 관리나 로직이 복잡한 경우에는 Presentational-Container 패턴이 필요할 수 있지만, 대부분의 경우에는 Hooks로 비즈니스 로직을 관리하는 것이 더 효율적이고 유연한 방법이다.

두 패턴을 결합하여 사용하는 예시이다. 컨테이너 컴포넌트에서 Hooks를 활용하여 로직을 간결하게 관리할 수 있다.

// UserProfileContainer.js (컨테이너 컴포넌트에서 Hooks 사용)
import React from 'react';
import UserProfile from './UserProfile';
import useUser from './useUser';

const UserProfileContainer = () => {
  const user = useUser();

  const handleLogout = () => {
    // 로그아웃 처리
    setUser(null);
  };

  return <UserProfile user={user} onLogout={handleLogout} />;
};

export default UserProfileContainer;

참고문헌

0개의 댓글