처음 만난 리액트 문서


13.1 Composition에 대해 알아보기

Composition이라는 영어 단어는 구성이라는 뜻을 갖고 있습니다. 리액트에서 Composition이라고 하면 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 것을 의미합니다. 앞에서 살펴봤던 에어비앤비 첫 화면을 다시 한 번 보도록 하겠습니다.

컴포넌트 기반 구조

이 페이지에는 A라는 컴포넌트와 B라는 컴포넌트가 반복적으로 나오는 것을 볼 수 있습니다. 또한 이 페이지 자체도 하나의 리액트 컴포넌트입니다. 결국 이 페이지는 컴포넌트A와 컴포넌트B를 합쳐서 페이지 컴포넌트를 만든 것이기 때문에, Composition을 사용했다고 볼 수 있습니다.

리액트로 개발을 하다보면 이처럼 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 일이 굉장히 많습니다. 그래서 Composition은 리액트 전반에 걸쳐서 굉장히 많이 사용되는 방법이기 때문에 잘 알고 있는 것이 좋습니다. Composition이라고 해서 무작정 그냥 컴포넌트들을 붙이는 것이 아니라 여러 개의 컴포넌트들을 어떻게 조합할 것인가에 대한 고민이 필요합니다. 그래서 이 조합 방법에 따라서 Composition의 사용 기법이 나뉘는데, 대표적인 Composition 사용 기법에 대해서 하나씩 배워보도록 하겠습니다.

13.1.1 Containment

먼저 Containment라는 영어 단어는 방지, 견제라는 뜻을 갖고 있습니다. 하지만 여기에서는 Contain의 의미가 조금 더 강하다고 볼 수 있습니다. 영어 단어 Contain은 안에 담다, 포함하다라는 뜻을 갖고 있습니다. 그래서 Containment는 하위 컴포넌트를 포함하는 형태의 Composition방법이라고 이해하면 됩니다.

보통 Sidebar나 Dialog 같은 박스 형태의 컴포넌트는 자신의 하위 컴포넌트를 미리 알 수 없습니다. 예를 들어, 동일한 사이드바 컴포넌트를 사용하는 두 개의 쇼핑몰이 있다고 해보겠습니다. 하나의 쇼핑몰에는 의류와 관련된 메뉴가 8개 들어있고, 다른 쇼핑몰에는 식료품과 관련된 메뉴가 10개 존재합니다. 사이드바 컴포넌트 입장에서는 자신의 하위 컴포넌트로 어떤 것들이 올지 알 수 없겠죠. 해당 컴포넌트를 사용하는 개발자가 어떤 것들을 넣느냐에 따라 하위 컴포넌트가 달라지기 때문입니다.

그렇기 때문에 이런 경우에는 Containment방법을 사용하여 Composition을 사용하게 됩니다. Containment를 사용하는 방법은 리액트 컴포넌트의 props에 기본적으로 들어있는 children속성을 사용하면 됩니다. 아래 예시 코드를 한 번 보도록 하겠습니다.

function FancyBorder(props) {
    return (
        <div className={'FancyBorder FancyBorder-' + props.color}>
            {props.children}
        </div>
    );
}

위 코드에는 FancyBorder라는 굉장히 간단한 컴포넌트가 나옵니다. props.children을 사용하면 해당 컴포넌트의 하위 컴포넌트들이 모두 children으로 들어오게 됩니다. 이 children이라는 prop은 개발자가 직접 넣어주는 것이 아니라 리액트에서 기본적으로 제공해주는 것입니다. 기억력이 좋은분들은 아마 기억하실텐데, 앞에서 리액트의 createElement()함수에 대해서 배울 때, 아래와 같은 형태로 호출했었습니다.

React.createElement(
    type,
    [props],
    [...children]
)

