Composition Pattern 을 React Component에 적용해보자

종현 김
13 min readApr 14, 2023

composition pattern이란?

OOP(Object Oriented Programing: 객체지향 프로그래밍)에서 객체간의 관계를 구성하는 패턴 중 하나입니다. 합성을 통해서 객체를 만들어내는 패턴으로써 객체간의 구조를 결정하는데 상속보다는 객체를 조합하여 보다 유연하고 확장가능한 형태로 구조를 만들어 낼 수 있습니다.

React는 이미 Composition Pattern을 실현하고 있습니다!
> React는 강력한 합성 모델을 가지고 있으며, 상속 대신 합성을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋습니다.
https://ko.reactjs.org/docs/composition-vs-inheritance.html

이러한 패턴은 우리가 이미 리액트 컴포넌트를 만들때 사용하고 있습니다. 간단한 예제로 살펴볼 수 있죠.

function CompositionComponent() {
return (
<div>
<p> Hello Composition Pattern! </p>
</div>
)
}

위 코드를 살펴보면 div 의 children으로 p 가 존재하는 계층구조로 된 컴포넌트인걸 확인할 수 있습니다. 위 컴포넌트에서는 자신을 소개하는 Hello Composition Pattern! 이 정해져있지만 우리는 어떠한 텍스트가 들어올지 예측할 수 없는 경우가 많습니다. 그럴땐 이렇게 수정하곤 합니다.

interface CompositionComponentProps {
introduce: string
}

function CompositionComponent({introduce}: CompositionComponentProps) {
return (
<div>
<p> {introduce} </p>
</div>
)
}

이렇게 수정된 코드는 밖에서 introduce 값을 props 로 받아서 사용할 수 있습니다. 하지만 그 다음으로 introduce 값이 꼭 p 태그에서만 렌더링 되라는 법은 없습니다. 아에 컴포넌트를 받을 수 있게 수정할 수 있겠죠.

interface CompositionComponentProps {
introduceChild: ReactElement;
}

function CompositionComponent({introduceChild}) {
return (
<div>
{introduceChild}
</div>
)
}
function Introduce() {
return (
<CompositionComponent>
<text>Hello It is a text HTML tag!</text>
</CompositionComponent>
)
}

이제는 어떠한 ReactElement라도 받아서 소개하는 부분을 처리할 수 있습니다. React에서 Composition Pattern은 이미 모두가 사용하고 있었습니다!

모두 다 받아서 처리 할 수 있으면 좋겠지만 우리는 어떠한 컴포넌트 내부에서 처리할 수 있는 다른 컴포넌트를 적절히 제한하는 방법이 필요합니다. 자주 사용하는 Form 같은 경우가 적절한 예시가 될 수 있습니다.

Composition Pattern을 좀더 멋지게 사용해서 Form 이라는 컴포넌트를 만들어보겠습니다.

먼저 Form 컴포넌트 내부에서 사용할 작은 단위의 컴포넌트들을 만들어 주겠습니다.

(간결한 코드 예시를 위해 SementicUI-React 의 컴포넌트를 활용합니다)

interface CustomFormButtonProps extends FormButtonProps {}

export function CustomFormButton(props: CustomFormButtonProps) {
return <FormButton {...props} />;
}
interface CustomFormFieldProps extends FormFieldProps {}
export function CustomFormField(props: CustomFormFieldProps) {
return <FormField {...props} />;
}
interface CustomFormGroupProps extends FormGroupProps {}
export function CustomFormGroup(props: CustomFormGroupProps) {
return <FormGroup {...props}>{props.children}</FormGroup>;
}
interface CustomFormProps extends FormInputProps {}
export function CustomFormInput(props: CustomFormProps) {
return <FormInput {...props} />;
}
interface CustomFormRadioProps extends FormRadioProps {}
export function CustomFormRadio(props: CustomFormRadioProps) {
return <FormRadio {...props} />;
}
interface CustomFormSelectProps extends FormSelectProps {}
export function CustomFormSelect(props: CustomFormSelectProps) {
return <FormSelect {...props} />;
}
interface CustomFormTextArea extends FormTextAreaProps {}
export function CustomFormTextArea(props: CustomFormTextArea) {
return <FormTextArea {...props} />;
}

Button, Field, Group, Input, Radio, Select, TextArea 이렇게 총 7개의 작은 컴포넌트들을 정의했습니다. 그리고 이 컴포넌트들을 감싸주는 CustomForm 컴포넌트도 만들어 주겠습니다.

interface CustomFormProps {
children: ReactElement[];
}

const CustomForm = ({ children }: CustomFormProps) => {
return <Form>{children}</Form>;
};
export default CustomForm;

이제 다 만들었네요. 그럼 컴포넌트를 합성하여 커다란 Form을 만들어 보겠습니다.

