함수형 컴포넌트만 사용해도 괜찮을까?

ho_vi·2024년 4월 12일

TIL

목록 보기
3/7

그동안 개발하면서 쉬운 사용성 때문에 주로 함수형 컴포넌트을 사용해왔었는데 문득 함수형 컴포넌트만을 사용해도 괜찮을까? 하는 의문점이 들었다.

클래스형 컴포넌트

  • 리액트 16.8버전(2019년 2월) 이전까지 활발히 사용되던 컴포넌트
  • 컴포넌트를 선언할 때 Class 문법 사용
  • 리액트에서 제공되는 Component 클래스를 상속하여 생성

클래스형 컴포넌트의 구조

  • class를 사용하여 컴포넌트를 정의하며 render()를 통해 컴포넌트를 렌더링 한다.
  • extends로 만들고 싶은 컴포넌트를 extends 해야한다.
    • React.Component
    • React.PureComponent
import React from "react";

class MyComponent extends React.Component {
  // constructor에서 props를 넘겨주고 state의 기본값을 설정한다.
  private constructor(props) {
    super(props);
    this.state = {
      count: 0,
      isLimited: false,
    };
  }

  // render 내부에서 쓰일 함수를 선언한다.
  private handleClick = () => {
    const newValue = this.state.count + 1;
    this.setState({ count: newValue, isLimited: newValue >= 10 });
  };

  // render에서 이 컴포넌트가 렌더링할 내용을 정의한다.
  public render() {
    // props와 state 값을 this, 즉 해당 클래스에서 꺼낸다.
    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를 초기화 한다.
    • super()는 컴포넌트를 만들면서 상속받은 상위 컴포넌트인 React.Component를 먼저 호출해 해당 클래스가 초기화될 때 부모 클래스의 기능을 사용할 수 있도록 한다.
  • props
    • 함수에 인수를 넣는 것과 비슷하게 컴포넌트에 특정 속성을 전달하는 용도로 쓰인다.
  • state
    • 클래스 컴포넌트 내부에서 관리하는 값을 의미한다.
    • 항상 객체여야 하며 값의 변화가 있을 때마다 리렌더링 된다.
  • 메서드
    • 렌더링 함수 내부에서 사용되는 함수
    • 보통 DOM에서 발생하는 이벤트와 함께 사용된다.
    • 만드는 방식은 크게 3가지로 나뉜다.
      • constructor에서 this 바인드를 하는 방법
        일반적인 함수로 메서드를 만들면 this가 전역 객체가 바인딩되기 때문에 strict로 인해 undefined로 나온다. 따라서 생성된 함수에 bind를 활용해 강제로 this를 바인딩 해야한다.
      • 화살표 함수를 쓰는 방법
        this가 상위 스코프로 결정되는 화살표 함수를 사용하면 굳이 바인딩을 하지 않아도 된다.
      • 렌더링 함수 내부에서 함수를 새롭게 만들어 전달하는 방법
        간편하지만 렌더링이 일어날 때마다 새로운 함수를 생성해서 할당하므로 최적화를 수행하기 어려워진다.
// constructor에서 this 바인드를 하는 방법
private constructor(props){
  this.handleClick = this.handleClick.bind(this)
}

// 화살표 함수를 쓰는 방법
private handleClick = () => {
    const newValue = this.state.count + 1;
    this.setState({ count: newValue, isLimited: newValue >= 10 });
  };

// 렌더링 함수 내부에서 함수를 새롭게 만들어서 전달하는 방법
<button onClick={() => this.handleClick()}>증가</button>

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

  • 마운트(mount) 컴포넌트가 마운팅(생성) 되는 시점
    • render() : UI를 렌더링함 this.setState 호출 금지
    • componentDidMount() : 마운트 되는 즉시 실행, this.setState가 가능하나 웬만하면 지양
  • 업데이트(update) : 이미 생성된 컴포넌트의 내용이 변경(업데이트) 되는 시점
    • render()
    • componentDidUpdate() : DOM을 업데이트하는 등에 쓰임 this.setState가 가능하나 업데이트 될 때마다 호출되기 때문에 적절한 조건문이 필요
  • 언마운트(unmount) : 컴포넌트가 더 이상 존재하지 않는 시점
    • componentWillUnmount() : 컴포넌트가 언마운트되거나 더이상 호출하지 않을 때 사용하며 this.setState를 호출하지 못함

클래스 컴포넌트의 한계

  • 데이터의 흐름을 추적하기 어려움
    • 서로 다른 메서드에서 state의 업데이트가 일어난다.
    • 코드를 읽는 과정에서 state가 어떤 식의 흐름으로 변경되서 렌더링이 일어나는지 판단하기 어렵다.
  • 기능이 많아질수록 컴포넌트의 크기가 커진다.
    • 컴포넌트 내부에 로직이 많아질수록 데이터 흐름이 복잡해서 생명 주기 메서드 사용이 잦아지는 경우 컴포넌트의 크기가 기하급수적으로 커진다.
  • 클래스는 함수에 비해 상대적으로 어렵다.
    • 자바스크립트는 프로토타입 기반의 언어라 클래스는 비교적 뒤에 나온 개념
    • 클래스보다는 함수에 익숙하며 자바스크립트 환경에서는 함수보다 클래스의 사용이 어렵고 일반적이지 않다.

함수 컴포넌트

  • 리액트16.8버전 이전에는 단순한 무상태 컴포넌트를 구현하기 위한 수단
  • 훅(Hook)의 등장으로 무상태 컴포넌트에 상태를 더할 수 있게 되었다.
  • 클래스 컴포넌트와 비교했을 때 간결한 코드와 좋은 사용성
// 클래스형 컴포넌트
import React, { Component } from 'react';

type SampleProps = {
  required?: boolean;
  text: string;
}

type SampleState = {
  count: number;
  isLimited: boolean;
}

class SampleComponent extends Component<SampleProps, SampleState> {
  constructor(props: SampleProps) {
    super(props);
    this.state = {
      count: 0,
      isLimited: false
    };
  }
  
  handleClick = () => {
    const newValue = this.state.count + 1;
    this.setState({
      count: newValue,
      isLimited: newValue >= 10
    });
  }
  
  render() {
    const { required, text } = this.props;
    const { count, isLimited } = this.state;
    
    return (
      <h2>
        Sample Component
        <div>{required ? '필수' : '필수 아님'}</div>
        <div>문자 : {text}</div>
        <div>count : {count}</div>
        <button onClick={this.handleClick} disabled={isLimited}>
          증가
        </button>
      </h2>
    );
  }
}

export default SampleComponent;
//함수형 컴포넌트
import { useState } from 'react'

type SampleProps = }
  required? : boolean
  text: string
}

