Santos의 개발블로그

Function component와 Class component는 어떻게 다를까? 본문

Language & Framework & Library/React

Function component와 Class component는 어떻게 다를까?

Santos 2023. 11. 4. 13:27

* 이 글은 How are Function Components Different from Classes? 를 번역 및 요약하였습니다.

 

How Are Function Components Different from Classes?

They’re a whole different Pokémon.

overreacted.io


리액트의 Function Component와 Class Component는 어떻게 다른 걸까? 

 

여기에 대한 통용적인 해답은 Class를 통해 더 많은 기능을 사용할 수 있다는 것이었다. 과거형이기는 하지만, state같은 기능은 함수에서 사용할 수 없었다. 물론 Hook이 탄생하기 전까지 말이다. 

 

그럼 속도가 차이가 날까? 이를 주장하는 벤치마크들이 있긴 하지만, 명분이 부족한 부분은 사실ㄹ이다. 그래서 결론을 이끌어 내기에는 조심스러운 부분이 있다. 

 

혹자는 성능의 차이가 난다고 하더래도 이미 작성되어 있는 Component를 바꿔쓰는 것을 권하지는 않는다. Hook이 나온지 얼마 되지 않았고(2014년 기준), 최선의 사용방식이 확립되지 않았기 때문이다. 

 

하지만 두 컴포넌트간의 차이를 아는 것은 중요하다. 핵심은 멘탈 모델이다. 이에 대해 하나씩 자세히 살펴보자.


Function Component는 렌더 당시의 값을 기억하고 사용한다.

아래의 Component를 살펴보자.

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

 

보시다시피 setTimeout을 이용하여 네트워크에 요청을 보내고 응답을 돌려받은 버튼 기능을 가진 컴포넌트이다. 여기서 Arrow Function을 사용해도 위 코드와 동일하게 동작한다는 것을 짋고 넘어간다. 

 

만약 위 코드를 Class Component로 작성한다면 아래와 같을 것이다. 

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }

 

일반적으로 두 컴포넌트는 같은 것으로 보고 같은 방식으로 사용한다. 하지만 이 두가지 코드는 미묘하게 다르다. 다른 점이 보이는가? 만약 다른 점이 보인다면, 이 글을 스킵해도 된다. 

 

이제 이 둘간의 차이를 설명하고, 왜 이러한 차이가 중요한 것인지에 대해 설명하려고 한다. 


리액트로 만든 어플리케이션에서 흔히 일어나는 버그 상황을 예시로 들면서 살펴보겠다. 

아래의 버튼을 렌더하는 컴포넌트를 Function과 Class 형식으로 만들어 보겠다. 

 

pjqnl16lm7 - CodeSandbox

pjqnl16lm7 by gaearon using react, react-dom, react-scripts

codesandbox.io

아래와 같은 순서로 이벤트를 호출해보자. (두가지 컴포넌트 모두 해당)

 

1. Follow 버튼을 클릭한다. 

2. 3초가 지나기 전에 프로필을 바꾼다. 

3. Alert 텍스트를 읽는다.

 

어떤가? 차이점을 느낄 수 있는가? 

Function 방식으로 만들어진 버튼을 눌렀을 때 업데이트 되기 전 profile의 이름이 나오는 반면, Class 방식으로 만들어진 버튼을 누르게 되면 업데이트 된 profile의 이름이 노출된다. 

 

 

본인이 생각하기에는 어떤 방식이 옳다고 생각이 되는가? 의도한 동작은 첫 번째 방식, 즉 Function 방식으로 만들어진 버튼의 동작이 옳은 것으로 생각된다.

 

"내가 만약 누군가를 팔로우 한 후 다른 사람의 프로필로 이동했다면, 본인이 팔로우한 대상을 헷갈려서는 안된다."

이렇게 생각해 보았을 때, Class 방식으로 만들어진 버튼의 동작은 버그다.


 

그렇다면 Class 컴포넌트에서는 왜 이러한 현상이 발생하는 것일까? 

showMessage 메서드를 살펴보자.

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

 

리액트에서 props는 불변한다. 하지만 this는 언제나 변한다. 

this는 변하기 위해 존재한다. 리액트는 this를 계속적으로 변경하여 렌더와 라이프사이클 메서드가 가장 최신의 값에 접근할 수 있도록 해준다. 

alert이 요청되었을 때 Component가 다시 렌더 된다면, this.props 역시 바뀔 것이다. 즉, showMessage 메서드는 이미 최신의 값으로 변경된 user 값을 props에서 읽어오게 된다. 

 

본래 모든 이벤트 핸들러는 특정 렌더의 prop 또는 state 값에 속해 있다. 즉 하나의 렌더 상태에 이벤트 핸들러도 포함된다. 하지만 setTimeout을 이용해 this.props를 불러옴으로써 본래의 속해있던 관계를 깨버린다. 이 시점에 showMessage 함수는 어떠한 렌더의 속해 있지 않아 올바른 props를 놓쳐버린다. 이후 바뀐 this의 정보를 읽어오는 현상이 발생 되버리는 것이다.

 


