Ayden's journal

합성 컴포넌트 패턴과 동적 프로퍼티 패턴

합성 컴포넌트 패턴이란 무엇인가

합성 컴포넌트 패턴(Composite Component Pattern)은 컴포넌트를 보다 유연하게 구성하고 재사용할 수 있도록 여러 하위 컴포넌트를 조합하여 만든 컴포넌트이다. 세간의 인식은 조금 더 복잡한 무언가를 예상하는 듯하지만, 실제로는 리액트를 사용하는 사람이라면 누구나 children을 통해 합성 컴포넌트 패턴을 사용하고 있다.

function Dialog({ children }) {
  return <div className="dialog">{children}</div>;
}

function App() {
  return (
    <Dialog>
      <p>이것은 합성 컴포넌트입니다.</p>
    </Dialog>
  );
}

 

만약 특정한 형태의 컴포넌트만을 사용하고 싶다면 children 대신 다른 props 이름을 사용하여 아래와 같이 구현할 수도 있다. props 이름을 사용한다고 해서 특정한 컴포넌트만 받을 수 있도록 하는 강제력이 생기는 것은 아니다. 다만 이름 그 자체를 통해 어떤 목적을 간접적으로 드러낼 뿐이다.

function Dialog({ title }) {
  return <div className="dialog">{title}</div>;
}

function App() {
  return (
    <Dialog title={<p>이것은 합성 컴포넌트입니다.</p>} />
  );
}

 

함수 : 자바스크립트의 1급 객체

자바스크립트의 함수에 대해 공부하다보면 '1급 객체(First-Class Citizen 또는 First-Class Object)'라는 표현을 심심치 않게 볼 수 있다. 문서에 따라 1급 객체를 설명하는 방식은 다소 차이가 있지만, 일반적으로는 어떤 프로그래밍 언어의 함수가 아래의 네 가지 경우를 모두 충족하는 경우 1급 객체라고 표현하는 듯하다.

  • 다른 값들과 동일하게 변수나 데이터 구조 안에 담을 수 있다 ➠ 함수 표현식 및 메소드
  • 파라미터로 전달할 수 있다 ➠ 콜백 함수
  • 리턴 값으로 사용할 수 있다 ➠ 고차 함수, 클로저
  • 동적으로 프로퍼티 할당이 가능하다. ➠ ???

다른 조건들은 일반적으로 널리 사용되지만, 마지막 경우는 그렇지 않은 것처럼 느껴진다. 동적으로 프로퍼티 할당이 가능하다는 것은, 쉽게 풀어서 해석하면 이미 생성된 어떤 객체에 언제든 프로퍼티를 추가할 수 있다는 의미이다. 객체에 대해서는 당연하다고 여겨지는 이러한 행위가 실제로 함수에서 가능한지는 확인해본 적이 없었다.

 

크롬의 개발자 도구에서 확인해본 결과 실제로 함수에 동적으로 프로퍼티를 추가할 수 있었다.

 

정적 메소드 패턴

고대의 리액트는 컴포넌트를 함수가 아니라 클래스 형태로 만들었다. 미친 소리같지만 사실이다. 그리고 정적 메소드 패턴(Static Method Pattern)은 클래스 컴포넌트 시절의 편린이다. 정적 메소드(Static Method)를 모르는 사람들을 위해 간단히 설명하자면, 이는 클래스의 인스턴스가 아닌 클래스 자체에 속하는 메소드이며, 클래스 이름을 통해서 직접 호출한다.

그리고 정적 메소드 패턴이란 static 키워드를 사용해 클래스 컴포넌트 내부에 정적 메소드의 형태로 서브 컴포넌트를 선언하는 형태를 말한다. 아래의 예시 코드에서 확인할 수 있는 것처럼 Dropdown 클래스 자체도 컴포넌트이지만, 그 내부의 정적 메소드 Label과 Content도 역시 컴포넌트이다.

import React, { Component } from "react";

const DropdownContext = React.createContext({
  show: false,
  toggle: () => {}
});

export default class Dropdown extends Component {
  static Label = ({ children = "Show", toggle }) => (
    <DropdownContext.Consumer>
      {({ toggle }) => <span onClick={toggle}>{children}</span>}
    </DropdownContext.Consumer>
  );
  
