클래스형 컴포넌트와 함수형 컴포넌트의 차이는 무엇일까요?

면접용

React에서는 컴포넌트를 정의하는 두 가지 방식인 클래스형 컴포넌트와 함수형 컴포넌트가 있습니다. 이 둘의 차이는 크게 3가지로 구분할 수 있습니다.

첫째, 상태관리 방법이 다릅니다.

클래스형 컴포넌트는 상태를 this.state로 정의하며, 상태 변경 시 this.setState를 사용합니다. 함수형 컴포넌트는 useState와 useReducer을 사용해 상태를 관리합니다.

둘째, 생명주기 관리 방법이 다릅니다.

클래스형 컴포넌트는 생명주기를 단계별로 분리하여 componentDidMount, componentDidUpdate, componentWillUnmount 등을 명시적으로 구현합니다.

함수형 컴포넌트는 useEffect 하나로 생명주기 로직을 통합적으로 관리하며, 의존성 배열을 활용해 특정 조건에서만 동작하도록 제어할 수 있습니다.

셋째, 성능상 차이가 있습니다.

클래스형 컴포넌트는 컴포넌트가 렌더링될 때마다 새로운 클래스 인스턴스를 생성합니다. 이 과정에서 메모리 할당과 초기화가 필요하며, 함수형 컴포넌트에 비해 무거울 수 있습니다.

또한 React의 최신 렌더링 엔진인 React Fiber는 함수형 컴포넌트와 더 잘 통합된다는 점, React.memo, useCallback, useMemo를 적절히 활용하면 불필요한 렌더링을 방지할 수 있다는 점에서 함수형 컴포넌트가 성능 최적화에 더 용이합니다.

과거에는 클래스형 컴포넌트를 많이 사용했지만, React 16.8 이후 React Hooks가 발표되며 함수형 컴포넌트에서도 라이프사이클 및 상태관리가 가능해져 React에서 함수형 컴포넌트 사용을 더 권장하고 있습니다.


React 컴포넌트

React 컴포넌트란 UI를 구성하는 최소 단위이며, React에서는 컴포넌트를 통해 UI를 작성하고 조합하여 하나의 완성된 어플리케이션을 만들기 위해 사용합니다.

이러한 컴포넌트 작성 방식엔 두 가지가 있는데, 바로 클래스형과 함수형입니다.

import React, { Component } from 'react';

class Timer extends Component {
  constructor(props) {
    super(props);
    // 상태 초기화
    this.state = {
      count: 0,
    };
  }

  componentDidMount() {
    // 컴포넌트가 마운트된 후 타이머 설정
    this.timer = setInterval(() => {
      this.setState((prevState) => ({
        count: prevState.count + 1,
      }));
    }, 1000);
  }

  componentDidUpdate(prevProps, prevState) {
    // 상태가 업데이트될 때 실행
    if (prevState.count !== this.state.count) {
      console.log('Count updated:', this.state.count);
    }
  }

  componentWillUnmount() {
    // 컴포넌트가 언마운트되기 전에 타이머 정리
    clearInterval(this.timer);
  }

  render() {
    return <p>Count: {this.state.count}</p>;
  }
}

export default Timer;
import React, { useState, useEffect } from 'react';

const Timer = () => {
  const [count, setCount] = useState(0);

  // componentDidMount + componentDidUpdate
  useEffect(() => {
    console.log('Count updated:', count);
  }, [count]); // count가 변경될 때 실행

  // componentWillUnmount
  useEffect(() => {
    const timer = setInterval(() => setCount((prev) => prev + 1), 1000);
    return () => clearInterval(timer); // Cleanup
  }, []);

  return <p>Count: {count}</p>;
};

export default Timer;

클래스형 컴포넌트

클래스형 컴포넌트는 ES6의 class 문법을 이용해 작성합니다. 클래스형 컴포넌트는 extends 키워드를 이용하여 React.Component 클래스를 상속받으며, render() 메소드를 이용하여 UI를 정의합니다.

상태와 생명 주기 메서드를 지원하며, React 초기부터 사용되어 왔습니다.

