함수형 컴포넌트 vs 클래스형 컴포넌트

제이든·2022년 5월 12일
0
post-thumbnail

리액트의 함수형 컴포넌트와 클래스는 어떻게 다를까?
이 두 컴폰넌트 간의 근본적인 차이에 대해서 얘기해보려고 한다.

함수형 컴포넌트는 렌더링된 값들을 고정시킨다

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

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

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

위 컴포넌트에 있는 버튼은 setTimeout을 이용해 네트워크 요청을 보내고 확인 창을 띄워주는 역할을 한다. 이 컴포넌트를 클래스형으로 만들어보자.

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

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

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

대부분의 사람들은 이 코드가 동일하다고 생각할 것이다. 하지만 이 둘은 미묘한 차이를 가지고 있다.

예제 테스트 다음 예제를 열어서 확인해보자.
다음과 같은 순서로 실행해보자.

  1. Follow 버튼을 누른다.
  2. 3초가 지나기 전에 선택된 프로필을 바꾸자.
  3. 알림창의 글을 읽어보자.

결과를 확인해보면 차이점을 발견했을 것이다.

  • Dan의 프로필에서 함수형 컴포넌트의 Follow 버튼을 누른 후 Sophie의 프로필로 이동하면 알림창에서는 'Followed Dan'이라고 쓰여져 있다.

  • 반면, 클래스형 컴포넌트에서는 똑같이 했을 때, 'Followed Sophie'라고 쓰여진걸 볼 수 있다.

이 예제에서는 함수형 컴포넌트의 경우가 올바른 케이스이다.

왜냐면 내가 어떤 사람을 팔로우하고 다른사람의 프로필로 이동했다 하더라도 컴포넌트가 이를 헷갈려해서는 안된다. 클래스 컴포넌트의 동작은 Bug다!

왜 이렇게 동작할까?

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

이 메서드는 this.props.user로부터 값을 불러온다. Props는 리액트에서 불변한 값이지만, this는 변경 가능하며 조작할 수 있다.

따라서 요청이 진행되고 있는 상황에서 클래스 컴포넌트가 다시 렌더링 된다면 this.props또한 바뀐다. showMessage메서드가 새로운 propsuser를 읽는 것이다.

다시말해서, UI가 현재 애플리케이션 상태를 보여주는 함수라 한다면, 이벤트 핸들러 또한 시각적 컴포넌트와 같이 렌더링 결과의 한 부분인 것이다.

즉 이벤트 핸들러가 어떤 props와 state를 가진 render에 종속된다는 것이다.

하지만 this.props를 읽는 콜백을 가진 timeout이 사용되면서 그 종속관계가 깨져버렸다. showMessage 콜백은 더이상 어떤 render에도 종속되지 않게 됐고, 올바른 props 또한 잃게 되었다.

함수형 컴포넌트라는 개념이 없다고 가정했을때 이 문제를 어떻게 해결할 수 있을까?

이를 위해서는 render와 올바른 props그리고 이들을 읽는 showMessage 사이의 관계를 다시 바로 잡아 주어야 한다.

한 가지 방법은 this.props를 조금 더 일찍 부르고 timeout 함수에게는 미리 저장해놓은 값을 전달하는 것이다.

이렇게 하는 것은 클래스의 장점을 무색하게 만들고 복잡하게 만든다.

이 방법 이외에도 render에서 propsstate를 클로저로 감싸준다면 우리가 원하는 방식으로 동작하게 할 수 있다.

class ProfilePage extends React.Component {
  render() {
    // props의 값을 고정!
    const props = this.props;

    // Note: 여긴 *render 안에* 존재하는 곳이다!
    // 클래스의 메서드가 아닌 render의 메서드
    const showMessage = () => {
      alert('Followed ' + props.user);
    };

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

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

이 방법은 매우 잘 동작하지만 조금 꺼림칙하다. 메서드를 클래스에서 선언하지 않고 render내부에서 선언할건데 굳이 클래스를 이용할 필요가 있냐라는 생각이든다.

이를 함수형 컴포넌트로 다시 선언해보자.

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

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

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

값이 인자로 전달됐기 대문에 아까와 마찬가지로 props는 보존된다. 클래스의 this와 다르게 함수가 받는 인자는 리액트가 변경할 수 없다.

함수형 컴포넌트는 render 될 때의 값들을 유지한다.

Hooks의 state에서도 같은 원리가 적용된다.

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>
    </>
  );
}

메세지 전송이 이루어졌을 때 컴포넌트는 어떤 메세지가 전송되었는지를 헷갈려서는 안된다. 이 함수의 메세지는 클릭핸들러가 호출됐을 때의 state를 고정시켜둔다. 때문에 내가 Send를 눌렀을 당시의 input 메세지 값을 간직할 수 있게된다.

지금까지 리액트에서 함수가 props와 state 값을 유지한다는 것에 대해 알아보았다. 그런데 만약 특정 redner에 종속된 것 말고 가장 최근의 state나 props를 읽고 싶다면 어떻게 해아할까?

클래스에서는 this가 변할 수 있는 값이기 때문에 this.props, this.state를 읽어오면 된다. 그런데 함수형 컴포넌트에서도 this처럼 변할 수 있고 서로 다른 render들끼리 공유할 수 있는 녀석이 하나 있다.

이 친구는 ref라고 부른다.

function MyComponent() {
  const ref = useRef(null);
  // `ref.current`로 읽고 쓸 수 있다.
  // ...
}

하지만 ref는 this와 다르게 직접 관리해줘야한다.

ref는 클래스의 인스턴스 영역과 같은 역할을 수행한다. 이는 무언가를 넣을 수 있는 박스라고 봐도 좋다.

리액트의 ref가 자동으로 state나 props를 최신값으로 유지하는 것은 아니다. 일반적으로 이러한 기능을 쓰게 되는 경우는 드물기 때문에 이를 기본동작으로 두는 것은 비효율 적이다.

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

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

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

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

우리가 showMessage로 부터 message를 읽는다면 우리가 send버튼을 눌렀을 때의 message를 볼 수 있다. 하지만 latestMessage.current를 읽는다면 우리는 가장 최근에 보내진 메세지 값을 읽어 올 수 있다.

ref는 고정된 값이 아니기 때문에 렌더링 도중에 읽거나 쓰는 것은 피하는 것이 좋다.

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

  // 최신값을 쫓아간다
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };
profile
개발자 제이든

0개의 댓글