대부분 함수형 컴포넌트를 써야 하는 이유 (feat. 클래스형 컴포넌트)

MayOwall·2024년 1월 30일
1

기술 아티클

목록 보기
3/5

리액트에는 두가지 종류의 컴포넌트가 있습니다. 하나는 함수형 컴포넌트, 그리고 다른 하나는 클래스형 컴포넌트입니다.

요즘 리액트를 사용해서 개발한 프로젝트들을 보면 함수형 컴포넌트를 주로 사용하고 클래스형 컴포넌트는 거의 사용되지 않고 있다는 것을 알 수 있습니다. 클래스형 컴포넌트로 짜여진 코드는 이따금 오래된 프로젝트에서 유지보수의 형태로 발견되곤 합니다.

왜 클래스형 컴포넌트가 아닌 함수형 컴포넌트를 사용하는 것이 권장될까요? 이번 글에서는 클래스형 컴포넌트와 함수형 컴포넌트에 대해 알아보고, 이 둘을 비교하여 함수형 컴포넌트가 권장되는 이유에 대해 알아보도록 하겠습니다.


클래스형 컴포넌트

클래스형 컴포넌트는 리액트 16.8 버전 (2019년 2월) 이전까지 활발히 사용되던 컴포넌트입니다. 컴포넌트를 선언할 때 자바스크립트 Class 문법을 사용하여 클래스형 컴포넌트라고 불립니다. 클래스형 컴포넌트의 구조에 대해 알아보고, 클래스형 컴포넌트의 가장 큰 특징인 생명주기 바탕의 메서드에 대해 알아보도록 하겠습니다.

클래스형 컴포넌트의 기본 구조

클래스형 컴포넌트는 리액트에서 기본 제공되는 Component 클래스를 상속하여 생성합니다. 클래스형 컴포넌트는 크게 constructor, props, state, 메서드로 구성되어 있습니다.

클래스형 컴포넌트 예시

import React from "react";

class MyComponent extends React.Component {
  private constructor(props) {
    super(props);
    this.state = {
      count: 0,
      isLimited: false,
    };
  }

  private handleClick = () => {
    const newValue = this.state.count + 1;
    this.setState({ count: newValue, isLimited: newValue >= 10 });
  };

  public render() {
    const {
      props: { required, text },
      state: { count, isLimited },
    } = this;

    return (
      <h2>
        Sample Component
        <div>{required ? "필수" : "필수 아님"}</div>
        <div>문자 : {text}</div>
        <div>count : {count}</div>
        <button onClick={this.handleClick} disabled={isLimited}>
          증가
        </button>
      </h2>
    );
  }
}

constructor()

컴포넌트의 state를 초기화할 수 있는 메서드입니다. 컴포넌트가 초기화 되는 시점에 호출되며, constructor() 내부에서 super()를 호출하여 상속한 React.Component의 스코프에 접근하는 것이 가능해지도록 해 줍니다.

props

컴포넌트에 특정 속성을 전달하는 용도로 쓰입니다. constructor()의 매개변수로 전달된 후, 해당 컴포넌트 인스턴스에 this.props 형태로 저장됩니다.

state

클래스형 컴포넌트 내부에서 관리하는 값을 의미합니다. 항상 객체여야 하며, 값에 변화가 있을 때 마다 리랜더링이 발생합니다. constructor()에서 초기화가 가능합니다.

메서드

컴포넌트 내보에서 사용되는 함수입니다. 보통 DOM에서 발생하는 이벤트와 함께 사용되며, 일반 함수로 선언시 this가 undefined으로 나오는 문제가 발생하게 됩니다. 따라서 이러한 문제를 해결하기 위해 일반 함수로 선언한 후 constructor 내부에서 bind를 이용하여 강제로 this를 바인딩 하거나, 상단의 예시처럼 화살표 함수 선언 방식을 사용하여 메서드를 선언합니다.

private constructor(props){
	...
	// constructor 내부에서 bind를 이용하여 강제로 this를 바인딩
	this.handleClick = this.handleClick.bind(this)
}

private handleClick() {
	....
}

생성자 함수가 아닌 일반 함수로 호출한 함수의 this는 전역 객체(strict 모드에서는 undefined)에 바인딩됩니다.
이와 달리 화살표 함수는 실행 시점이 아닌 작성 시점의 상위 스코프가 this로 결정됩니다.

