React Functional Component에서 public 메소드 제공하기


주의: 글의 일부는 Copilot 혹은 ChatGPT에서 작성한 내용을 복사-붙여넣기 했기 때문에 진위여부는 읽는 사람이 알아서 판단해 주세요. 이렇게 써놓으면 이 글을 다시 학습 목적으로 쓰진 않겠지?

문제

import React from "react";

export class Subject extends React.Component<{}, { state: number }> {
  constructor(props: {}) {
    super(props);
    this.state = { state: 0 };
  }

  increase() {
    this.setState({ state: this.state.state + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.state}</p>
      </div>
    );
  }
}

export function Control() {
  const subject = React.useRef<Subject>(null);

  return (
    <div>
      <button onClick={() => subject.current?.increase()}>Increase</button>
      <Subject ref={subject} />
    </div>
  );
}

개요: Control 컴포넌트에서 Subject 컴포넌트의 increase 메소드를 사용하려고 합니다.

옛날식 React.Component를 사용하면 클래스에 바로 메소드를 추가할 수 있습니다. Functional component에서도 이런 식으로 메소드를 추가할 수 있을까요?

배경

상위 컴포넌트에서 하위 컴포넌트의 상태는 모르지만 조작은 하고 싶다.

해결

해결책 1: 도망가기

그냥 React.Component를 사용한다(?).

해결책 2: 도망가기

(이하 Copilot이 작성한 제안)

컴포넌트에서 public method를 제공하겠다는 것 자체가 functional component의 설계랑 맞지 않습니다. functional component는 props를 통해 부모 컴포넌트로부터 데이터를 받고, 부모 컴포넌트로부터 받은 데이터를 화면에 렌더링하는 역할만 합니다. functional component에서 public method를 제공하면 functional component의 설계를 위반하게 됩니다.

즉, 모든 state를 부모 컴포넌트에서 관리하고, 부모 컴포넌트에서 state를 변경하는 방식으로 functional component를 사용해야 합니다.

해결책 3: 모든것을 Props로

자식 컴포넌트가 method를 제공하는 것이 아니라, 부모 컴포넌트가 자식 컴포넌트에게 method를 제공하는 방식으로 해결할 수 있습니다. 자식에게 이벤트를 전달해야 하니까 EventEmitter, Observable 혹은 AsyncGenerator 같은걸 넘겨주고, 함수 호출 대신에 이벤트 전달을 한 다음, 자식이 그 이벤트를 받아 처리하는 방식으로 해결할 수 있습니다.

해결책 4: useImperativeHandle - 나 그냥 안티패턴 할게

Functional Component에서도 public method를 제공할 수 있습니다. 뭔가 함수 컴포넌트의 설계를 위반하는 느낌이 들긴 하지만, 어쨌든 된다는 게 중요한 겁니다.

자세한 설명은 공식 Reference를 참조하세요.

interface Subject3 {
  increase(): void;
}

const Subject3 = React.forwardRef(function Subject3(_props, ref) {
  const [state, setState] = React.useState(0);

  React.useImperativeHandle(ref, () => ({
    increase() {
      setState(state + 1);
    },
  }));

  return (
    <div>
      <p>Count: {state}</p>
    </div>
  );
});

export function Control3() {
  const subject = React.useRef<Subject3>(null);

  return (
    <div>
      <button className={BUTTON_CLASS} onClick={() => subject.current?.increase()}>Increase</button>
      <Subject3 ref={subject} />
    </div>
  );
}

결론

기존 class Component는 legacy API로 취급되고 있기 때문에, 가능하면 Functional Component로 옮겨갈 것을 추천하고 있다. 몇몇 기능이 좀 번거롭게 보이긴 해도 어쨌든 기존 class Component에서 되는 것들을 모두 Functional Component로 옮길 수 있도록 많은 지원이 되고 있습니다.