2.3 클래스 컴포넌트와 함수 컴포넌트

gonn-i·2025년 3월 20일
post-thumbnail

함수 컴포넌트는 0.14버전에서부터 존재하던 선언방식인데, 단 당시엔 무상태 컴포넌트 라서 그저 요소를 상태 관리없어 정적으로 랜더링하는 것이 목적이었다.
(별다른 생태 주기 메서드나 상태가 필요없이 render 만 하는 경우에만 사용)

훅이 등장해서야 많은 작업을 할 수 있었기에, 이전까지는 클래스 컴포넌트를 사용했다.

2.3.1 클래스 컴포넌트

기본적으로 클래스 컴포넌트를 만들려면, 클래스 선언 후 extends로 만들고 싶은 컴포넌트를 상속받아야 한다.

React.Component / React.PureComponent

Class 내부 살펴보기

  • constructor (생성자)

    • 컴포넌트가 초기화되는 시점에 호출된다.

    • 여기에서 state 초기화를 할 수 있다.

    • super()를 호출해 부모 클래스(React.Component)의 생성자를 실행해야 한다.
      - 이를 통해 React의 내부 기능을 사용할 수 있도록 초기 설정이 이루어진다.

      • super(props)를 호출하면 부모 클래스에서 props를 사용할 수 있도록 전달할 수 있다.
      ES2022의 클래스 필드(Class Fields) 문법 덕분에 
      constructor 없이도 state를 초기화할 수 있는데, state = { count: 1 }처럼 선언 가능하다.
      
      ES2022 이상의 환경에서 지원되므로 구형 브라우저에서는 
      Babel의 @babel/plugin-proposal-class-properties 플러그인을 사용해 트랜스파일링해야 한다.
      
  • props (속성)

    • 컴포넌트 외부에서 값을 전달하는 용도로 쓰인다.
    • 부모 컴포넌트에서 값을 변경할 수 있지만, 자식 컴포넌트에서는 읽기 전용임.
  • state (상태)
    - 컴포넌트 내부에서 관리하는 값을 의미한다.

    • 반드시 객체 형태여야 하며, state가 변경되면 컴포넌트가 리렌더링된다.
      class SampleComponent extends React.Component {
    state = { count: 0 };
    
      render() {
        return <div>{this.state.count}</div>;
      }
    }
    		```
    
  • 메서드

    • 클래스 컴포넌트에서 렌더링 함수 내부에서 사용되는 함수이다.
    • 보통 이벤트 핸들러(onClick, onChange 등)와 함께 사용된다.
    • this 바인딩 문제를 해결하는 방법은 3가지가 있다.
      • constructor에서 this 바인딩:
        this가 undefined가 되는 문제를 방지하기 위해 bind()를 사용해야 한다.
      • 화살표 함수 사용:
        화살표 함수는 작성 시점에 this가 결정되므로 bind()가 필요 없다.
      • 렌더링 함수 내부에서 새 함수 생성 (지양해야 함):
        매 렌더링마다 새로운 함수가 생성되어 이벤트 핸들러가 매번 새롭게 만들어짐 → 메모리 낭비 초래.
class SampleComponent extends React.Component {
  state = { count: 0 };

  // 1. Constructor에서 this 바인딩
  // constructor(props) {
  //   super(props);
  //   this.handleClick = this.handleClick.bind(this);
  // }

  // 2. 화살표 함수 사용
  handleClick = () => {
    console.log(this.state.count);
  };

  render() {
    return (
      // 3. 렌더링 시 함수 생성 (지양)
      // <button onClick={() => this.handleClick()}>Click</button> 

      // 2번 방식인 화살표 함수 사용
      <button onClick={this.handleClick}>Click</button> 
    );
  }
}

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

생명주기 메서드가 실행되는 시점

  • 마운트 : 컴포넌트가 생성되는 시점
  • 업데이트 : 이미 생성된 컴포넌트의 내용이 변경되는 시점
  • 언마운트 : 컴포넌트가 더 이상 존재하지 않는 시점
  • render()

    • 컴포넌트가 UI를 랜더링하기 위해 쓰이며, 이 랜더링은 마운트와 업데이트 과정에서 일어난다.
    • render 함수는 항상 순수해야 하기 때문에, 내부에서 state 를 직접 변경하는 this.setState를 호출해서는 안된다.
      • 여기에서 말하는 순수성이란, 같은 입력값이 들어가면, 항상 같은 결과를 반환해야 한다는 뜻
  • componentDidMount()

    • 마운트 후 호출: 컴포넌트가 화면에 렌더링(마운트)된 후 즉시 호출된다.
    • 상태 변경 가능: this.setState()로 상태를 변경할 수 있다.
    • 주의) 성능 문제를 일으킬 수 있으므로 일반적으로 state를 다루는 것은 생성자에서 하는 게 좋다. this.setState를 허용하는 것은 API 호출이나 DOM 관련 작업을 위해서 이다.
  • componentDidUpdate()

    • 업데이트 후 호출: 일반적으로 state나 props의 변화에 따라 DOM을 업데이트하는 등에 사용된다.
    • 상태 변경 가능: this.setState()로 상태를 변경할 수 있으나, 적절한 조건문이 없다면 불필요하게 상태변경함수가 호출된다.
  • componentWillUnmount()

    • 언마운트 혹은 더 이상 사용되지 않기 직전에 호출된다.
    • 메모리 누수나 불필요한 작동을 막기 위한 클린업 함수를 호출하기 위한 최적의 위치
      • 이벤트를 지우거나, API 호출을 취소하거나, setInterval, setTimeout 타이머를 지우는 작업 등을 한다.
    • 상태 변경 불가능: this.setState()로 상태를 변경할 수 없다 ❌
  • shouldComponentUpdate()

    • 리렌더링 방지: state나 props 변경으로 인한 리렌더링을 막고 싶을 때 사용한다.
      • 특정 조건에서만 리렌더링을 허용하여 성능을 최적화 가능!

      • this.setState() 호출 시 컴포넌트는 기본적으로 리렌더링을 트리거하는데, shouldComponentUpdate()에서 false를 반환하면 리렌더링을 방지할 수 있다.

        	shouldComponentUpdate(nextProps, nextState) {
            // props의 title이 같지 않거나 state의 input이 같지 않은 경우에만 컴포넌트를 업데이트
            return this.props.title !== nextProps.title || this.state.input !== nextState.input;
          }
    • PureComponent 가 shouldComponentUpdate()를 사용할 수 있기에, Component와 랜더링에 있어 차이점을 갖는다.
      • 얕은 비교: PureComponent는 props와 state의 얕은 비교를 수행하여, 값이 변경되지 않으면 리렌더링을 방지한다.
        • 단! 복잡한 객체나 배열 상태 변화는 얕은 비교로 감지되지 않으므로, 상태가 객체일 때는 문제가 발생할 수 있다.
  • static getDerivedStateFromProps()

    • render()를 호출하기 직전에 호출된다.
    • static으로 선언돼 있어 this에 접근할 수 없다.
    • 여기에서 반환하는 객체는 모두 state로 들어가게 된다.
  • getSnapShotBeforeUpdate()

    • DOM이 업데이트되기 직전에 호출된다. (componentWiUUpdate()를 대체할 수 있는 메서드)
    • 반환되는 값은 componentDidUpdate로 전달되며, 랜더링 전 원도우 크기 조절 / 스크롤 위치 조정 등의 작업 처리에 유용하다.
  • getDerivedStateFromError()

    • 에러 상황에서 실행 : 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드이다.
    • static 메서드로, 하위 컴포넌트에서 발생한 에러정보인 error를 인수로 받는다.
    • 해당 메서드는 항상 state 값을 반환한다.
      • 반환된 state 값은 컴포넌트의 상태로 설정되어 에러 발생 시 보여줄 UI를 결정하는 데 사용하기 때문
  • componentDidCatch()

    • 에러 상황에서 실행 : getDerivedStateFromError에서 에러를 잡고 state를 결정한 이후에 실행
    • 인수로는 getDerivedStateFromError와 동일한 error, 그리고 정확히 어떤 컴포넌트가 에러를 발생시켰는지 정보를 가지고 있는 info 를 받는다.
    • getDerivedStateFromError와 달리, 부수 효과를 수행하며 에러 로깅을 할 수 있다.

getDerivedStateFromError() 와 componentDidCatch()는 ErrorBoundary, 에러 경계 컴포넌트를 만들기 위한 목적으로 많이 사용된다. 이를 통해 전역에서 처리되지 않은 에러를 처리하기 위한 용도로 사용된다.

다만, ErrorBooundary에서 주의할 점이 있다.
1. 개발모드와 프로덕션 모드에서 동작 차이
개발 모드에서는 에러가 window까지 전파되지만, 프로덕션 모드에서는 componentDidCatch()가 잡지 못한 에러만 전파된다. (개발 모드에서 에러 처리가 중복될 수 있음)
2. componentDidCatch()의 두 번째 인수 errorInfo
errorInfo는 에러가 발생한 컴포넌트의 위치를 추적하는 정보인데, errorInfo.componentStack 를 통해 어느 컴포넌트에서 에러가 발생했는지 알 수 있다.
이 정보는 Function.name 또는 displayName에 따라 결정되는데 익명함수(memo(() => { ... }))로 선언하면, 컴포넌트를 선언할 수 없어 정보가 제대로 표시되지 못한다.


클래스 컴포넌트의 한계

클래스 컴포넌트의 단점
  1. 데이터 흐름 추적 어려움

    • 여러 생명주기 메서드에서 state가 업데이트됨 → 흐름 파악 어려움
    • 메서드 작성 순서가 강제되지 않아 코드 읽기 불편
  2. 내부 로직 재사용 어려움

    • 고차 컴포넌트(HOC) 사용 시 → wrapper hell 발생
    • 상속(PureComponent) 사용 시 → 복잡도 증가
  3. 기능 증가 시 컴포넌트 크기 증가

    • 생명주기 메서드 사용이 많아질수록 컴포넌트 크기 기하급수적 증가
  4. 클래스 문법의 복잡성

    • 자바스크립트의 this 특성으로 인해 초보자에게 혼란 유발
    • 함수형 컴포넌트보다 사용이 어렵고 덜 직관적
  5. 코드 크기 최적화 어려움

    • 트리 쉐이킹(Tree Shaking) 미적용 → 사용하지 않는 메서드도 번들에 포함됨
    • 메서드 이름이 최소화되지 않아 번들 크기 증가
  6. 핫 리로딩 불리

    • 핫 리로딩 : 코드 변경 사항을 즉시 반영하여 애플리케이션을 다시 시작하지 않고도 업데이트된 내용을 적용하는 기술
    • 코드 변경 시 즉각적인 반영 어려움
    • (why)
      클래스 컴포넌트는 instance(인스턴스) 내부에서 state를 관리하기 때문에, render() 함수가 수정되면 새 instance를 생성해야 하며, 기존 state는 초기화됨
      <-> 함수형 컴포넌트는 클로저를 활용하여 state를 유지할 수 있다고 함,

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

생명주기 메서드의 부재

  • 클래스 컴포넌트: React.Component를 상속받아 componentDidMount, componentDidUpdate, componentWillUnmount 등의 생명주기 메서드 사용한다.
  • 함수 컴포넌트: useEffect 훅을 사용하여 비슷한 동작을 구현하지만, 생명주기 메서드와는 엄연하게 같다고는 할 수 없다. useEffect는 부수 효과 처리용으로 설계되었다.

함수 컴포넌트 vs 클래스 컴포넌트: 렌더링 값 고정 여부

차이점

  • 클래스 컴포넌트: this.props를 사용하여 props를 가져오고, 컴포넌트의 인스턴스가 변할 때마다 업데이트된 값을 읽는다.
  • 함수 컴포넌트: props는 함수의 인수로 전달되어, 렌더링 시점의 값을 기준으로 렌더링된다. 즉, 클릭한 시점의 값을 기준으로 메시지가 뜬다.

왜 차이가 발생하는데?

  • 클래스 컴포넌트this가 가리키는 객체가 변할 수 있으므로, 렌더링 중에 props 값이 바뀌면 변경된 값이 반영된다.
  • 함수 컴포넌트this가 없고, props와 state가 함수의 인수로 들어오므로 그 시점의 값이 렌더링에 사용된다.

해결 방법

  • 클래스 컴포넌트에서는 this.props를 더 일찍 호출하여 값을 처리하거나, 함수의 인수로 넘겨줄 수 있다. (그치만, state과 props가 늘어날 수록 코드가 복잡해짐)
  • render 에 필요한 값 넣고 필요한 순간에 부르기 (랜더링될때마다 함수가 다시 할당되기에 성능저하 야기)

2.3.3 정리

클래스 컴포넌트는 당장 사라질 계획은 없고, 기존 코드가 많아 이를 모두 함수 컴포넌트로 바꾸는 건 쉽지 않다. 함수 컴포넌트로 전환하려면 충분한 경험과 실수 감지 시스템이 필요하고, 그렇지 않으면 굳이 바꿀 필요는 없다. 리액트를 처음 배우거나 새로운 프로젝트를 시작할 땐 함수 컴포넌트를 쓰는 게 좋다. 그치만 어느 정도 익숙해졌다면 클래스 컴포넌트도 공부할 필요가 있는데, 일부 에러 처리는 클래스 컴포넌트에서만 가능하기 때문이다.

profile
https://gonnn-i.tistory.com/

0개의 댓글