React 애플리케이션에서 상태관리는 어려운 작업이다. 특히 루트컴포넌트에서 깊게 중첩된 컴포넌트로 데이터를 전달해야 할때 많은 경우 코드가 지나치게 복잡해지고 불필요한 중복이 발생하게 된다.
이러한 문제(prop drilling)를 해결하기 위해 컴포넌트트리 아래로 데이터를 전달할 때 다른 접근 방식으로 고려할 필요가 있는데 외부 라이브러리에 의존하지 않고도 React의 기능으도 해결이 가능하다.
( React의 고유 기능을 고려하지 않고 Context API 또는 Redux를 선택하는 방법도 있지만 React에 내장된 상태관리 옵션을 이용함으로써 타사 라이브러리에 대한 불필요한 의존을 피할 수 있다.)
이글은 아래 링크를 변역하고 편집하였다.
Prop drilling effect란 깊게 중첩된 컴포넌트에 데이터를 전달하기 위해 여러 계층의 중첩된 하위 컴포넌트를 통해 데이터를 전달하는 방식을 의미한다.
그러나 이러한 접근 방식은 데이터 전달과 관련된 대부분의 중간 컴포넌트가 실제로 이 데이터를 필요로 하지 않기때문에 불필요한 코드이다. (이러한 컴포넌트는 데이터를 의도한 대상 컴포넌트로 전송하기 위한 통로로만 사용되기 때문이다.)
결과적으로 컴포넌트의 재사용성이 감소하고 앱 성능이 저하되는 등 심각한 문제가 발생할 수 있다.
Context API를 사용하면 기본적으로 상태/데이터를 컨텍스트 공급자로 래핑하여 여러 컴포넌트에 브로드캐스팅할 수 있다.
그러면 value 속성을 사용하여 상태를 컨텍스트 제공자에게 전달할 수 있게되고 하위컴포넌트는 필요할 때 컨텍스트소비자 또는 useContext hook를 사용하여 상태에 액세스할 수 있다.
React의 Context API는 컴포넌트 트리의 각 레벨을 통해 props를 전달할 필요 없이 컴포넌트 간 데이터 공유를 허용하는 강력한 기능이지만 애플리케이션의 성능과 유지 관리 가능성에 영향을 미칠 수 있는 몇 가지 제한 사항이 있다.
성능 문제
컨텍스트 값을 업데이트하면 변경 사항이 컴포넌트와 관련이 없더라도 컴포넌트를 소비할 때 불필요한 다시 렌더링이 발생할 수 있다.
이는 특히 대규모 컴포넌트트리에서 성능에 영향을 미칠 수 있다. 따라서 이 문제를 완화하려면 컨텍스트 값을 업데이트하는 시기와 방법을 신중하게 고려해야 한다.
이는 전체 앱 상태에 하나의 컨텍스트가 사용되어 여기저기서 많은 재렌더링이 발생하는 경우 특히 문제가 될수 있다.
테스트 복잡성
컨텍스트를 사용하는 컴포넌트 테스트는 props 기반 상태 관리를 사용하는 컴포넌트 테스트에 비해 더 복잡할 수 있다.
테스트 중에 올바른 컨텍스트 값을 모의하거나 제공하려면 추가 설정이 필요할 수 있으며 단위 테스트가 더 번거로울 수 있다.
남용 가능성
Context API의 단순성과 사용 편의성으로 인해 남용이 발생하여 컴포넌트간의 과도한 결합이 발생하고 코드베이스를 이해하고 유지 관리하기가 더 어려워질 수 있다.
특히 관련되지 않은 문제에 대해 공유 상태의 모든 부분에 대해 컨텍스트를 과도하게 사용하면 코드베이스가 덜 모듈화되고 추론하기가 더 어려워질 수 있다.
유형 안전성 부족
컨텍스트 값은 기본적으로 유형 검사를 하지 않는다. 즉 잘못된 사용이나 컨텍스트값 형태의 변경이 컴파일러나 개발 도구에서 발견되지 않을 수 있다.
이로 인해 런타임 오류 및 디버깅 문제가 발생할 수 있다.
확장 문제
Context API는 대규모 앱의 경우 확장 및 유지 관리가 어려울 수 있다. 여러 컨텍스트의 종속성과 업데이트를 관리해야 하므로 코드 중복과 불일치가 발생할 수 있다.
개발 도구 또는 미들웨어의 부재
Redux와 같은 전용 상태 관리 솔루션과 달리 Context API는 상태 관리를 디버깅, 테스트 또는 최적화하는 데 도움이 되는 개발 도구나 미들웨어를 제공하지 않는다.
React 애플리케이션에서 복합 컴포넌트를 사용하면 다음과 같은 이점이 있다.
재사용성
복합컴포넌트 내의 개별컴포넌트는 함께 작동하도록 설계되었으며 코드나 기능을 복제할 필요 없이 애플리케이션의 다른 부분에서 쉽게 재사용할 수 있다.
이를 통해 코드베이스의 전반적인 복잡성을 줄이고 시간이 지남에 따라 애플리케이션을 더 쉽게 유지 관리하고 업데이트할 수 있다.
유연성
복합 컴포넌트는 깔끔하고 간결한 API를 유지하면서 복잡한 UI 컴포넌트를 구축하는 유연한 방법을 제공한다.
이 패턴을 사용하면 컴포넌트에 너무 많은 소품을 전달하지 않고도 고도로 사용자 정의 가능하고 재사용 가능한 컴포넌트를 만들 수 있다.
우려 사항 분리
복합컴포넌트를 사용하면 각 하위컴포넌트가 특정 기능을 담당하므로 전체 시스템에 영향을 주지 않고 개별컴포넌트를 더 쉽게 유지 관리하고 업데이트할 수 있다.
직관적인 API
복합컴포넌트 패턴은 관련 하위컴포넌트를 상위컴포넌트 내에 캡슐화함으로써 다른 개발자가 쉽게 이해하고 사용할 수 있는 명확하고 직관적인 API를 제공한다.
향상된 사용자 정의
이 패턴을 사용하면 개발자는 하위컴포넌트를 쉽게 교체하거나 확장하여 향상된 사용자 정의를 얻을 수 있다.
일관성
복합컴포넌트패턴은 복합컴포넌트와 상호 작용하기 위한 일관된 인터페이스를 제공함으로써 애플리케이션의 사용자 경험을 개선하는 데 도움이 될 수 있다.
Less Prop Drilling
복합 컴포넌트에서는 props를 통해 상태를 전달하는 대신 요소를 부모 요소에 자식으로 전달한다. 이를 통해 부모는 암시적 상태를 관리하고 prop 드릴링의 필요성을 줄일 수 있다.
유지 관리성
복합컴포넌트는 React 애플리케이션 내에서 상태를 공유하는 보다 유연한 방법을 제공하므로 React 애플리케이션에서 복합컴포넌트를 사용하면 앱을 더 쉽게 유지 관리하고 실제로 디버깅할 수 있다.
복합컴포넌트에서는 props를 통해 상태를 전달하는 대신 요소를 부모 요소에 하위 요소로 전달한다. 이를 통해 부모는 암시적 상태를 관리할 수 있다.
Compound Components
는 부모컴퍼넌트로 자식컴퍼넌트 전체를 감싸서 유저에게 지정시키는 패턴이다. 늘어난 데이터를 Props를 통해 전달하는 대신 하위컴포넌트로 전달한다.
이렇게하면 상위컴포넌트와 하위컴포넌트간의 응집도를 높이고 보다 직관적인 인터페이스를 제공할 수 있다.
아래의 HTML 예제를 보면,
<select>
<option value="react">React</option>
<option value="vue">Vue</option>
<option value="angular">Angular</option>
</select>
<select>
는 선택상자의 큰틀을 나타내고 <option>
은 그안에 표시되는 항목을 나타낸다.
HTML에서는 <option>
을 <select>
의 자식으로 전달할 수 있습니다. <select>
요소는 상태를 관리하고 자식요소로 전달된 <option>
이 선택상자의 항목으로 취급되는것을 알수 있다.
React 에서도 매우 비슷하며 부모컴퍼넌트가 암묵의 상태(state)를 관리하고 자식컴퍼넌트를 그 요소로서 건네줄 수 있다. 이것이 바로 Compound Components이다.
예로 Card
컴포넌트를 구현해 보자. Card
컴퍼넌트는 Title
, Body
, Body
내부에 Item
을 배열로서 받아들여 표시할 수 있다고 가정하자.
우선 Compound Component
패턴을 사용하지 않고 Props
를 건네 가는 방법으로 구현을 생각해보자.
Card
컴퍼넌트를 Props
를 통해 받아 구현한 코드는 아래와 같다.
import React, { useState } from "react";
interface CardItem {
id: number;
text: string;
}
interface CardProps {
title: string;
items: CardItem[];
}
export const Card: React.FC<CardProps> = ({ title, items }) => {
const [isShow, setIsShow] = useState(false);
const handleClick = () => setIsShow(show => !show);
return (
<div>
<h2>{title}</h2>
<div>
<button onClick={handleClick}>Show items</button>
{isShow && (
<div>
{items.map(item => (
<div key={item.id}>{item.text}</div>
))}
</div>
)}
</div>
</div>
);
};
Props
를 통해 items
, title
을 받고 표시한다. 또 state를 포함한 로직도 컴포넌트에 구현되어 있다.
그럼 Card
컴퍼넌트를 이용하는 컴퍼넌트를 구현해 보자.
import type { NextPage } from "next";
import { Card } from "../components/Card";
const Home: NextPage = () => {
const cardItems = [
{ id: 1, text: "부모와 자식으로 UI와 논리 분리"},
{ id: 2, text: "Props 대신 자식컴포넌트로 전달"},
{ id: 3, text: "컴포넌트의 재사용성 향상"},
];
return (
<div>
<Card title="Props pattern" items={cardItems} />
</div>
);
};
export default Home;
코드양은 많지않지만 사용하는 측에선 직관적인 인터페이스가 되어 있지 않아보인다. 또한 Props
를 통해 호출하고 있기 때문에 Props
에 변경이 있으면 Card
컴퍼넌트 전체가 렌더링되는 문제가 있다.
다음으로 Compound Component
패턴을 이용하는 경우를 생각해 보자.
마찬가지로 Card
컴포넌트에서부터 구현해보면,
import { useState } from "react";
interface CardProps {
children: React.ReactNode;
}
interface CardComposition {
Title: React.FC<CardTitleProps>;
Body: React.FC<CardBodyProps>;
Item: React.FC<CardItemProps>;
}
interface CardTitleProps {
children: React.ReactNode;
}
interface CardBodyProps {
children: React.ReactNode;
}
interface CardItemProps {
children: React.ReactNode;
}
export const Card: React.FC<CardProps> & CardComposition = ({ children }) => {
return <div>{children}</div>;
};
const CardTitle: React.FC<CardTitleProps> = ({ children }) => {
return <h2>{children}</h2>;
};
const CardBody: React.FC<CardBodyProps> = ({ children }) => {
const [isShow, setIsShow] = useState(false);
const handleClick = () => setIsShow(show => !show);
return (
<div>
<button onClick={handleClick}>Show items</button>
{isShow && <div>{children}</div>}
</div>
);
};
const CardItem: React.FC<CardItemProps> = ({ children }) => {
return <div>{children}</div>;
};
Card.Title = CardTitle;
Card.Body = CardBody;
Card.Item = CardItem;
interface
선언으로 인해 코드가 조금 늘어나긴했지만. state
를 보관하고 유지하는 stateful
인 컴퍼넌트와 UI의 표시만 담당하는 stateless
컴퍼넌트로 분할되어 정리할 수 있다.
다음으로 Card
컴포넌트를 이용하는 컴포넌트를 구현해 보자.
import type { NextPage } from "next";
import { Card } from "./components/Card";
const Compound: React.FC = () => {
const cardItems = [
{ id: 1, text: "부모와 자식으로 UI와 논리 분리"},
{ id: 2, text: "Props 대신 자식 컴포넌트로 전달"},
{ id: 3, text: "컴포넌트의 재사용성 향상"},
];
return (
<div>
<Card>
<Card.Title>Component compound pattern</Card.Title>
<Card.Body>
{cardItems.map((cardItem) => (
<Card.Item key={cardItem.id}>{cardItem.text}</Card.Item>
))}
</Card.Body>
</Card>
</div>
);
};
Compound Components
는 데이터를 Props
통해서 건네주는 것이 아니라 자식컴퍼넌트로서 건네주는 부모-자식 관계로 구축되는 컴퍼넌트로 매우 유효한 디자인 패턴이다.
상위컴포넌트와 하위컴포넌트 간의 응집도를 높이고 직관적인 인터페이스를 제공할 수 있으며 유연성과 성능 향상도 기대할 수 있다.