주요 특징

  1. 상태 관리

    • 클래스형 컴포넌트는 this.state를 통해 상태를 관리하며, 상태는 constructor에서 초기화합니다.

      	class Counter extends Component{
      	contstructor(props){
      		super();
      		this.state = {
      			count: 0;
      		}
      	}
      	
      	render(){
      		return <p>Count: {this.state.count}</p>
      	}
      }
  • 클래스형 컴포넌트에서는 상태를 변경하기 위해 반드시 setState 메서드를 사용해야 합니다.

    import React, { Component } from 'react';
    
    class Counter extends Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0,
        };
      }
    
      // 상태 업데이트 함수
      handleIncrease = () => {
        this.setState({ count: this.state.count + 1 });
      };
    
      render() {
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={this.handleIncrease}>Increase</button>
          </div>
        );
      }
    }
    
    export default Counter;
  • setState()의 병합 특성

    • setState()는 상태 객체의 특정 부분만 업데이트 합니다.

    • 기존 상태를 유지하면서 일부 값을 덮어씁니다.

    this.state ={
    	name: "hyorin",
    	age: 25
    };
    
    this.setState({age: 30}); // 기존 name은 유지
  • 비동기적 상태 업데이트

    setState()는 비동기로 작동하므로, 상태 변경 직후에 this.state를 참조하면 이전 상태가 반환될 수 있습니다.

    this.setState({count: this.state.count + 1);
    console.log(this.state.count); // 이전 상태값 출력
    
    this.setState({count: this.state.count+1}, ()=>{console.log(this.state.count})// 변경된 상태값 출력

생명주기 메서드

  1. 마운트 단계

    1. constructor(props)

      컴포넌트가 생성될 때 호출됩니다.

      상태를 초기화하거나, 메서드를 this에 바인딩하는 데 사용합니다.

    2. static getDerivedStateFromProps(props, state)

      컴포넌트가 마운트되기 직전에 호출되며, props로부터 상태를 업데이트하는 데 사용됩니다.

      순수함수로 동작하며, 사이드이펙트를 발생시켜서는 안 됩니다.

      static getDerivedStateFromProps(props, state) {
        if (props.initialCount !== state.count) {
          return { count: props.initialCount };
        }
        return null;
      }
    3. render()

      UI를 렌더링합니다.

      JSX를 반환하며, 순수 함수처럼 동작해야합니다.

      render() {
        return <h1>Count: {this.state.count}</h1>;
      }
    4. componentDidMount()

      컴포넌트가 처음으로 렌더링된 후 호출됩니다.

      DOM작업, API호출, 타이머 설정 등에 사용됩니다.

      componentDidMount() {
        fetch('/api/data')
          .then((response) => response.json())
          .then((data) => this.setState({ data }));
      } 
  2. 업데이트 단계

    1. static getDerivedStateFromProps(props, state)

      업데이트 시에도 호출됩니다.

      새로운 props에 따라 state를 업데이트해야 할 때 사용합니다.

    2. shouldComponentUpdate(nextProps, nextState)

      컴포넌트가 리렌더링될지 결정합니다.

      true를 반환하면 리렌더링, false를 반환하면 리렌더링을 막습니다.

      shouldComponentUpdate(nextProps, nextState) {
        return this.state.count !== nextState.count;
      }
    3. render()

      업데이트 후 컴포넌트의 UI를 렌더링합니다.

    4. getSnapshotBeforeUpdate(prevProps, prevState)

      DOM이 업데이트되기 직전에 호출됩니다.

      스크롤 위치나 DOM의 이전 상태를 기록할 때 유용합니다.

      getSnapshotBeforeUpdate(prevProps, prevState) {
        if (prevProps.scrollPosition !== this.props.scrollPosition) {
          return this.divRef.scrollHeight;
        }
        return null;
      }
    5. componentDidUpdate(prevProps, prevState, snapshot)

      업데이트가 완료된 후 호출됩니다.

      snapshot 값은 getSnapshotBeforeUpdate(prevProps, prevState)에서 반환된 값입니다.

      componentDidUpdate(prevProps, prevState, snapshot) {
        if (snapshot !== null) {
          console.log('Scroll height before update:', snapshot);
        }
      }

3. 언마운트 단계

  1. componentWillUnmount()

    컴포넌트가 DOM에서 제거되기 직전에 호출됩니다.

    타이머 해제, 이벤트 리스너 제거 등 리소스 정리에 사용됩니다.

생명주기 메서드 호출 순서

  • 마운트

    1. constructor()

    2. static getDerivedStateFromProps()

    3. render()

    4. componentDidMount()

  • 업데이트

    1. static getDerivedStateFromProps()

    2. shouldComponentUpdate()

    3. render()

    4. getSnapshotBeforeUpdate()

    5. componentDidUpdate()

  • 언마운트

    1. componentWillUnmount()

React 16 이후 Deprecated된 메서드

  • 아래 메서드들은 React 16.3 이후로 사용이 권장되지 않습니다.

    • componentWillMount()

    • componentWillReceiveProps()

    • componentWillUpdate()

대신 getDerivedStateFromProps()와 getSnapshotBeforeUpdate()를 사용해야 합니다.

import React, { Component } from 'react';

class LifeCycleExample extends Component {
  constructor(props) {
    super(props);
    console.log('Constructor');
    this.state = { count: 0 };
  }

  static getDerivedStateFromProps(props, state) {
    console.log('getDerivedStateFromProps');
    return null;
  }

  componentDidMount() {
    console.log('componentDidMount');
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('shouldComponentUpdate');
    return true;
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('getSnapshotBeforeUpdate');
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('componentDidUpdate');
  }

  componentWillUnmount() {
    console.log('componentWillUnmount');
  }

  render() {
    console.log('Render');
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increase
        </button>
      </div>
    );
  }
}

export default LifeCycleExample;

클래스형 컴포넌트의 장단점

  1. 장점

    1. 명확한 생명주기 메서드

      componentDidMount, componentDidUpdate, componentWillUnmount 등 생명주기 메서드가 명시적으로 구분되어 있어 상태 및 사이드 이펙트 관리를 단계별로 분리할 수 있습니다.

      생명주기 메서드가 정해진 순서대로 호출되기 때문에 컴포넌트의 동작이 예측 가능하고 이해하기 쉽습니다.

    2. 상태와 로직 통합

      상태 관리(this.state)와 메서드(this.setState)가 클래스 내부에서 통합적으로 관리됩니다.

      상태와 메서드가 하나의 객체에 묶여 있어 구조를 파악하기 쉽습니다.

  2. 단점

    1. 코드가 장황해짐

      this 키워드 사용과 constructor, super 호출 등으로 인해 코드가 길어지고 복잡해질 수 있습니다. 클래스 메서드를 사용할 때 this를 바인딩해야 하는 번거로움이 있습니다.

    2. 테스트 및 유지보수 어려움

      클래스형 컴포넌트는 상태와 로직이 컴포넌트 내부에 묶여 있어 테스트와 재사용이 어렵습니다.

      함수형 컴포넌트에서는 Hooks를 통해 로직을 분리(Custom Hooks)하고 재사용 가능하지만, 클래스형에서는 이러한 작업이 번거롭습니다.

    3. 성능 문제

      초기 React에서는 클래스형 컴포넌트가 함수형 컴포넌트보다 약간 더 무거운 경향이 있었습니다.

      함수형 컴포넌트는 React의 최신 렌더링 최적화 방식(React Fiber)과 더 잘 어울립니다.

함수형 컴포넌트

함수형 컴포넌트는 React에서 컴포넌트를 정의하는 간결하고 현대적인 방식입니다. React Hooks의 등장 이후로 함수형 컴포넌트는 상태관리와 생명주기 관리도 가능해지면서 현재는 클래스형 컴포넌트를 대체하는 표준 방식으로 자리잡았습니다.

기본 개념

  • 함수형 컴포넌트는 단순히 JavaScript 함수로 정의됩니다.

  • props를 인자로 받아 JSX를 반환합니다.

  • React Hooks(React 16.8 이후)를 사용하면 상태 관리와 생명주기 메서드 구현도 가능합니다.

import React from 'react';

const MyComponent = (props) => {
  return <h1>Hello, {props.name}!</h1>;
};

export default MyComponent;

함수형 컴포넌트의 특징

  1. 심플한 구조

    클래스형 컴포넌트와 달리 class, constructor, this를 사용하지 않아 코드가 간결하고 직관적입니다.

    상태관리도 함수형 문법으로 처리할 수 있습니다.

    const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;
  2. React Hooks로 상태 관리 가능

    React 16.8 이전에는 함수형 컴포넌트에서 상태 관리가 불가능했지만, Hooks(useState, useEffect 등)의 도입으로 가능해졌습니다.

    import React, { useState } from 'react';
    
    const Counter = () => {
      const [count, setCount] = useState(0); // 상태 선언
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increase</button>
        </div>
      );
    };
    
    export default Counter;
  3. React Hooks로 생명주기 관리

    함수형 컴포넌트에서는 생명주기 메서드 대신 useEffect 훅을 사용하여 동일한 동작을 구현할 수 있습니다.

    클래스형 메서드
    useEffect 구현

    componentDidMount

    useEffect(()⇒{},[]);

    componentDidUpdate

    useEffect(()⇒{},[dependencies]);

    componentWillUnmount

    useEffect(()⇒{ return ()⇒{} },[]);

    import React, { useState, useEffect } from 'react';
    
    const Timer = () => {
      const [count, setCount] = useState(0);
    
      // componentDidMount + componentDidUpdate
      useEffect(() => {
        console.log('Count updated:', count);
      }, [count]); // count가 변경될 때 실행
    
      // componentWillUnmount
      useEffect(() => {
        const timer = setInterval(() => setCount((prev) => prev + 1), 1000);
        return () => clearInterval(timer); // Cleanup
      }, []);
    
      return <p>Count: {count}</p>;
    };
    
    export default Timer;

함수형 컴포넌트의 장단점

  1. 장점

    1. 간결함

      보일러플레이트 코드가 적고 읽기 쉽습니다.

      this 바인딩 문제를 걱정할 필요가 없습니다.

    2. Hooks로 더 많은 기능 제공

      상태관리, 생명주기 메서드, 컨텍스트 관리 등이 가능합니다.

    3. 테스트와 유지보수 용이

      순수 함수처럼 동작하므로 테스트하기 쉽습니다.

    4. 최신 React 표준

      React 공식 문서와 커뮤니티에서도 함수형 컴포넌트를 권장합니다.

    5. 재사용 가능한 로직

      Custom Hooks로 비즈니스 로직을 분리하고 재사용성을 높일 수 있습니다.

  2. 단점

    1. Hooks의 초기 학습 곡선

      useEffect 등 일부 혹은 초보자에게 혼란을 줄 수 있습니다.

    2. 복잡한 로직 관리

      중첩된 useEffect나 상태 관리가 많아지면 가독성이 떨어질 수 있습니다.

    3. 이전 상태 유지 어려움

      클래스형 컴포넌트에서의 생명주기 메서드처럼 명확한 단계 구분이 없어, 복잡한 로직이 섞이기 쉽습니다.

Last updated