컴포넌트의 생명주기와 클래스형 메서드

클래스형 컴포넌트의 가장 큰 특징 중 하나는, 부모 컴포넌트인 React.Component로 부터 컴포넌트의 생명주기로 바탕으로 짜여져있는 메서드를 상속받아 사용할 수 있다는 것입니다. 이러한 메서드를 생명주기 메서드라고 합니다.

생명주기 메서드가 실행되는 시점은 크게 3가지로 나뉠 수 있습니다.

  • 마운트(mount) : 컴포넌트가 생성되는 시점
  • 업데이트(update) : 컴포넌트의 내용이 변경되는 시점
  • 언마운트(unmount) : 컴포넌트가 삭제되어 더 이상 존재하지 않는 시점

출처 : react-lifecycle-methods-diagram

상단의 3가지 시점을 바탕으로 실행되는 생명주기 메서드 중, 대표적인 메서드는 아래와 같습니다.

  • render()
  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

또한 일부 생명주기 메서드의 기능은 오직 클래스형 컴포넌트에서만 사용가능합니다. 리액트 팀이 아래의 메서드와 동일한 작업을 할 수 있는 훅을 추가할 것이라고 언급하긴 했지만 정확하게 밝혀진 사항은 없습니다. 즉 아래의 메서드들이 제공하는 기능들은 오직 클래스형 컴포넌트에서만 사용이 가능합니다.

  • getDerivedStateFromError()
  • componentDidCatch()
  • getSnapshotBeforeUpdate()

클래스형 컴포넌트에서 발생할 수 있는 문제

지금까지 클래스형 컴포넌트의 구조와 클래스형 컴포넌트에서 제공되는 생명주기 메서드에 대해 알아보았습니다. 그렇다면 클래스형 컴포넌트가 가지는 문제점으로는 어떤 것들이 있을까요?

  • this 문제 (하단에서 더 자세히 설명할 예정)
  • 데이터 흐름 추적의 어려움
  • 코드 크기 최적화가 어려움 (이름 최소화, 트리 쉐이킹 X)
  • 개발시의 핫 리로딩 문제

함수형 컴포넌트

이번에는 함수형 컴포넌트에 대해 알아보겠습니다. 함수형 컴포넌트는 현재에도 많이 사용되고 있는 만큼, 훅이 등장하기 이전과 이후를 바탕으로 간략하게 알아보도록 하겠습니다.

과거 함수형 컴포넌트

꽤 많은 개발자들이 함수형 컴포넌트는 최근에 생겼다고 오해하지만, 함수형 컴포넌트 자체는 리액트 0.14 버전에서부터 존재하던 유구한 역사를 지닌 컴포넌트 선언 방식입니다. 당시의 함수형 컴포넌트는 별도의 상태나 생명주기 메서드 없이 단순히 어떠한 요소를 정적으로 랜더링하는 것이 목적인 무상태 함수형 컴포넌트였습니다.

아래 코드는 실제 리액트 0.14 버전에서 함수형 컴포넌트를 설명한 코드입니다.

var Aquarium = (props) => {
	var fish = getFish(props.species);
	return <Tank>{fish}</Tank>
}

var Auarium = ({ species }) => <Tank>{getFish(species)}</Tank>

Hook이 등장한 이후의 함수형 컴포넌트

리액트의 16.8 버전에서 훅이 소개된 이후 함수형 컴포넌트가 각광받기 시작했습니다. useState, useEffect 등 훅을 통해 클래스형 컴포넌트의 생명주기 메서드를 비슷하게 구현할 수 있게 되면서 더 이상 함수형 컴포넌트는 무상태에 머무르지 않고 클래스형 컴포넌트와 거의 비슷한 기능을 해낼 수 있게 되었습니다.

함수형 방식으로 선언한 컴포넌트는 다음과 같습니다.

import { useState } from "react";

function FunctionalComponent({ required, text }) {
  const [count, setCount] = useState(0);
  const [isLimited, setIsLimited] = useState(false);

  function handleClick() {
    const newValue = count + 1;
    setCount(newValue);
    setIsLimited(newValue >= 10);
  }

  return (
    <h2>
      Sample Component
      <div>{required ? "필수" : "필수 아님"}</div>
      <div>문자 : {text}</div>
      <div>count : {count}</div>
      <button onClick={handleClick} disabled={isLimited}>
        증가
      </button>
    </h2>
  );
}

