Container와 Presentational Component를 통한 상태 관리 개선

컨테이너(Container)프레젠테이셔널(Presentational) 컴포넌트 패턴을 통해 상태 관리와 UI 렌더링을 어떻게 분리할 수 있는지 설명합니다.



1. 기존 방식의 한계

1) 기존 방식 (App2.js)

// App2.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { plus, minus } from "./store/counterActions";

export default function App2() {
  // Redux 상태를 읽어오는 로직 (상태 관리)
  const number = useSelector((state) => state.counter.number); 

  // Redux 액션을 디스패치하는 로직 (상태 관리)
  const dispatch = useDispatch();

  return (
    <div className="App">
      {/* UI 렌더링 로직 */}
      <h1>Redux 상태 관리</h1>
      <h2>number: {number}</h2>

      {/* 버튼 클릭 시 상태를 업데이트하는 로직 (상태 관리) */}
      <button onClick={() => dispatch(plus())}>PLUS</button>
      <button onClick={() => dispatch(minus())}>MINUS</button>
    </div>
  );
}
  • 상태 관리 로직: useSelectoruseDispatch 훅을 통해 리덕스 상태를 관리하고 액션을 디스패치하는 부분입니다.
  • UI 렌더링 로직: 상태 값(number)을 화면에 출력하고, 버튼을 렌더링하는 부분입니다.


2) 문제점

기존 방식에서는 상태 관리와 UI 렌더링 로직이 하나의 컴포넌트에 혼재되어 있습니다. 이는 작은 프로젝트에서는 문제가 없을 수 있지만, 애플리케이션이 커지면서 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.


3) 기존 파일 트리

src/
│
├── store/                   # Redux 관련 폴더
│   ├── counterActions.js    # counter 액션 타입 및 액션 생성자 정의
│   ├── counterReducer.js    # counter 상태를 관리하는 리듀서
│
├── styles/                  # CSS 파일 폴더
│   └── Box.css              # Box 컴포넌트 스타일링
│
├── index.js                 # ReactDOM 및 Redux Provider 설정
└── App2.js                  # 메인 컴포넌트 (Box 컴포넌트와 Redux 상태 관리)

2. 컨테이너와 프레젠테이셔널 컴포넌트 패턴

상태 관리와 UI 렌더링을 분리하는 컨테이너/프레젠테이셔널 패턴을 사용해 보겠습니다.

  • 컨테이너 컴포넌트: 리덕스와 직접적으로 연결되어 상태를 관리하고 비즈니스 로직을 처리합니다. 그리고 프레젠테이셔널 컴포넌트에 필요한 데이터를 전달합니다.
  • 프레젠테이셔널 컴포넌트: UI 렌더링만을 담당하며, 상태나 비즈니스 로직에 대해서는 신경 쓰지 않습니다.


1) 변경된 Redux 파일 트리 구조

컨테이너 컴포넌트와 프레젠테이셔널 컴포넌트로 분리한 후의 파일 트리 구조는 다음과 같습니다.

src/
│
├── store/                   # Redux 관련 폴더
│   ├── counterActions.js    # counter 액션 타입 및 액션 생성자 정의
│   ├── counterReducer.js    # counter 상태를 관리하는 리듀서
│
├── containers/              # 컨테이너 컴포넌트 폴더
│   ├── BoxesContainer.js    # Box1~Box4의 상태 관리 및 액션 디스패치
│
├── styles/                  # CSS 파일 폴더
│   └── Box.css              # Box 컴포넌트 스타일링
│
├── index.js                 # ReactDOM 및 Redux Provider 설정
└── App4.js                  # 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트의 분리된 상태 관리


2) 컨테이너 컴포넌트의 역할

// containers/BoxesContainer.js
import { useDispatch, useSelector } from "react-redux";
import { Box1, Box2, Box3, Box4 } from "../App4";
import { minus, plus } from "../store/counterReducer";

export const Box1Container = () => {
  return <Box1 />;
};

export const Box2Container = () => {
  return <Box2 />;
};

export const Box3Container = () => {
  return <Box3 />;
};

export const Box4Container = () => {
  const number = useSelector((state) => state.counter.number);
  const dispatch = useDispatch();
  return (
    <Box4
      number={number}
      onPlus={() => dispatch(plus())}
      onMinus={() => dispatch(minus())}
    />
  );
};
  • 컨테이너 컴포넌트는 데이터 로직과 상태 관리를 담당하며, 프레젠테이셔널 컴포넌트에 데이터를 전달합니다. 이 컴포넌트에서는 상태를 가져오고(useSelector), 상태를 변경하기 위해 액션을 디스패치(useDispatch)합니다.


3) 프레젠테이셔널 컴포넌트의 역할

// App4.js
export const Box1 = () => {
  return (
    <div className="Box">
      <h2>Box1</h2>
      <Box2Container />
    </div>
  );
};

export const Box2 = () => {
  return (
    <div className="Box2">
      <h2>Box2</h2>
      <Box3Container />
    </div>
  );
};

export const Box3 = () => {
  return (
    <div className="Box3">
      <h2>Box3</h2>
      <Box4Container />
    </div>
  );
};

export const Box4 = ({ number, onPlus, onMinus }) => {
  return (
    <div className="Box4">
      <h2>Box4: {number}</h2>
      <button onClick={onPlus}>PLUS</button>
      <button onClick={onMinus}>MINUS</button>
    </div>
  );
};
  • 프레젠테이셔널 컴포넌트는 UI 렌더링만을 담당합니다. 상태 관리에 대해서는 전혀 신경 쓰지 않으며, 컨테이너 컴포넌트로부터 전달받은 props를 통해 UI를 업데이트합니다.


3. 컨테이너/프레젠테이셔널 패턴의 장점

1) 단일 책임 원칙 충족

  • 컨테이너 컴포넌트상태 관리비즈니스 로직을 담당하고,
  • 프레젠테이셔널 컴포넌트UI만을 담당하게 되어
  • 단일 책임 원칙을 충족할 수 있습니다.
  • 이를 통해 각 컴포넌트의 역할이 명확해지며, 코드의 유지보수성과 확장성이 크게 향상됩니다.

2) 코드의 가독성과 재사용성 향상

  • 프레젠테이셔널 컴포넌트는 상태와 독립적이기 때문에 재사용 가능하며,
  • UI만 변경하고 싶은 경우 쉽게 수정할 수 있습니다.
  • 또한, 비즈니스 로직과 UI 로직이 분리되어 코드의 가독성이 높아집니다.