[React] Presentational & Container 디자인 패턴

이룸·2026년 5월 17일

위클리페이퍼

목록 보기
16/17

React의 가장 큰 특징이자 장점 중 하나는 눈에 보이는 'UI를 표현하는 코드'와 해당 UI가 동작하게 하는 '기능'을 하나의 컴포넌트 안에서 선언할 수 있다는 것이다.

하지만 프로젝트가 커지고 요구사항이 복잡해지면서, 하나의 컴포넌트 안에 상태(State), 이벤트 핸들러, 마크업, 스타일링이 모두 뒤섞이게 된다. 코드가 섞여 있으면 파악하고 수정하기 어려워질 뿐만 아니라, 특정 UI 컴포넌트를 다른 곳에서 재사용하기도 매우 까다로워진다.

이러한 문제를 해결하고 '관심사의 분리'를 달성하기 위해 등장한 고전적이면서도 훌륭한 해답이 바로 Presentational & Container 디자인 패턴이다.


1. 패턴의 탄생 배경과 역사

과거 버전의 React에서는 클래스형 컴포넌트만이 상태(State)를 가질 수 있었다. 반면 상태가 필요 없고 오직 시각적인 표현(Presentation)에만 집중하는 단순한 컴포넌트는 함수형으로 작성했다.

이러한 기술적 한계와 특징을 바탕으로, 자연스럽게 역할을 나누는 패턴이 정립되었다.

  • 상태를 가지고 특정 기능을 수행하는 클래스 기반의 Container 컴포넌트
  • 상태 없이 전달받은 데이터만 화면에 그리는 함수 기반의 Presentational 컴포넌트

2. 두 컴포넌트의 특징

Presentational 컴포넌트와 Container 컴포넌트는 서로 상반되는 명확한 특징을 가지고 있다.

Presentational 컴포넌트 (UI 중심)

오직 '어떻게 보이는지'를 구현하는 데 집중한다. 코드의 대부분이 JSX 마크업과 스타일로 이루어져 있다.

  1. 순수하다 (Pure): 외부에서는 오로지 props를 통해서만 영향을 받으며, 컴포넌트 외부로 영향을 주지 않는다.
  2. 재사용성이 높다: 다른 맥락(Context)과 완벽히 독립적으로 동작하도록 설계된다. (예: 디자인 시스템의 Atom 컴포넌트)
  3. 데이터 로직 배제: UI를 위한 시각적인 상태 변화는 있을 수 있지만, 서버에서 온 데이터나 URL 같은 외부 상태가 어떻게 동적으로 변하는지에 대한 로직은 절대 포함하지 않는다.

Container 컴포넌트 (기능 중심)

'어떤 동작을 하고 어떤 데이터를 다룰지'를 구현한다. UI 마크업을 직접 작성하지 않고, 정보를 가공하여 Presentational 컴포넌트에게 props로 전달한다.

  1. 순수하지 않다 (Impure): 앱의 다른 맥락(전역 상태, 리액트 라이프사이클, 렌더링 환경 등)에 강하게 영향을 받는다.
  2. 외부 환경 의존: 어떤 환경에서 사용되는지에 따라 동작이 달라지거나 동작하지 않을 수 있다. 앱의 다른 상태에 영향을 주기도 한다.

3. 코드로 보는 적용 사례

현재 부트캠프에서 진행하는 미션 코드를 토대로 사례를 적용해보면 다음과 같다.

Container: ProductListContainer.jsx

데이터를 불러오고 로딩 상태를 관리하는 등 '로직'만 처리한 뒤, 렌더링은 ProductList에게 위임한다.

import { useState, useEffect } from 'react';
import ProductList from './ProductList'; // Presentational 컴포넌트

export default function ProductListContainer() {
  const [products, setProducts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  // 비즈니스 로직: 데이터 패칭
  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch('/api/products');
        const data = await response.json();
        setProducts(data);
      } catch (error) {
        console.error("데이터를 불러오는데 실패했다.", error);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchProducts();
  }, []);

  const handleProductClick = (id) => {
    console.log(`${id}번 상품 상세 페이지로 이동`);
  };

  return (
    // 상태값과 이벤트 핸들러만 props로 전달
    <ProductList isLoading="{isLoading}" onProductClick="{handleProductClick}" products="{products}"/>
  );
}

Presentational: ProductList.jsx

복잡한 데이터 패칭 로직 없이, 오직 props로 받은 데이터를 화면에 예쁘게 그려주는 역할만 수행한다.

// 오직 UI 렌더링에만 집중하는 순수 함수 컴포넌트
export default function ProductList({ products, isLoading, onProductClick }) {
  if (isLoading) return <div>상품 목록을 불러오는 중입니다...</div>;
  if (products.length === 0) return <div>등록된 상품이 없습니다.</div>;

  return (
    <ul className="product-grid">
      {products.map((product) => (
        <li 
          key={product.id} 
          className="product-card"
          onClick={() => onProductClick(product.id)}
        >
          <img src={product.thumbnail} alt={product.title} />
          <h3>{product.title}</h3>
          <p>{product.price}</p>
        </li>
      ))}
    </ul>
  );
}

4. 모던 리액트 환경 (React Hooks의 등장)

React 16 버전 이후, Hook(useState, useEffect 등)이 도입되면서 큰 변화가 생겼다. 이제는 함수형 컴포넌트에서도 상태와 라이프사이클을 다룰 수 있게 되었다.

이에 따라 Container 컴포넌트가 하던 역할을 Custom Hook으로 완벽하게 대체할 수 있게 되었다.
즉, 부모 컴포넌트가 물리적인 Container 역할을 하지 않더라도, 하나의 컴포넌트 안에서 Custom Hook을 호출하여 데이터와 로직을 가져오고 이를 Presentational 요소에 넘겨주는 방식으로 설계가 가능해졌다.

// Hook을 이용한 현대적인 Container 역할 수행
function ProductPage() {
  // 로직은 Hook으로 추상화
  const { products, isLoading, handleProductClick } = useProducts(); 
  
  return (
    <ProductList isLoading="{isLoading}" onProductClick="{handleProductClick}" products="{products}"/>
  );
}

5. 주의할 점과 결론

Presentational & Container 패턴이 훌륭하다고 해서 모든 컴포넌트를 무조건 두 개로 쪼개야 하는 것은 아니다. 때로는 분리하는 기준이 모호할 수 있으며, 억지로 나누다 보면 오버엔지니어링이 될 수 있다.

어떻게 나눌지 결정하기 어렵다면 굳이 나누지 않아도 괜찮다. 실제 제품과 시장은 끊임없이 변화하며, 때로는 몇 주 만에 버려질 코드를 작성하기도 한다.

가장 중요한 것은 코드를 작성하면서 어떤 변화가 일어나는지 관찰하는 것이다. 유지보수가 어렵고 재사용성이 떨어진다고 느껴지는 시점에 이 패턴이 주는 '관심사 분리'의 교훈을 적용해 보자. 수정하기 쉬운 코드를 작성하는 것이 결국 더 매력적인 제품을 만드는 가장 확실한 지름길이다.

profile
내 꿈은 풀스택 개발자

0개의 댓글