export function SampleComponent({ required, text } : SampleProps) {
  const [count, setCount] = useState<number>(0)
  const [isLimited, setLimited] = useState<boolean>(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>
    )
}

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

  • 생명주기 메서드의 부재
    • 생명주기 메서드는 React.Component에서 오기 때문에 이를 상속받아서 구현하는 클래스 컴포넌트에서만 생명주기 메서드가 사용가능하다.
    • 함수 컴포넌트에서는 useEffect 훅을 사용해 componentDidMount, componentDidUpdate...등을 비슷하게 구현할 수 있다.
  • 함수 컴포넌트와 렌더링된 값
    • 함수 컴포넌트는 props와 state가 변경된다면 다시 한 번 그 값을 기준으로 함수가 호출된다.
    • 클래스 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링이 일어난다.

클래스 컴포넌트를 공부해야 할까?

  • 클래스 컴포넌트가 당장 사라질 계획은 없다.
    • 시간이 많다면 기존의 클래스형 컴포넌트를 함수형 컴포넌트로 변경하는 것은 고려해볼 만 하지만 단순히 코드를 옮기는 것 이상의 세심한 주의가 필요하다.
  • 예전의 코드를 유지보수하는데 도움이 된다.
    • 이전 리액트 16.8버전에서는 클래스 컴포넌트가 활발히 사용되었기 때문에 기존 리액트 코드를 리팩토링하고 유지보수 하는데에는 많은 도움이 될 수 있다.

마치며

처음 함수 컴포넌트만 사용해도 괜찮은가 라는 질문에는 Yes라고 대답할 수 있을 것 같다. 하지만 숙련된 리액트 개발자가 되려면 그동안 리액트가 어떠한 고민을 통해 발전했는지 이해할 필요가 있다. 클래스 컴포넌트에 대해 배워둔다면 리액트를 매끄럽게 다루는 데에 큰 도움이 될 수 있을 것 같다.

profile
FE 개발자🌱

0개의 댓글