이미지 출처: 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 패턴은 리액트 컴포넌트 패턴이다. 이 패턴은 리액트에서 컴포넌트의 구조와 책임을 어떻게 나눌지에 대한 방식으로, 컴포넌트 간의 역할 분리에 중점을 둔 패턴이다.
이 패턴의 핵심 개념은 단일 책임 원칙(Single Responsibility Principle, SRP)에 있다. 단일 책임 원칙은 객체지향 프로그래밍에서 중요한 원칙 중 하나로, 각 클래스 또는 객체는 단 하나의 책임만 가져야 한다는 것이다. 이 원칙을 컴포넌트에 적용하게 되면, UI를 그리는 것과 비즈니스 로직을 처리하는 것을 명확하게 나누어야 한다는 생각이 나온다.
SRP에 대한 자세한 설명: 객체 지향 프로그래밍의 5원칙: S.O.L.I.D 원칙 (SOLID Principles)
Presentational Component는 말 그대로 프레젠테이션, 즉 UI를 담당하는 컴포넌트이다. 이 컴포넌트는 비즈니스 로직에 대해서는 전혀 관여하지 않으며, 순수하게 화면을 어떻게 렌더링할 것인가에만 초점을 맞춘다. 즉, UI의 외형과 동작을 관리하는 컴포넌트이다. 이런 컴포넌트는 주로 stateless (상태를 가지지 않는)이며, 데이터를 처리하거나 변경하는 기능은 없다.
Presentational Component의 장점 중 하나는 재사용성이다. 상태나 비즈니스 로직과 독립적이기 때문에, 다양한 상황에서 재사용할 수 있다. 만약 한 컴포넌트가 UI를 렌더링하는 것만 담당한다면, 그 컴포넌트를 여러 다른 컨텍스트에서도 사용할 수 있게 된다.
Container Component는 비즈니스 로직을 관리하는 컴포넌트이다. 이 컴포넌트는 상태를 관리하고, API를 호출하거나, 데이터를 처리하는 등 로직과 관련된 모든 작업을 담당한다. Container Component는 Presentational Component에게 데이터를 전달하고, 사용자가 상호작용할 때 그에 따른 로직을 처리하는 역할을 한다.
useState
, useEffect
같은 훅을 사용하여 상태를 변경하거나 관리한다.Container Component는 주로 비즈니스 로직과 데이터 처리에 집중하는데, 이는 상태 관리나 데이터베이스와의 상호작용, 사용자 입력 처리 등의 복잡한 로직을 UI와 분리하기 위함이다.
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 Component는 데이터를 props로만 전달받아야 하므로, 이 사이에 전달해야 할 데이터가 많아지면 컴포넌트 간 데이터 전달 과정이 복잡해질 수 있다. Container Component에서 처리된 상태와 로직을 Presentational Component로 전달하는 과정이 많아지면 컴포넌트 간 결합도가 높아질 수 있다.
로직과 UI를 각각 다른 컴포넌트로 분리하면서 컴포넌트의 수가 기하급수적으로 증가할 수 있다. 이는 대규모 프로젝트에서 팀이 체계적으로 관리할 수 있을 때만 유용하게 적용될 수 있으며, 그렇지 않을 경우에는 오히려 유지보수가 더 어려워질 수 있다.
이 패턴을 사용하는 이유는 주로 코드의 유지보수성과 재사용성 때문이다. 대규모 애플리케이션에서는 컴포넌트가 비대해지고, 이를 제대로 관리하지 않으면 코드가 스파게티처럼 얽히게 된다. 이 때 UI와 비즈니스 로직을 분리하면 컴포넌트의 역할이 명확해지며, 코드 수정이나 확장이 훨씬 쉬워진다.
또한, 재사용성도 중요한 이유 중 하나이다. 동일한 UI가 여러 곳에서 필요할 때 Presentational Component로 만들어두면, 다양한 페이지에서 해당 컴포넌트를 재사용할 수 있다. 이는 개발 속도를 높이고 코드 중복을 줄이는 데 도움을 준다.
Presentational-Container 패턴은 대규모 애플리케이션에서 특히 유용하다. 애플리케이션이 커질수록 컴포넌트의 역할이 복잡해지고, 유지보수나 테스트가 어려워질 수 있기 때문에 UI와 로직을 분리하는 것이 필수적이다. 또한, 재사용 가능한 UI 컴포넌트를 만들거나 상태 관리가 복잡한 경우에도 이 패턴을 사용하는 것이 좋다.
반면, 작은 프로젝트나 단순한 UI의 경우에는 Presentational-Container 패턴을 굳이 사용할 필요가 없다. 과도한 컴포넌트 분리는 오히려 프로젝트를 복잡하게 만들 수 있다.
가장 전통적인 방식은 Presentational Component와 Container Component를 각기 다른 폴더에 나누어 배치하는 방식이다. 이 방식은 두 역할을 물리적으로도 분리하는 것으로, 큰 규모의 프로젝트에서 관리가 용이하다.
/src
/components
/UserProfile
UserProfile.js (Presentational Component)
/containers
UserProfileContainer.js (Container Component)
Presentational Component와 Container Component를 같은 폴더 내에 배치하는 방식이다. 이렇게 하면 컴포넌트와 그에 대응하는 Container가 가까이 위치하게 되어 연관성을 더 쉽게 파악할 수 있다.
/src
/UserProfile
UserProfile.js (Presentational Component)
UserProfileContainer.js (Container Component)
Container Component와 Presentational Component를 역할에 따라 폴더 내에서 더 세분화하는 방식이다. 이 방식은 프로젝트가 더 커지고 복잡해질수록 컴포넌트를 체계적으로 관리할 수 있게 도와준다.
/src
/components
/UserProfile
/presentational
UserProfile.js (Presentational Component)
/container
UserProfileContainer.js (Container Component)
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;
폴더 구조를 선택할 때는 프로젝트의 규모, 팀의 협업 방식, 유지보수성을 고려해야 한다.
프로젝트의 복잡도와 팀의 관리 스타일에 따라 적절한 폴더 구조를 선택하는 것이 핵심이다.
최근에는 Presentational-Container 패턴의 필요성에 대해 의문이 제기되고 있다. Dan Abramov조차도 훅(Hooks)의 도입으로 이 패턴의 필요성을 재평가하고 있으며, 컴포넌트 설계를 내부 구현이 아닌 컴포넌트 인터페이스(API 디자인) 관점에서 바라보는 것이 더 중요하다는 의견이 많아지고 있다.
따라서, 이 패턴을 맹목적으로 따르는 대신, 컴포넌트 인터페이스 중심의 설계로 전환하는 것이 더욱 유연하고 유지보수에 효과적일 수 있다.
컨테이너 컴포넌트를 따로 만들지 않고도, 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를 렌더링하지 않지만, 여전히 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>
);
};
여러 프레젠테이셔널 컴포넌트가 동일한 상태를 공유하는 경우, 컨테이너 컴포넌트를 통해 상태를 집중적으로 관리하는 것이 유리하다.
대규모 애플리케이션에서 복잡한 상태를 관리해야 할 경우, 컨테이너 컴포넌트를 사용하는 예시이다.
// 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는 컨테이너 컴포넌트를 대체할 수 있지만, 상황에 따라 두 패턴을 결합하는 것이 더 적절할 수 있다. 특히, 대규모 애플리케이션에서 상태 관리나 로직이 복잡한 경우에는 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;
- Presentational and Container Components
- Container/Presentational Pattern
- Container-presentational pattern in React – why and how to use
- Hooks versus container components in React
- Atomic Design Pattern: Structuring Your React Application
- Hooks Pattern
- Why presentational/container components pattern is still important in 2021
- Goodbye presentational and container components?