여기서 세 번째에 들어가는 파라미터가 바로 children입니다. children이 배열로 되어 있는 이유는 여러 개의 하위 컴포넌트를 가질 수 있기 때문입니다. 결과적으로 FancyBorder컴포넌트는 자신의 하위 컴포넌트들을 모두 포함(Containment) 하여 이쁜 테두리(Border)로 감싸주는 컴포넌트가 됩니다. 실제로 FancyBorder컴포넌트를 사용하는 예시를 볼까요?

function WelcomeDialog(props) {
    return (
        <FancyBorder color="blue">
            <h1 className="Dialog-title">
                어서오세요
            </h1>
            <p className="Dialog-message">
                우리 사이트에 방문하신 것을 환영합니다!
            </p>
        </FancyBorder>
    );
}

위 코드에서는 WelcomeDialog라는 컴포넌트가 나오고, 여기에서 FancyBorder컴포넌트를 사용하고 있습니다. FancyBorder컴포넌트로 감싸진 부분 안에는 <h1><p> 이렇게 두 개의 태그가 들어가 있습니다. 이 두 개의 태그는 모두 FancyBorder컴포넌트에 children이라는 이름의 props로 전달됩니다. 그리고 결과적으로 파란색의 테두리로 모두 감싸지는 결과가 나오겠죠.

리액트에서는 하위 컴포넌트들을 props.children으로 하나로 모아서 제공해주는데, 그렇다면 여러 개의 children 집합이 필요한 경우에는 어떻게 해야할까요? 그런 경우에는 별도로 props를 정의해서 각각 원하는 컴포넌트들을 넣어주면 됩니다. 아래 예시 코드를 보도록 하겠습니다.

function SplitPane(props) {
    return (
        <div className="SplitPane">
            <div className="SplitPane-left">
                {props.left}
            </div>
            <div className="SplitPane-right">
                {props.right}
            </div>
        </div>
    );
}

function App(props) {
    return (
        <SplitPane
            left={
                <Contacts />
            }
            right={
                <Chat />
            }
        />
    );
}

위 코드에는 먼저 SplitPane이라는 화면을 왼쪽과 오른쪽으로 분할해서 보여주는 컴포넌트가 있습니다. 그리고 아래 쪽에 나와 있는 App컴포넌트에서는 이 SplitPane컴포넌트를 사용하고 있는데, 여기서 left, right라는 두 개의 props을 정의해서 그 안에 각각 다른 컴포넌트를 넣어주고 있습니다. SplitPane에서는 이 left, rightprops로 받게 되고, 각각 화면의 왼쪽과 오른쪽에 분리해서 렌더링하게 됩니다. 이처럼 여러 개의 children 집합이 필요한 경우에는 별도의 props를 정의해서 사용하면 됩니다.

지금까지 살펴본 것처럼 props.children이나 직접 정의한 props를 이용하여 하위 컴포넌트들을 포함하는 형태로 Composition하는 방법을 Containment라고 합니다.

13.1.2 Specialization

다음으로 배워볼 Composition방법은 Specialization입니다. 영어 단어 Specialization은 전문화, 특수화라는 뜻을 갖고 있습니다. 특수화라는 것의 의미가 어렵게 느껴질 수도 있는데, 아래 한 가지 예를 들어보겠습니다.

WelcomeDialog는 Dialog의 특별한 케이스이다.

Dialog라는 것은 굉장히 범용적인 의미를 갖고 있습니다. 모든 종류의 Dialog를 다 포함하는 개념이라고 볼 수 있죠. 반면에 WelcomeDialog는 누군가를 반기기 위한 Dialog라고 볼 수 있습니다. 범용적인 의미가 아닌, 조금 더 구체적이고 특수화된 것입니다. 이처럼 범용적인 것을 구체적이고 특수화하는 것을 Specialization이라고 합니다.

객체지향에 대해서 배운 분들은 알겠지만, 기존의 객체지향 언어에서는 상속(Inheritance)을 사용하여 Specialization을 구현합니다. 하지만 리액트에서는 Composition을 사용하여 Specialization을 구현하게 됩니다. 예시 코드를 직접 보면서 더 자세히 설명해보도록 하겠습니다.

