13-1강. 합성 (Composition) vs 상속 (Inheritance)

정원·2023년 2월 2일
0

React

목록 보기
31/42

2023.02.02 합성 (Composition) vs 상속 (Inheritance)

React는 강력한 합성 모델을 가지고 있으며,
상속 대신 합성을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋습니다.

합성( Composition )

Compositions이라는 단어눈 구성,합성이라는 뜻을 갖고 있습니다.
리액트에서는 합성을 의미하여 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 것을 말합니다.

조합 방법에 따라 합성의 사용기법이 나뉘는데 대표적인 합성 사용 기법에 대해서 하나씩 배워 보도록 하겠습니다.

합성 방법 1.Containment

컴포넌트에서 다른 컴포넌트를 담기

리액트에서 Containment는 하위 컴포넌트를 포함하는 형태의 합성 방법입니다.

어떤 컴포넌트들은 어떤 하위 엘리먼트가 들어올 지 미리 예상할 수 없는 경우가 있습니다.
범용적인 ‘박스’ 역할을 하는 Sidebar 혹은 Dialog와 같은 컴포넌트에서 특히 자주 볼 수 있습니다.

이런 경우에는 Containment 방법을 사용하여 합성을 사용하게됩니다.
Containment를 사용하는 방법은 리액트 컴포넌트의 props에 기본적으로 들어있는 children 속성을 사용하면 됩니다.

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

props.children을 사용하면 해당 컴포넌트의 하위 컴포넌트가 모두 children으로 들어오게 됩니다.
children이라는 prop은 개발자가 직접 넣어 주는 것이 아니라
리액트에서 기본적으로 제공해 주는 것입니다.

앞에서 리액트의 createElement() 함수에 대해서 배울 때 아래와 같은 형태로 호출했습니다.

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

children이 배열로 되어있는 이유는 여러 개의 하위 컴포넌트를 가질 수 있기 때문입니다.

결과적으로 FancyBorder 컴포넌트는 자신의 하위 컴포넌트를 모두 포함하여 예쁜 테두리도 감싸주는 컴포넌트가 됩니다.

아래는 실제로 FancyBorder를 사용하는 예제입니다.

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </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,right를 props로 받게 되고
각각 화면의 왼쪽과 오른쪽에 분리해서 렌더링하게 됩니다.

이처럼 여러 개의 children 집합이 필요한 경우에는 별도의 props를 정의해서 사용하면 됩니다.

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

합성 방법2.Specialization(특수화)

때로는 어떤 컴포넌트의 “특수한 경우”인 컴포넌트를 고려해야 하는 경우가 있습니다.
예를 들어, WelcomeDialog는 Dialog의 특수한 경우라고 할 수 있습니다.

다이얼로그라는 것은 굉장히 범용적인 의미를 갖고 있습니다.
반면에 웰컴다이얼로그는 누군가를 반기기 위한 다이얼로그라고 볼 수 있습니다.

이처럼 범용적인 개념을 구별이 되게 구체화하는 것을 Specialization이라고 합니다.

  • 객체지향에서는 상속을 사용하여 Specialization을 구현
  • 리액트에서는 합성을 사용하여 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="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}

위 코드에는 먼저 Dialog라는 범용적인 의미를 가진 컴포넌트가 나옵니다.
그리고 이 Dialog컴포넌트를 사용하는 WelcomeDialog 컴포넌트가 나옵니다.
Dialog 컴포넌트는 title과 messge라는 두 가지 props를 갖고 있는데 각각 다이얼로그에 나오는 제목과 메시지를 의미합니다.
그래서 제목과 메시지를 어떻게 사용하느냐에 따라서 경고 다이얼로그가 될 수도 있고,
인사말 다이얼로그가 될 수도 있습니다.

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

Containment와 Specialization을 같이 사용하기

Containment를 위해서 props.children를 사용하고 Specialization을 위해 직접 정의한 props를 사용하면 될 것 같습니다.

import { useState } from "react";

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 = (e) => {
        setNickName(e.target.value);
    }

    const handleSignUp = () => {
        alert(`어서오세요, ${nickName}님!`);
    }
  
    
    return (
    <Dialog title="Mars Exploration Program"
            message="닉네임을 입력해 주세요.">
        <input value={nickName}
                onChange={handleChange} />
        <button onClick={handleSignUp}>
        Sign Me Up!
        </button>
    </Dialog>
    );
}