const options = [
{ key: "m", text: "Male", value: "male" },
{ key: "f", text: "Female", value: "female" },
{ key: "o", text: "Other", value: "other" },
];

export function CompositionPatternForm() {
//
return (
<CustomForm>
<CustomFormField>
<label>label</label>
<CustomFormInput />
</CustomFormField>
<CustomFormGroup widths="equal">
<CustomFormInput label="first" />
<CustomFormInput label="second" />
<CustomFormSelect
fluid
label="Gender"
options={options}
placeholder="Gender"
/>
</CustomFormGroup>
<CustomFormGroup inline>
<label>Size</label>
<CustomFormRadio label="Small" value="sm" />
<CustomFormRadio label="Medium" value="md" />
<CustomFormRadio label="Large" value="lg" />
</CustomFormGroup>
<CustomFormTextArea
label="About"
placeholder="Tell us more about you..."
/>
<CustomFormButton>Submit</CustomFormButton>
</CustomForm>
);
}

이렇게 만들어주면 CustomForm 아래로 합성된 다양한 Custom 컴포넌트들을 통해서 화면을 구성할 수 있습니다.

이렇게!!

하지만 우리는 CustomForm안에서 정의된 Custom 컴포넌트를 정해놓고 사용하고 싶습니다. 물론 컴포넌트 이름만 봐도 CustomForm 안에서 사용하는 컴포넌트들 같지만 Composition Pattern을 제대로 사용해서 정의해 줄 수 있습니다.

이쯤에서 Composition Pattern의 좀더 구체적인 설명을 보고 갑시다. 다른분이 잘 설명해주셨네요.

컴포지트 패턴(Composite pattern)이란 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다.

역할이 수행하는 작업

Component

  • 구체적인 부분
  • 즉 Leaf 클래스와 전체에 해당하는 Composite 클래스에 공통 인터페이스를 정의

Leaf

  • 구체적인 부분 클래스
  • Composite 객체의 부품으로 설정

Composite

  • 전체 클래스
  • 복수 개의 Component를 갖도록 정의
  • 그러므로 복수 개의 Leaf, 심지어 복수 개의 Composite 객체를 부분으로 가질 수 있음

출처: https://gmlwjd9405.github.io/2018/08/10/composite-pattern.html

우리는 CustomForm 내부에서 다른 Custom 컴포넌트들을 인스턴스화 해서 사용하려고 합니다. 바로 이렇게 말이죠

import { CustomFormButton } from "./CustomFormButton";
import { CustomFormField } from "./CustomFormField";
import { CustomFormGroup } from "./CustomFormGroup";
import { CustomFormInput } from "./CustomFormInput";
import { CustomFormRadio } from "./CustomFormRadio";
import { CustomFormSelect } from "./CustomFormSelect";
import { CustomFormTextArea } from "./CustomFormTextArea";


interface CustomFormProps {
children: ReactElement[];
}

const CustomForm = ({ children }: CustomFormProps) => {
return <Form>{children}</Form>;
};

CustomForm.Input = CustomFormInput;
CustomForm.Group = CustomFormGroup;
CustomForm.TextArea = CustomFormTextArea;
CustomForm.Select = CustomFormSelect;
CustomForm.Radio = CustomFormRadio;
CustomForm.Field = CustomFormField;
CustomForm.Button = CustomFormButton;

export default CustomForm;

이제 아까 화면을 구성하던 코드는 이렇게 변경됩니다.

const options = [
{ key: "m", text: "Male", value: "male" },
{ key: "f", text: "Female", value: "female" },
{ key: "o", text: "Other", value: "other" },
];

export function CompositionPatternForm() {
//
return (
<CustomForm>
<CustomForm.Field>
<label>label</label>
<CustomForm.Input />
</CustomForm.Field>
<CustomForm.Group widths="equal">
<CustomForm.Input label="first" />
<CustomForm.Input label="second" />
<CustomForm.Select
fluid
label="Gender"
options={options}
placeholder="Gender"
/>
</CustomForm.Group>
<CustomForm.Group inline>
<label>Size</label>
<CustomForm.Radio label="Small" value="sm" />
<CustomForm.Radio label="Medium" value="md" />
<CustomForm.Radio label="Large" value="lg" />
</CustomForm.Group>
<CustomForm.TextArea
label="About"
placeholder="Tell us more about you..."
/>
<CustomForm.Button>Submit</CustomForm.Button>
</CustomForm>
);
}

별로 달라진게 없어보이지만 자식 컴포넌트들을 모두 CustomForm 컴포넌트 내부의 인스턴스화 한 컴포넌트를 사용함으로써 계층구조를 코드로써도 명확히 표현해 줄 수 있습니다.

이거 이외에도 React에서는 HoC(고차함수) 컴포넌트에서도 동일한 패턴을 사용하고 있습니다. 해당 내용은 다른패턴을 확인하면서 적어볼 예정입니다

--

--