function Dialog(props) {
    return (
        <FancyBorder color="blue">
            <h1 className="Dialog-title">
                {props.title}
            </h1>
            <p className="Dialog-message">
                {props.message}
            </p>
        </FancyBorder>
    );
}

function WelcomeDialog(props) {
    return (
        <Dialog
            title="어서오세요"
            message="우리 사이트에 방문하신 것을 환영합니다!"
        />
    );
}

위 코드에는 먼저 Dialog라는 범용적인 의미를 가진 컴포넌트가 나옵니다. 그리고 이 Dialog컴포넌트를 사용하는 WelcomeDialog컴포넌트가 나옵니다. Dialog컴포넌트는 titlemessage라는 두 가지 props를 갖고 있는데, 각각 다이얼로그에 나오는 제목과 메시지를 의미합니다. 그래서 제목과 메시지를 어떻게 사용하느냐에 따라서 경고 다이얼로그가 될 수도 있고, 인사말 다이얼로그가 될 수도 있겠죠. WelcomeDialog컴포넌트는 제목을 '어서오세요'라고 하여 사이트에 접속한 사용자에게 인사말을 하는 다이얼로그를 만들었습니다.

지금까지 살펴본 것처럼 범용적으로 쓸 수 있는 컴포넌트를 만들어 놓고, 이를 특수화 시켜서 컴포넌트를 사용하는 Composition방식을 Specialization이라고 합니다.

13.1.3 Containment와 Specialization 같이 사용하기

자 그렇다면 Containment와 Specialization을 같이 사용하려면 어떻게 해야 할까요? 일단 떠오르는 생각으로는 Containment를 위해서 props.children을 사용하고 Specialization을 위해서 직접 정의한 props를 사용하면 될 것 같습니다. 실제 예시 코드를 보면서 한 번 자세히 알아보도록 하겠습니다.

function Dialog(props) {
    return (
        <FancyBorder color="blue">
            <h1 className="Dialog-title">
                {props.title}
            </h1>
            <p className="Dialog-message">
                {props.message}
            </p>
            {props.children}
        </FancyBorder>
    );
}

function SignUpDialog(props) {
    const [nickname, setNickname] = useState('');

    const handleChange = (event) => {
        setNickname(event.target.value);
    }

    const handleSignUp = () => {
        alert(`어서오세요, ${nickname}님!`);
    }

    return (
        <Dialog
            title="화성 탐사 프로그램"
            message="닉네임을 입력해주세요.">
            <input
                value={nickname}
                onChange={handleChange} />
            <button onClick={handleSignUp}>
                가입하기
            </button>
        </Dialog>
    );
}

위 코드에 나오는 Dialog컴포넌트는 이전에 나왔던 코드와 거의 비슷한데 Containment를 위해서 끝부분에 props.children를 추가했습니다. 이를 통해 하위 컴포넌트들이 다이얼로그 하단에 렌더링되게 됩니다. 실제로 Dialog컴포넌트를 사용하는 SignUpDialog컴포넌트를 살펴보면, Specialization을 위한 propstitle, message에 값을 넣어주고 있으며 사용자로부터 닉네임을 입력 받고 가입을 하도록 유도하기 위해 <input><button>태그가 들어 있습니다. 이 두 개의 태그는 모두 props.children으로 전달되어 다이얼로그에 표시됩니다. 이러한 형태로 Containment와 Specialization을 동시에 사용할 수 있습니다.

지금까지 리액트의 컴포넌트 Composition을 위한 두 가지 방법인 Containment와 Specialization에 대해서 배워봤습니다. 각 방법을 따로 사용하거나 또는 동시에 함께 사용하면 다양한 복잡한 컴포넌트들을 효율적으로 개발할 수 있습니다.


마지막 업데이트: 2025년 08월 28일 08시 16분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.

On this page