  static Hidden = ({ show, children }) => (
    <DropdownContext.Consumer>{({ show }) => (show ? null : children)}</DropdownContext.Consumer>
  );
  
  static Content = ({ show, children }) => (
    <DropdownContext.Consumer>{({ show }) => (show ? children : null)}</DropdownContext.Consumer>
  );
  
  toggle = () => {
    const { onToggle = () => {} } = this.props;
    this.setState(
      ({ show }) => {
        return {
          show: !show
        };
      },
      () => onToggle(this.state.show)
    );
  };
  
  state = {
    toggle: this.toggle,
    show: false
  };

  render() {
    return (
      <DropdownContext.Provider value={this.state}>{this.props.children}</DropdownContext.Provider>
    );
  }
}
function App() {
  return (
    <div className="App">
      <Dropdown>
        <Dropdown.Label>Click here to open dropdown</Dropdown.Label>

        <div className="content-container">
          <Dropdown.Content>This is the content</Dropdown.Content>
        </div>
      </Dropdown>
    </div>
  );
}

이 방식은 컴포넌트의 서브 컴포넌트를 하나의 네임스페이스 아래에 두어 계층 구조를 더욱 직관적으로 만들고, 컴포넌트의 구성을 명확히 하여 가독성을 높이는 효과가 있다. 하지만 초반에 말했던 것처럼 정적 메소드 패턴은 클래스 컴포넌트 시절의 편린이며, 함수에는 static을 사용하여 정적 메소드를 선언할 수 없다.

하지만 함수에는 동적으로 프로퍼티를 추가할 수 있으며, 이러한 특징을 활용할 수 있지 않을까?

 

동적 프로퍼티 패턴

1급 객체 항목에서 개발자 항목을 통해 살펴본 것처럼, 어떤 함수에 동적으로 프로퍼티를 추가할 때는 문자열이나 숫자 같은 단순 값을 넣을 수도 있고, 객체나 함수와 같은 참조형 타입의 값을 넣을 수도 있다. 요즘 리액트는 함수를 사용해 컴포넌트를 구현하므로, 컴포넌트에 프로퍼티를 추가하여 서브 컴포넌트를 할당할 수 있다. 이를 동적 프로퍼티 패턴(Dynamic Property Pattern)이라고 부른다.

이 패턴을 사용하면 From∙Input∙InputWrapper 컴포넌트를 각각 내보내는 것이 아니라, Form 컴포넌트 하나만 내보내도 서브 컴포넌트들이 함께 따라온다. 이를 합성 컴포넌트 패턴을 사용해 아래와 같은 방식으로 더 큰 규모의 컴포넌트를 구현하는 것이다.

 

결론

이 글을 쓰게 된 이유가 있다. 그 이유란 것이 아주 길고 너저분하며 누구도 알고싶어하지 않을 만한 내용이므로 최선을 다 해 간단히 요약해보자면, 동적 프로퍼티 패턴을 합성 컴포넌트 패턴이라고 알고 있는 사람이 생각보다 많다는 것 때문이었다. 동적 프로퍼티 패턴은 그 형태가 하나의 커다란 컴포넌트를 여러 개의 서브 컴포넌트로 분리하는 식이라 합성 컴포넌트 패턴을 적용하기 아주 적합하다.

그렇다고 해서 동적 프로퍼티 패턴이 곧 합성 컴포넌트 패턴이 되는 것은 아니다. 이 두 개의 패턴은 사용함에 있어서 그 목적이 다르다. 합성 컴포넌트 패턴은 컴포넌트의 children이나 props를 통해 여러 컴포넌트를 상속하지 않고 합성하도록 하며, 이를 통해 작은 컴포넌트를 모아 규모 있는 컴포넌트를 구성할 수 있도록 돕는다. 이에 반해 동적 프로퍼티 패턴은 자바스크립트의 함수가 1급 객체라는 점을 활용하여 과거의 정적 메소드 패턴과 유사하게 사용할 수 있도록 ─ 또한 정적 메소드 패턴의 장점을 그대로 가져다 쓸 수 있도록 ─ 컴포넌트의 프로퍼티에 서브 컴포넌트를 선언하는 패턴을 말한다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기