위의 코드에 Dialog 컴포넌트는 이전에 나왔던 코드와 거의 비슷한데
Containment를 위해 끝부분에 props.children을 추가했습니다.
이를 통해 하위 컴포넌트가 다이얼로그 하단에 렌더링 됩니다.

실제로 Dialog 컴포넌트를 사용하는 SignUpDialog 컴포넌트를 살펴보면
Specialization을 위한 props인 title,message에 값을 넣어 주고 있으며
사용자로부터 닉네임을 입력받고 가입하도록 유도하기 위해 <input>과 <button>태그가 들어 있습니다.
이 두개의 태그는 모두 props.children으로 전달되어 다이얼로그에 표시됩니다.

이러한 형태로 Containment와 Specialization을 같이 사용할 수 있습니다.

상속(inheritance)

Facebook에서는 수천 개의 React 컴포넌트를 사용하지만, 컴포넌트를 상속 계층 구조로 작성을 권장할만한 사례를 아직 찾지 못했습니다.

결국 리액트에서는 상속이라는 방법을 사용하는 것보다는 앞에서 배운 합성을 사용해서
개발하는 것이 더 좋은 방법입니다. 따라서 결론은 다음과 같습니다.

복잡한 컴포넌트를 쪼개 여러 개의 컴포넌트로 만들고,
만든 컴포넌트들을 조합하여 새로운 컴포넌트를 만들자!

(실습) Card 컴포넌트 만들기

Card.jsx

function Card(props) {
    const { title, backgroundColor, children} = props;

    return (
        <div
            style={{
                margin: 8,
                padding: 8,
                borderRadius: 8,
                boxShadow: "0px 0px 4px gray",
                backgroundColor: backgroundColor || "white",
            }}
            >
                {title && <h1>{title}</h1>}
                {children}
        </div>

    );
}

export default Card;

Card 컴포넌트는 하위 컴포넌트를 감싸서 카드 형태로 보여주는 컴포넌트입니다.
앞에서 배운 Containment와 Sprcialization, 이 두 가지 합성 방법을 모두 사용하여 구현했습니다.
여기에서 children을 사용한 부분이 Containment이고
title과 background를 사용한 부분이 Specialization이라고 할 수 있습니다.

Card 컴포넌트는 범용적으로 재사용이 가능한 컴포넌트인데 이것을 사용하여
ProfileCard 컴포넌트를 만들어 보겠습니다.

ProfileCard.jsx

import Card from "./Card";

function ProfileCard(props) {
    return (
        <Card title="Inje Lee" backgroundColor="#4ea04e">
            <p>안녕하세요, 소플입니다.</p>
            <p>저는 리액트를 사용해서 개발하고 있습니다.</p>
        </Card>
    );
}

export default ProfileCard;

ProfileCard 컴포넌트는 Card컴포넌트를 사용하여 title에 이름을 넣고
backgroundColor를 녹색으로 설정하였습니다.
children으로는 간단한 소개 글을 넣어봤습니다.
이렇게 하면 Card 컴포넌트가 사용자의 프로필을 나타내는 ProfileCard 컴포넌트가 됩니다.

index.js

수정

import React, { Profiler } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import ProfileCard from './chapter_13/ProfileCard';
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <ProfileCard />
  </React.StrictMode>
);

reportWebVitals();

요약

합성이란?

  1. 여러 개의 컴포넌트를 합쳐서 새로운 컴포넌트를 만드는 것
  2. 다양하고 복잡한 컴포넌트를 효율적으로 개발할 수 있음

합성 기법

  1. Containment
    • 하위 컴포넌트를 포함하는 형태의 합성 방법
    • 리액트 컴포넌트의 props에 기본적으로 들어있는 children 속성을 사용
    • 여러 개의 chlidren 집합이 필요한 경우 별도로 props를 각각 정의해서 사용
  2. Specialization
    • 범용적인 개념을 구별되게 구체화하는 것
    • 범용적으로 쓸 수 있는 컴포넌트를 만들어 놓고 이를 구체화시켜서 컴포넌트를 사용하는 합성 방법
  3. Containment와 Specialization을 함께 사용하기
    • props.children을 통해 하위 컴포넌트를 포함시키기(Containment)
    • 별도의 props를 선언하여 구체화시키기(Specialization)

상속

  1. 리액트에서는 상속이라는 방법을 사용하는 것보다는 합성을 사용하는 것이 더 좋음

0개의 댓글