들어가기 전에

리액트 공식문서로 배워보자 #11, 합성(Composition) vs 상속(Inheritance)

리액트는 강력한 합성 모델을 갖고 있습니다. 그리고 우리는 컴포넌트 사이에서 코드를 재사용하기 위해서 상속보다는 조합을 사용하는 것을 권장합니다.

이 섹션에서, 우리는 리액트에 새로 입문하는 개발자들이 상속을 위해서 자주 당면하는 몇가지 문제에 대해 고민할 것입니다. 그리고 합성으로 그 문제들을 어떻게 해결하는지 보여줄 것입니다.

방지(Containment)

어떤 컴포넌트들은 그들의 자식을 미리 알지 못합니다. Sidebar 또는 Dialog 와 같은 일반적인 "박스(Boxes)"를 표현하는 컴포넌트는 특히 일반적으로 그렇습니다.

우리는 그러한 컴포넌트들이 특별한 children prop을 사용하여 children 엘리먼트를 직접 출력으로 넘겨주는 것을 권장합니다.

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

위와 같이 하면, JSX에 children을 내장함으로써 임시적인 children 엘리먼트를 넘겨주는 것을 가능하게 합니다.

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> JSX 태그 내부의 어떤 것이든 FancyBorder 컴포넌트에 children prop으로서 넘어가게 됩니다. Fnacyborder<div> 내부의 {props.children}을 렌더링하기 때문에, 넘겨진 엘리먼트는 최종 결과물에 표시됩니다.

이것은 일반적인 경우는 아니지만, 가끔은 컴포넌트 내부에 여러개의 "구멍(holes)"이 필요할 수 있습니다. 그러한 경우에, 당신은 children을 사용하는 것 대신에 자신만의 컨벤션을 떠올릴 수도 있습니다.

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

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

코드펜에서 직접 해보기

<Contacts /><Chat />과 같은 리액트 엘리먼트는 그냥 오브젝트입니다. 그러므로 여러분은 그냥 리액트 엘리먼트를 다른 데이터처럼 props로 넘겨줄 수 있습니다. 이러한 접근은 다른 라이브러리에서 "slots"를 다시 떠올리게 할수도 있습니다. 하지만 리액트에서는 props를 몇개 넘기던 그 제한이 없습니다.

특화

때때로, 우리는 컴포넌트들을 그저 다른 컴포넌트의 "특별한 케이스"로 여기는 경우가 있습니다. 예를 들자면, WelcomeDialogDialog의 "특별한 케이스"라고 말할 수 있습니다.

리액트에서, 이것 역시 합성으로 해결할 수 있습니다. 더 상세한 컴포넌트는 더욱 일반적인 것을 렌더링한 뒤, props로 상세한 부분을 세팅합니다.

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

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />

  );
}

코드펜에서 직접 해보기

합성은 클래스로 정의된 컴포넌트에 대해서도 잘 작동합니다.

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

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />

        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

  handleChange(e) {
    this.setState({login: e.target.value});
  }

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }
}

코드펜에서 직접 해보기

그럼 상속으론 무엇을 하죠?

페이스북에서, 우리는 몇천개의 컴포넌트를 만드는데 리액트를 씁니다. 그리고 아직 상속 계층 컴포넌트를 만드는 것을 권장할만한 케이스를 찾지 못했습니다.

Props와 합성(Composition)은 컴포넌트의 모양과 명시적이고 안전한 행위를 커스터마이징하는데 필요한 모든 유연성을 충분히 제공합니다. 컴포넌트는 원시적 값, 리액트 엘리먼트 또는 함수와 같은 임시적인 props를 받을 수도 있다는 것을 기억하세요.

만일, 컴포넌트 사이에서 UI가 없는 기능을 재사용하길 원한다면, 우리는 독립된 자바스크립트 모듈에서 추출하는 것을 권장합니다. 컴포넌트는 그것을 import하고 확장없이 함수, 오브젝트 또는 클래스를 사용할 것입니다.