만약 Function Component가 존재하지 않으면, 우리는 이 문제를 어떻게 해결 할 수 있을까? 

 

해결을 위해서 props가 가야할 올바른 길을 안내해주어야 한다. 

this.props를 이벤트 발생 초기에 읽고, 이 값을 timeout 함수에 넘겨보면 어떨까?

 

아래의 코드를 살펴보자.

class ProfilePage extends React.Component {
  showMessage = (user) => {
    alert('Followed ' + user);
  };

  handleClick = () => {
    const {user} = this.props;
    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

 

해당 방법은 동작하지만, 지향하는 방법은 아니다. 하나 이상의 props를 사용하거나 state 값과 함께 사용해야 할 때 같은 문제를 똑같이 겪을 것이다. 해당 렌더 상황에 맞는 props와 state를 읽을 수 있는 코드를 작성해야 한다. 

 

혹시 constructor에 메서드를 bind에 해 놓으면 어떨까?

class ProfilePage extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  showMessage() {
    alert('Followed ' + this.props.user);
  }

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

 

이 방법도 별다른 효과가 없다. 문제는 this.props에서 읽어오는 시점이 빨라서였지, 사용하는 문법의 문제가 아니었다. 

여기서 closure에 대한 개념이 나온다. 이를 잘 사용해보자. 

 

사실 closure를 사용하는 것은 약간 꺼려질 수 있다. 시간이 지남에 따라 변화하는 값을 생각하기 어렵기 때문이다. 그러나 리액트에서의 props와 state의 값은 변하지 않기 때문에 closure를 사용하기 적절하다. 

class ProfilePage extends React.Component {
  render() {
    // Capture the props!
    const props = this.props;
    // Note: we are *inside render*.
    // These aren't class methods.
    const showMessage = () => {
      alert('Followed ' + props.user);
    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

 

props를 렌더가 되는 시점에 잡아두면 어떨까?

showMessage가 아닌 다른 코드에서도 특정 렌더에 해당하는 props를 바라보고, 올바른 값을 나타낼 수 있다. 

 

closure를 사용하여 해당 문제를 해결하였다. 이제 어떠한 함수라도 렌더 함수에 추가하여 올바른 props와 state 값을 바라볼 수 있다. 


Function을 사용하여 더 단순하게 해보자!!!

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

 

리액트는 props를 인자로 전달하지만 this와 다르게 props 객체는 더 이상 변경되지 않는다. 

** 뭔가 render() 함수가 Function 컴포넌트화 된 것 같은 느낌이 있네요!! ** 

 

만약 부모 컴포넌트가 ProfilePage를 다른 props로 렌더한다면 리액트는 해당 컴포넌트를 다시 실행 할 것이다.

그러나 이미 클릭한 이벤트 핸들러는 이전의 렌더와 유저 값에 귀속해 있기 때문에 showMessage 함수는 귀속되어 있는 값을 읽는다.

즉, 해당 렌더에 속해 있는 값들은 이전 class 방식처럼 훼손되지 않는 것이다.


 

 

Hook을 사용할 때도 마찬가지다.

Hook을 사용할 때도 살펴보자!!

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

 

hook를 사용하는 경우 같은 원칙이 state에 적용된다. 위 예시는 아래와 같은 흐름을 가진다. 

 

1. 인풋 창을 통해 특정 메시지를 입력한다. 

2. 버튼을 클릭한다. 

3. 3초 후에 message의 값은 send 버튼을 누른 시점의 입력값으로 표출된다. 

 

해당 Function Component에서 message는 클릭 이벤트가 일어난 시점의 render에 속하는 state값을 사용하는 것을 확인할 수 있다. 


Class Component와 Function Component 비교를 통해 class에서 일반적으로 발생되는 버그를 살펴보았고, closure가 어떻게 이러한 문제를 해결하는지 살펴 보았다. closure는 더 나아가 Concurrent Mode에서도 정확하게 작동하는 코드를 쓸 수 있도록 도울 것으로 생각이 된다. 

 

Function은 props와 state가 바뀔때마다 종결된다. 즉, 그들이 어떤 시점에 실행 되었는지가 중요하다. 우리가 function을 사용하려 리액트 코드를 짤 때는 어떤 값이 시간이 지남에 따라 바뀔 수 있는지, 이를 이용해 어떻게 코드를 최적화 하는 것이 옳을 지에 대해 생각해 보아야 한다. 

 

리액트의 Function은 항상 값을 캡쳐한다.

이제 그 이유는 closure에게 물어 보면 알 수 있다.


< 참고자료 >

[사이트] #How are Function Components Different from Classes?

 

How Are Function Components Different from Classes?

They’re a whole different Pokémon.

overreacted.io

How are Function Components Different from Classes? end

Comments