React 디자인 패턴: Container/Presentational
2025년 3월 23일
목차
- 들어가면서
- Container/Presentational 패턴이란?
- Presentational 컴포넌트
- Container 컴포넌트
- Hooks 시대의 Container/Presentational 패턴
- 토마토들에서 사용했던 패턴은 Container/Presentational 패턴인가?
들어가면서
토마토들 회고를 진행하면서 폴더 구조에 대한 이야기를 한 적이 있습니다. 당시 프로젝트에서는 UI와 로직을 분리하기 위해 컴포넌트를 어떻게 구조화할지 고민했었고, 블로그 글을 참고해서 컨테이터 패턴을 적용했다고 언급했습니다.
그러나 최근 이력서 피드백을 받으면서 이것이 제대로 된 컨테이너 패턴이 아니라는 것을 깨닫게 되었습니다. 이번 글에서는 그 고민과 연결되는 React의 대표적인 디자인 패턴인 Container/Presentational 패턴에 대해 자세히 알아보겠습니다.
Container/Presentational 패턴이란?
이 패턴은 Dan Abramov가 Presentational and Container Components라는 제목의 글에서 처음 소개한 패턴으로, 애플리케이션 로직과 UI를 명확하게 분리하는 방법을 제시합니다. 이 패턴을 사용하면 다음과 같은 두 가지 타입의 컴포넌트로 관심사를 분리할 수 있습니다:
- Presentational 컴포넌트: 어떻게 보여지는지(how things look)에 집중
- Container 컴포넌트: 어떻게 동작하는지(how things work)에 집중
예시를 통해 Container/Presentational 패턴을 살펴볼까요? 사용자의 정보를 불러와 표시하는 컴포넌트를 생각해봅시다.
Presentational 컴포넌트
Presentational 컴포넌트는 다음과 같은 특징을 가집니다:
- 데이터를 props로만 받음: 외부로부터 데이터를 전달받아 표시합니다.
- UI에만 집중: 어떻게 보여질지에만 관심을 둡니다.
- 일반적으로 상태를 갖지 않음: 대부분 순수 함수형 컴포넌트로 구현됩니다(UI 관련 상태 제외).
- 데이터 변경 방법을 모름: 데이터 변경은 props로 전달받은 콜백 함수를 통해서만 수행합니다.
- 재사용성이 높음: 다양한 Container와 함께 사용될 수 있습니다.
// UserProfileView.jsx - Presentational 컴포넌트
import './UserProfile.css';
const UserProfileView = ({ user, onEditClick }) => (
<div className='user-profile'>
<img src={user.avatar} alt={user.name} className='user-profile__avatar' />
<div className='user-profile__info'>
<h2 className='user-profile__name'>{user.name}</h2>
<p className='user-profile__email'>이메일: {user.email}</p>
<p className='user-profile__date'>
가입일: {new Date(user.createdAt).toLocaleDateString()}
</p>
<button className='user-profile__edit-button' onClick={onEditClick}>
프로필 수정
</button>
</div>
</div>
);
export default UserProfileView;
UesrProfileView
컴포넌트는 사용자 프로필을 어떻게 표시할지에만 집중하고 있습니다. 데이터를 어디서 가져오는지, 어떻게 처리하는지에 대한 로직은 전혀 포함하지 않습니다.
Container 컴포넌트
Container 컴포넌트는 다음과 같은 특징을 가집니다:
- 데이터 제공에 집중: 데이터를 가져오고 처리하는 로직을 담당합니다.
- 상태 관리: 데이터와 관련된 상태를 관리합니다.
- API 호출 및 데이터 변환: 외부 API와 통신하고 데이터를 가공합니다.
- 직접적인 UI 렌더링 없음: 대신 Presentational 컴포넌트에 데이터를 전달합니다.
- 라이프사이클 메소드/효과 활용: 컴포넌트 생명주기에 따른 데이터 처리를 관리합니다.
import { Component } from 'react';
import UserProfileView from './UserProfileView';
class UserProfileContainer extends Component {
state = {
user: null,
loading: true,
error: null,
};
componentDidMount() {
// 데이터 페칭 로직
fetch(`https://api.example.com/users/${this.props.userId}`)
.then((response) => response.json())
.then((data) => {
this.setState({ user: data, loading: false });
})
.catch((error) => {
this.setState({ error, loading: false });
});
}
handleEditClick = () => {
// 프로필 편집 페이지로 이동 로직
const { user } = this.state;
window.location.href = `/edit-profile/${user.id}`;
};
render() {
const { user, loading, error } = this.state;
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러 발생: {error.message}</div>;
if (!user) return <div>사용자를 찾을 수 없습니다</div>;
// Presentational 컴포넌트에 데이터와 이벤트 핸들러 전달
return <UserProfileView user={user} onEditClick={this.handleEditClick} />;
}
}
export default UserProfileContainer;
UserProfileContainer
컴포넌트는 사용자 데이터를 가져오고, 상태를 관리하며, 이벤트 핸들러를 정의하는 등의 로직을 담당합니다. 실제 UI 렌더링은 UserProfileView
컴포넌트에 위임하고 있습니다.
지금까지 이야기한 두 컴포넌트를 다이어그램으로 표시하면 다음과 같습니다:

Hooks 시대의 Container/Presentational 패턴
React 16.8에서 Hooks가 도입되면서 Container/Presentational 패턴에도 변화가 생겼습니다. 이전에는 클래스 컴포넌트로 Container를 구현하고 함수형 컴포넌트로 Presentational을 구현하는 경우가 많았지만, Hooks를 사용하면 함수형 컴포넌트만으로도 상태 관리와 사이드 이펙트 처리가 가능해졌습니다.
커스텀 Hook을 활용한 로직 분리
import { useState, useEffect } from 'react';
export const useUser = (userId) => {
const [state, setState] = useState({
user: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setState({ user: data, loading: false, error: null });
} catch (error) {
setState({ user: null, loading: false, error });
}
};
fetchUser();
}, [userId]);
return {
...state,
refreshUser: () => {
setState((prev) => ({ ...prev, loading: true }));
},
};
};
Hooks로 인한 변화: 단일 컴포넌트 접근법

Hooks를 사용하면 로직을 커스텀 훅으로 추출하고, 컴포넌트 자체는 UI 표현에 집중할 수 있습니다.
import { useUser } from './useUser';
const UserProfile = ({ userId = 1 }) => {
const { user, loading, error } = useUser(userId);
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러 발생: {error.message}</div>;
if (!user) return <div>사용자를 찾을 수 없습니다</div>;
const handleEditClick = () => {
window.location.href = `/edit/${user.id}`;
};
return (
<div className='user-profile'>
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>이메일: {user.email}</p>
<p>가입일: {new Date(user.createdAt).toLocaleDateString()}</p>
<button onClick={handleEditClick}>프로필 수정</button>
</div>
);
};
export default UserProfile;
그렇다면 Container/Presentational 패턴은 더 이상 필요 없을까요? 그렇지 않습니다. Hooks가 많은 경우에 이 패턴을 대체할 수 있지만, 다음과 같은 상황에서는 여전히 Container/Presentational 패턴이 유용합니다:
- 큰 규모의 애플리케이션: 팀 규모가 크고 역할이 명확히 구분된 경우
- 높은 재사용성이 필요한 컴포넌트: 다양한 데이터 소스와 함께 사용될 UI 컴포넌트
- 복잡한 로직과 UI를 포함하는 컴포넌트: 분리가 코드 가독성을 높이는 경우
토마토들에서 사용했던 패턴은 Container/Presentational 패턴인가?
그럼 지금까지 다뤄본 내용을 바탕으로, 이전에 작성했던 토마토들 회고에서 언급한 구조가 Container/Presentational 패턴이 맞는지 살펴보겠습니다.
패턴 분석
회고에서 설명한 containers
폴더의 사용 방식을 살펴보면:
import { fetchActivityContestAbstractWith } from '@/lib/fetchActivityAbstractWith';
import Activity from '@/containers/activity/Activity';
export default async function Page({ searchParams }: PageProps) {
const filters = searchParams.filters?.split(',').filter(Boolean) || [];
const sort = searchParams.sort || '관련도순';
const { data: activitiesContests, error } =
await fetchActivityContestAbstractWith({
filters,
sort,
mainCategory: '대외활동',
});
if (error) {
// 에러 핸들링 로직
}
return <Activity activitiesContests={activitiesContests || []} />;
}
위와 같은 구조를 가지고 있는 것을 확인할 수 있습니다. 이는 전통적인 Container/Presentational 패턴과는 미묘하게 다릅니다. 정확히 말하자면, Next.js의 페이지 컴포넌트와 컨테이너 컴포넌트의 하이브리드 패턴이라고 볼 수 있습니다.
차이점
-
역할 분배의 차이:
- 전통적인 패턴: Container 컴포넌트가 데이터 페칭, 상태 관리, 이벤트 핸들링을 모두 담당
- 토마토들:
Page
컴포넌트(Next.js 라우팅 컴포넌트)가 데이터 페칭을 담당하고,Activity
컴포넌트는 UI 렌더링과 상태 관리를 담당
-
데이터 흐름:
- 전통적인 패턴: Container가 데이터를 가져오고 Presentational에 전달
- 토마토들:
Page
가 데이터를 가져와Activity
에 전달 (Container가 Presentational 역할만 하는 것이 아님)
토마토들에 적용된 패턴의 정체
여러 가지 자료를 찾아본 결과, 제가 사용한 패턴은 페이지 기반 데이터 페칭 패턴(Page-based Data Fetching Pattern) 또는 라우트 중심 구성 패턴(Route-centric Organization Pattern) 이라고 볼 수 있습니다.
- 라우트 컴포넌트(Page): 데이터 페칭, URL 파라미터 처리, 에러 핸들링 등 라우팅 관련 로직 담당
- 컨테이너 컴포넌트(Activity): UI 구조화, 상태 관리, 사용자 인터랙션 처리 담당
- 프레젠테이셔널 컴포넌트: UI 렌더링만 담당
패턴 사용에 대한 분석
토마토들 프로젝트에서 이 패턴을 사용한 이유는 크게 두 가지였습니다.
Next.js의 서버 컴포넌트 활용이 첫 번째 이유였습니다. 서버에서 데이터를 가져오는 것이 성능상 유리하다고 판단했고, 클라이언트-서버 컴포넌트 경계를 명확히 하고 싶었기 때문입니다.
코드 구조화의 명확성이 두 번째 이유였습니다. 페이지 레벨의 로직과 컴포넌트 레벨의 로직을 분리하고 싶었고, 폴더 구조를 통해 관심사를 명확히 구분하고 싶었습니다.
하지만 이 패턴이 정말 필요한 선택이었는지 다시 생각해보면 몇 가지 문제점이 있었습니다.
과도한 분리가 첫 번째 문제였습니다. 프로젝트 규모가 작았음에도 불구하고 불필요하게 복잡한 구조를 만들었고, 실제로는 대부분의 컴포넌트가 단순한 렌더링 작업만 수행했습니다.
Next.js의 기본 패턴과의 불일치가 두 번째 문제였습니다. Next.js는 이미 페이지 기반 라우팅을 제공하며 서버 컴포넌트를 통한 데이터 페칭을 권장하는데, 추가적인 컨테이너 계층이 오히려 코드의 복잡성을 증가시켰습니다.
유지보수 비용이 세 번째 문제였습니다. 컴포넌트 간 데이터 흐름을 추적하기 어려웠고, 새로운 기능 추가 시 여러 파일을 수정해야 하는 번거로움이 있었습니다.
당시에는 "좋은 구조"를 만들고 싶은 욕심에 불필요한 복잡성을 추가했던 것 같습니다. 프로젝트의 규모와 복잡성을 고려했을 때, 더 단순한 구조로 시작하고 필요에 따라 점진적으로 개선하는 것이 더 나은 접근 방식이었을 것 같습니다.
결론
지금까지 React의 Container/Presentational 패턴에 대해 자세히 알아보았습니다. 이 패턴은 Dan Abramov가 제안한 것으로, 애플리케이션의 로직과 UI를 명확하게 분리하는 방법을 제시합니다. Presentational 컴포넌트는 UI 표현에만 집중하고, Container 컴포넌트는 데이터 처리와 상태 관리에 집중하는 것이 핵심입니다.
하지만 제가 이전에 토마토들 프로젝트에서 사용했던 패턴은 이와는 달랐습니다. Next.js의 특성을 활용한 라우트-컨테이너 패턴이었는데, 이를 Container/Presentational 패턴이라고 잘못 이해하고 있었습니다.
이번 회고를 통해 기술적 개념을 정확히 이해하고 올바르게 적용하는 것이 얼마나 중요한지 깨달았습니다. 특히 디자인 패턴과 같은 핵심 개념을 제대로 파악하지 못한 채 사용하면, 팀원들에게 잘못된 정보를 전달할 뿐만 아니라 코드의 진짜 의도도 왜곡될 수 있다는 점을 알게 되었습니다. 앞으로는 기술적 용어와 개념을 더 꼼꼼히 학습하고, 정확한 표현을 사용하는 데 더 신경 쓰려고 합니다.