함수형 컴포넌트가 클래스형 컴포넌트에 비해 가지는 장점

함수형 컴포넌트가 가지는 가장 큰 장점은 this로 인해 발생하는 문제가 해결된다는 것입니다.

기존 클래스형 컴포넌트에서는 props와 state를 사용하기 위해서 컴포넌트의 this에 접근해야 했습니다. 그러나 클래스형 컴포넌트의 this는 변경 가능한 (mutable한) 값이기 때문에 의도치 않은 props와 state에 접근할 수 있다는 문제로 이어졌습니다.

대표적인 예시로 리액트 개발자 댄 아브라모프의 블로그 글이 있습니다.

아래의 클래스형 컴포넌트를 클릭하면 handleClick에 의해 3초 뒤 props에 있는 user를 alert로 띄워준다. 만약 이 3초 사이에 props를 변경하게 된다면, 클래스형 컴포넌트는 변경 이후의 props 값을 기준으로 메세지가 뜨게 된다. 이러한 결과가 발생한 이유는 this가 가리키는 객체, 즉 컴포넌트의 인스턴스 멤버가 변경 가능한 값이기 때문이다.
이러한 클래스 컴포넌트의 결과는 일반적인 개발자의 의도와는 다른 방향일 것이기에 주의할 필요가 있다.

import React from "react";

export class ClassCompoennt extends React.Component {
  private showMessage = () => {
    alert("Hello" + this.props.user);
  };

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

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

함수형 컴포넌트는 props를 인수로 받기 때문에 클래스형 컴포넌트의 this.props와 달리 그 값을 변경할 수 없습니다. 따라서 위와 같이 3초 뒤에 props가 변경되는 상황에도 함수형 컴포넌트는 변경 이전의 props를 보여주는 것이 가능합니다. 즉 함수형 컴포넌트를 통해 props와 state의 불변성 immutable 을 지킬 수 있게 됩니다.

그 외에도 함수형 컴포넌트가 가지는 장점으로는 아래 사항들이 있습니다.

  • 기본 구조가 간결하다. (constructor, render 등의 함수 사용 필요 X)
  • 데이터 흐름을 추적 및 내부 로직 재사용이 용이하다.
  • 코드 크기 최적화가 가능하다. (함수명 최소화, 트리세이킹 적용)
  • 개발시 핫 리로딩이 가능하다.

그렇다면 클래스형 컴포넌트는 아주 안쓰면 되나요?

대부분의 경우 그러합니다. 새로 리액트 프로젝트를 시작하는 경우에는 상단에서 서술한 클래스형 컴포넌트에서만 사용하는 메서드를 사용해아 하는 등의 특별한 경우를 제외하곤 함수형 컴포넌트를 쓰는 것을 권장합니다. 이는 리액트에서도 공식적으로 제안하는 사항입니다.

다만 과거에 작성된 클래스형 컴포넌트를 반드시 함수형 컴포넌트로 마이그레이션할 필요는 없습니다. 당분간은 리엑트에서 클래스형 컴포넌트를 deprecated할 계획은 없기 때문입니다.


결론

새로 시작하는 리액트 프로젝트에서는 함수형 컴포넌트를 이용합시다.

다만 특별한 상황이나, 기존에 클래스형 컴포넌트로 작성된 레거시를 유지보수하는 등의 상황에서는 클래스형 컴포넌트를 사용하는 것도 좋습니다.

클래스형 컴포넌트에서 함수형 컴포넌트로의 마이그레이션도 가능하나, 현재 리액트가 클래스형 컴포넌트를 deprecated할 계획이 없기에 반드시 마이그레이션할 필요는 없습니다.



오늘은 이렇게 함수형 컴포넌트를 써야하는 이유를 비롯해 클래스형 컴포넌트도 함께 알아보았습니다.
궁금하신 사항이나 의견이 있으시면 댓글로 남겨주세요.

여기까지 읽어주셔서 감사합니다 : D

profile
지속 가능한 개발자

0개의 댓글