본문 바로가기

웹 개발

디자인 패턴) Container/Presentational Pattern

이 글을 공부하면서 번역하고 정리한 글입니다. 


애플리케이션 로직과 뷰를 분리하여 관심사 분리(separation of concerns)를 강제하는 패턴 
웹 개발 디자인 패턴 분야에서 관심사(concern)이라는 용어는 소프트웨어 시스템 내에서 특정한 기능을 의미합니다. 예를 들어 사용자 인증 처리, 데이터 영속성, UI 컴포넌트 렌더링, 비즈니스 로직 관리 등과 같은 작업이 있습니다. 

관심사를 별개의 모듈로 처리하면 코드의 조직화, 유지보수성, 재사용성이 높아지는 장점이 있습니다. 이를 관심사 분리(separation of concerns)라고 부릅니다. 관심사를 분리함으로써, 개발자는 시스템의 특정 측면에 집중할 수 있고 모듈화를 장려하게 되며, 관심사별로 독립적인 작업이 가능하여 협업이 용이해집니다. 

리액트에서 관심사 분리를 강제하는 방법은 container/presentational pattern을 사용하는 것입니다. 이 패턴을 사용하여 로직과 뷰를 분리할 수 있습니다. 


6개의 강아지 사진을 가져와서 화면에 렌더링하는 애플리케이션을 만들었다고 가정해봅시다. 

이 상황에서 다음과 같이 관심사를 분리하고자 할 것입니다. 

  1. Presentational Components: 사용자에게 데이터를 어떻게 보여줄 것인지 결정하는 컴포넌트, 위 예시에서는 강아지 사진들을 랜더링하는 역할을 할 것입니다. 
  2. Container Components: 사용자에게 어떤 데이터를 보여줄 것인지 결정하는 컴포넌트, 위 예시에서는 강아지 사진을 가져오는 역할을 할 것입니다. 

강아지 이미지를 가져오는 것은 애플리케이션 로직만 다루면 되고, 이미지를 보여주는 것은 뷰만 다루면 됩니다. 


Presentational Component

presentational component는 props를 통해 데이터를 받아서 스타일을 적용하여 원하는 방식으로 데이터를 보여주는 기능을 합니다. 이때, 데이터를 수정하지는 않습니다.

 

위 예시를 다시 살펴봅시다.

// React

export default function DogImages({ dogs }) {
  return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}
// Vue3 + Typescript

<script setup lang="ts">
defineProps<{
	images: string[];
}>();
</script>

<template>
	<div>
		<img 
			v-for=(imgSrc, idx) in images
			:key=`img-${idx}`
			:src="imgSrc"
			:alt=`Dog${idx}`
		/>
	</div>
</template>

DogImages 컴포넌트는 presentational component입니다. props로 dogs에 강아지 이미지 리스트 데이터를 받아오고 map 함수로 이미지 리스트를 랜더링합니다. presentational component들은 주로 stateless합니다. 즉, UI에 필요한 상태 이외에 React에서는 state, Vue에서는 ref로 컴포넌트 자신만의 상태를 가지고 있지 않습니다. 그들이 prop으로 넘겨 받는 데이터는 presentational component에 의해 변경되지 않습니다. 

Presentational component는 container component로부터 데이터를 받습니다. 


Container Components

container component의 주요 기능은 그들이 포함하고 있는 presentational component에 데이터를 전달하는 것입니다. container component 자체는 데이터에 관여하는 presentational component 외의 다른 컴포넌트를 직접 랜더링하지 않고, 그렇기 때문에 스타일링 요소를 포함하고 있지도 않습니다. 

예시에서는 DogImages라는 presentational component에 강아지 이미지를 전달하고자 합니다. 이미지 데이터를 전달하기 전에 외부 API로부터 데이터를 가져와야 합니다. 따라서 화면에 이미지를 표현하기 위해서는 API로부터 데이터를 가져오고 DogImages로 데이터를 전달하는 container component를 만들어야 합니다. 

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} />;
  }
}

// Vue3 + TypeScript
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import DogImages from './DogImages.vue';

const dogs = ref<string[]>([]);

onMounted(async () => {
	await fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then(res => res.json())
      .then(({ message }) => {
	      images.value = message;
      });
});
</script>

<template>
	<div>
		<DogImages :images="dogs" />
	</div>
</template>

다음과 같이 presentational component와 container component 두 가지를 조합하면 view와 로직을 분리하여 애플리케이션을 운영할 수 있습니다. 


Hooks

많은 경우에, container/presentational 패턴은 React의 Hooks로 대체될 수 있습니다. Hooks의 도입으로 인해 개발자들이 container component에 state를 주입하지 않고 무상태성(statefulness)를 추가하기 쉬워졌습니다. 

DogImagesContainer 컴포넌트에 API로부터 데이터를 가져오는 로직을 넣는 대신, images를 가져와서 dogs 배열 데이터를 반환하는 custom hook을 만들 수 있습니다.

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;
}

위 hook을 사용하면 더이상 DogImagesContainer 컨테이터 컴포넌트가 데이터를 가져오고 DogImages presentational componet로 데이터를 보내지 않아도 됩니다. 대신 DogImages 컴포넌트에서 직접 이 hook을 사용해서 데이터를 가져올 수 있습니다!

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" />);
}

useDogImages hook에서 반환된 데이터를 사용하면 DogImages 컴포넌트에서는 데이터를 수정하지 않을 수 있기 때문에 DogImages hook을 사용하면 화면의 랜더링 부분과 로직 부분을 분리할 수 있습니다. 

따라서 Hook 역시 container/presentational 패턴과 같은 효과를 주게 됩니다. hook을 사용하면 container component로 presentational component를 감싸기 위해 추가적인 계층을 만들지 않을 수 있기도 합니. 


장단점

장점

  • 관심사의 분리를 권장합니다. presentational component는 UI에 관여하는 기능만 가지고, container component는 애플리케이션의 데이터와 상태에 관여하는 기능만 가질 수 있습니다.
  • presentational component는 데이터 변경 없이 데이터를 단순히 보여주는 역할을 하기 때문에 쉽게 재사용할 수 있습니다. 
  • presentational component는 애플리케이션 로직을 변경하지 않기 때문에 코드에 대한 이해가 없어도 쉽게 수정해서 사용할 수 있습니다. 만약 presentational component가 애플리케이션의 많은 부분에 재사용 되었다면 컴포넌트의 변경이 애플리케이션 전체에 한번에 반영될 수 있기 때문에 유지보수에도 좋습니다. 
  • presentational component에 전달한 데이터를 기반으로 컴포넌트가 어떤 형태로 랜더링될지 알 수 있기 때문에 presentational component는 테스트가 쉽습니다.

단점

  • Hook은 container/presentational 패턴보다 간단하게 랜더링 로직과 데이터 관리 로직을 분리하는 효과를 냅니다. 
  • 규모가 작은 애플리케이션에서 이 패턴은 과하게 느껴질 수 있습니다. 

'웹 개발' 카테고리의 다른 글

디자인 패턴) Prototype Pattern  (0) 2023.07.12
디자인 패턴) Provider Pattern  (0) 2023.07.12