합성 컴포넌트란 소프트웨어 개발에서 재사용이 가능한 구성 요소를 만들기 위해 여러 개의 다른 컴포넌트를 조합하는 디자인 패턴이다. 복합적인 기능을 가진 큰 컴포넌트를 작은 단위의 컴포넌트들로 구성함으로써 코드의 유지보수성과 확장성을 높일 수 있다.
합성 컴포넌트 패턴에서는 컴포넌트들이 계층 구조로 구성된다. 각 컴포넌트는 하위 컴포넌트들을 가지며, 최상위 컴포넌트는 하위 컴포넌트들의 조합으로 이루어진다. 이로 인해 단일 컴포넌트를 사용하는 것보다 더 복잡하고 다양한 기능을 제공할 수 있다.
예를 들어, 그래픽 사용자 인터페이스(GUI)를 개발하는 경우, 버튼, 텍스트 상자, 체크박스 등의 작은 컴포넌트들을 만들어두고, 이러한 작은 컴포넌트들을 조합하여 더 큰 형태의 윈도우 또는 팝업 컴포넌트를 만들 수 있다. 이렇게 함으로써 유사한 기능을 가진 컴포넌트들을 중복으로 작성할 필요 없이 재사용성이 증가하고 코드의 가독성이 높아진다.
하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 컴포넌트 패턴을 의미한다.
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
간단한 예시로 html의 select를 볼 수 있는데, select는 select와 option 태그의 조합으로 이루진다.
select와 option은 각각 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 된다.
UI와 로직을 분리하는 것은 소프트웨어 개발에서 중요한 원칙 중 하나인 "단일 책임 원칙"을 따르기 위한 접근 방법이다. 합성 컴포넌트 패턴은 이를 구현하는 데 도움이 되며, 이를 통해 컴포넌트들 간의 의존성을 줄이고 독립성을 강화할 수 있다.
<Counter title="카운터" initValue={0} minimum={0} maximum={100} />
이러한 Counter 컴포넌트가 있다. 이 Counter 컴포넌트를 합성컴포넌트로 리팩토링 해보았다.
이런식으로 로직과 UI를 분리하여 합성컴포넌트로 구현할 수 있다.
import React, { createContext, useContext, useState } from "react";
import CounterButton from "./pages/UI/Button";
import CounterTitle from "./pages/UI/Title";
import CounterStatus from "./pages/UI/CounterStatus";
interface Props {
children: React.ReactNode;
initValue: number;
minimum?: number;
maximum?: number;
}
const compositeContext = createContext({
counter: 0,
counterPlus: () => {},
counterMinus: () => {},
});
export const Counter = ({ children, initValue, minimum, maximum }: Props) => {
const [counter, setCounter] = useState(initValue);
const counterPlus = () => {
setCounter((prev) => {
if (maximum === undefined) {
return prev + 1;
} else {
return prev < maximum ? prev + 1 : prev;
}
});
};
const counterMinus = () => {
setCounter((prev) => {
if (minimum === undefined) {
return prev - 1;
} else {
return prev > minimum ? prev - 1 : prev;
}
});
};
return (
<compositeContext.Provider value={{ counter, counterPlus, counterMinus }}>
{children}
</compositeContext.Provider>
);
};
Counter.Title = CounterTitle;
Counter.Button = CounterButton;
Counter.Status = CounterStatus;
export const useCounter = () => useContext(compositeContext);
ContextApi를 사용하여 상태와 로직들을 중앙에서 관리하고 UI와 분리시켰다.
import { useCounter } from "../../CompositContext";
interface Props {
children: React.ReactNode;
type: "increment" | "decrement";
}
export default function CounterButton({ children, type }: Props) {
const { counterPlus, counterMinus } = useCounter();
return (
<button onClick={type === "increment" ? counterPlus : counterMinus}>
{children}
</button>
);
}
import { useCounter } from "../../CompositContext";
export default function CounterStatus() {
const { counter } = useCounter();
return <p>{counter}</p>;
}
interface Props {
children: React.ReactNode;
}
export default function CounterTitle({ children }: Props) {
return <h2>{children}</h2>;
}