컨테이너 프레젠테이셔널 패턴 (Container-Presentational Pattern)
React와 같은 컴포넌트 기반의 프레임워크에서 UI와 로직을 분리하는 데 사용됩니다.
처음 이 패턴을 소개한 Dan Abramov는 2019년 기준으로 현재는 이 패턴을 사용하지 말라고 언급
내가 이 패턴을 유용하게 보았던 주된 이유는 이 패턴이 다른 관점의 컴포넌트로부터
복잡하고 stateful한 로직을 분리하게 해주었기 때문이다.
임의의 분리없이 Hook은 똑같은 일을 할 수 있다.
하지만 해당 패턴이 등장하게 된 이유와 방법은 현재에도 의미가 있으니 역사적인 자료로서 공부할 것.
리액트의 컴포넌트는 상태, DOM, 이벤트 등을 모두 관리 가능.
문제 제기
컴포넌트 간의 의존도가 높아지면 코드의 재사용 불가
컴포넌트 내에서도 추가적으로 레이어를 적절히 나눠 의존도를 낮춰주어야 할 필요가 있음
해결책 제시
로직과 view를 분리 ➡️ Presentational and Container 패턴
Container 컴포넌트의 주요 기능은 Presentational 컴포넌트에 데이터를 전달. 컴포넌트 자체는 화면에 아무것도 렌더링하지 않는다. 스타일시트도 포함하지 않는다.
Presentational 컴포넌트는 props를 통해 데이터를 받고 주요 기능은 받은 데이터를 화면에 표현.
스타일시트를 포함하는 반면, UI 변경을 위한 상태 외에는 상태를 갖지 않는다. prop을 통해 받은 데이터는 Presentational 컴포넌트에 의해 수정되지 않는다.
예시
import React from "react";
import DogImages from "./DogImages";
export default class DogImagesContainer extends React.Component {
constructor() {
super();
this.state = {
dogs: []
};
}
componentDidMount() {
fetch("https://dog.ceo/api/breed/labrador/images/random/6")
.then(res => res.json())
.then(({ message }) => this.setState({ dogs: message }));
}
render() {
return <DogImages dogs={this.state.dogs} />;
}
}
import React from "react";
export default function DogImages({ dogs }) {
return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}
대개 Container/Presentational 패턴은 React Hooks로 대체 가능. React 에 Hooks가 추가되면서 Container 컴포넌트 없이도 stateless 컴포넌트를 쉽게 만들 수 있게 되었다.
DogImagesContainer
컴포넌트에 있는 데이터 로드 코드를 아래와 같이 커스텀 훅으로 만들 수 있다.
훅은 비즈니스 로직과 뷰를 쉽게 분리할 수 있게 해주고. 불필요한 Container 래핑을 줄일 수 있게 해 준다.
예시 1 : 데이터 패치
export default function useDogImages() {
const [dogs, setDogs] = useState([])
useEffect(() => {
fetch('https://dog.ceo/api/breed/labrador/images/random/6')
.then(res => res.json())
.then(({ message }) => setDogs(message))
}, [])
return dogs
}
import React from "react";
import useDogImages from "./useDogImages";
export default function DogImages() {
const dogs = useDogImages();
return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}
예시 2 : 상태 관리
import React, { useState, useEffect } from 'react';
function useLightbox() {
const [isOpen, setIsOpen] = useState(false);
const [imageSrc, setImageSrc] = useState('');
const openLightbox = (src) => {
setImageSrc(src);
setIsOpen(true);
};
const closeLightbox = () => {
setImageSrc('');
setIsOpen(false);
};
return {
isOpen,
imageSrc,
openLightbox,
closeLightbox,
};
}
function Lightbox() {
const { isOpen, imageSrc, openLightbox, closeLightbox } = useLightbox();
return (
<div>
<h1>이미지 라이트 박스</h1>
<button onClick={() => openLightbox('image.jpg')}>이미지 열기</button>
{isOpen && (
<div className="lightbox">
<img src={imageSrc} alt="이미지" />
<button onClick={closeLightbox}>닫기</button>
</div>
)}
</div>
);
}