[React] Deep Dive 모던 리액트(6) 클래스형&함수형 컴포넌트
FrontEnd/Deep Dive

[React] Deep Dive 모던 리액트(6) 클래스형&함수형 컴포넌트

728x90

 

 

 

함수형 컴포넌트는 리액트 0.14버전부터 있었던 생각보다 오래된 문법이다.

 

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

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

 

 

이당시 함수형 컴포넌트는 클래스형 컴포넌트에서 별다른 생명주기 메서드나 상태가 없이 단순히 render를 하는 경우에만 사용되었다. 함수형 컴포넌트에 훅이 등장하면서 점점 복잡한 클래스 컴포넌트보다 함수형 컴포넌트가 사용되었다.

 

이 과도기에서 문제도 많았고 과연 함수형 컴포넌트와 클래스형 컴포넌트의 차이가 무엇인지 알 필요도 있다. 

 

 

클래스형 컴포넌트

 

리액트 16.8 미만으로 작성된 코드는 대부분 클래스형 컴포넌트이다.

 

import React from 'react'

class SampleComponet extends React.Component{
	render() {
    	return <h2>Sample Component</h2>
        }
    }

 

 

클래스형 컴포넌트를 만들기 위해서는 위처럼 클래스를 선언하고 extends로 만들고 싶은 컴포넌트를 extends하면 된다.

 

import React, { Component } from 'react';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    console.log('컴포넌트가 마운트되었습니다.');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('컴포넌트가 업데이트되었습니다.');
    if (prevState.count !== this.state.count) {
      console.log('count 값이 변경되었습니다.');
    }
  }

  componentWillUnmount() {
    console.log('컴포넌트가 언마운트되었습니다.');
  }

  handleIncrement = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.handleIncrement}>증가</button>
      </div>
    );
  }
}

export default MyComponent;

 

 

 

생명 주기

클래스형 컴포넌트를 사용하면서 가장 많이 언급되는 것이 생명주기라고 한다. 생명주기 메서드가 실행되는 시점은 크게 3가지로 나눌 수 있다.

 

 

1. 마운트 : 컴포넌트가 마운팅(생성)되는 시점

2. 업데이트 : 이미 생성된 컴포넌트의 내용이 변경되는 시점

3. 언마운트 : 컴포넌트가 더이상 존재하지 않는 시점

 

 

 

render()

컴포넌트가 UI를 렌더링하기 위해서 사용된다. 해당 함수는 항상 순수해야하며 부수효과가 없어야 한다. 따라서 render() 안에는 state를 직접 업데이트하는 setState 함수를 호출해서는 안된다.

 

componentDidMount()

클래스형 컴포넌트가 마운트되고 준비가 되었다면 호출되는 생명주기 메서드이다. setState로 state를 변경하는 작업이 가능하다. 일반적으로 state를 다루는 것은 생성자에서 하는것이 좋으며 해당 함수에서는 API호출 후 업데이트, DOM에 의존적 작업들을 하는 것이 좋다.

 

componentDidUpdate()

컴포넌트가 업데이트가 일어난 이후 바로 실행된다. 적절한 조건문을 사용하지 않으면 this.setState가 무한으로 호출되는 문제에 빠질 수 있다.

 

componentWillUnMount()

컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출된다. 메모리 누수 , 불필요한 작동을 막기위한 클린업 함수들을 호출한다.

 

shouldComponentUpdate()

state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링이 되는 것을 막을때 사용하는 메서드. 특별한 성능 최적화 상황에서 고려할 수 있는 함수이다.

 

 

 

Components vs PureComponent

위 두 유형의 차이는 생명주기를 다루는데 있다. 

 

import React, { Component } from "react";

class MyComponent extends Component {
  state = {
    count: 0,
  };

  handleClick = () => {
    this.setState((prevState: any) => ({
      count: prevState.count,
    }));
  };

  render() {
    console.log("Component 렌더링");
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

export default MyComponent;

Component의 경우 버튼을 누르는대로 (state가 업데이트되는대로) 렌더링이 일어난다.

 

 

 

import React, { PureComponent } from "react";

class MyComponent extends PureComponent {
  state = {
    count: 0,
  };

  handleClick = () => {
    this.setState((prevState: any) => ({
      count: prevState.count,
    }));
  };

  render() {
    console.log("PureComponent 렌더링");
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

export default MyComponent;

 

PureComponent 는 버튼을 눌러도 렌더링이 새로 되지않는다. PureComponent는 state값에 대해 얕은 비교를 수행하여 결과가 다른 경우에만 렌더링을 수행하기 때문이다.

 

버튼을 눌러도 랜더링이 되지 않음

 

 

그렇다고 PureComponent로 선언하는 것이 모둔 경우에 좋은 경우는 아닌 것 같다. 우선 PureComponent의 경우 객체의 얕은 비교를 수행하기 때문에 깊은 객체의 데이터 변경은 감지할 수 없다. 

 

만약 대부분 컴포넌트에서 PureComponent로 구성되어 있지만 state가 객체로 되어있다면 성능 향상에 무의미한 효과를 가질 수도 있다. 

 

 

 

 

static getDerivedStateFromProps()

 

가장 최근에 도입된 생명주기 메서드로 render()를 호출하기 직전에 호출된다. static으로 선언되어 있어 this에 접근할 수 없다는 특징이 있다.

 

getSnapShotBeforeUpdate()

DOM이 업데이트되기 직전에 호출된다. DOM이 렌더링 되기 전에 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업처리에 유용하다.

 

 

 

생명주기 메서드를 다이어그램으로 확인하면 아래와 같은 구조를 가지게 된다.

 

 

 

getDerivedStateFromError()

에러상황에서 실행되는 메서드로 자식 컴포넌트에서 에러가 발생했을때 호출된다.

 

 

클래스형 컴포넌트의 한계

클래스형 컴포넌트에서 제공하는 메서드들을 보면 해당 메서드들로 분명 완성도 있는 리액트 애플리캐이션을 만들 수 있을 것 같이 보인다. 그럼에도 현재 함수형 컴포넌트에 밀리는 이유는 무엇일까?

 

1. 데이터의 흐름을 추적하기가 어렵다. 서로 다른 여러 메서드에서 state의 업데이트가 일어날 수 있기 때문에 사람이 읽기도 어렵고 메서드의 순서가 정해져 있지도 않다.

 

2. 내부 로직의 재사용이 어렵다. 공통 로직이 많아질수록 이를 감싸는 고차 컴포넌트 (혹은 props)가 많아지는데 이를 클래스형 컴포넌트에서 매끄럽기 처리하기 쉽지 않다.

 

3. 기능이 많아질수록 컴포넌트의 크기가 커지게 된다. 특히 생명주기 메서드의 사용이 잦아지는 경우 그 크기가 기하급수적으로 커진다.

 

4. 클래스는 함수에 비해 상대적으로 어렵다. 또한 JS에서 클래스의 사용은 비교적 어렵고 일반적이지 않기에 개발자들에게 혼란을 안겨줄 수 있다.

 

5. 코드 크기를 최적화하기 어렵다. 최종 결과물인 번들 크기를 줄이는데 어려움을 겪게 된다.

 

 

 

함수형 컴포넌트

16.8 버전 이후 훅이 등장하면서 각광받게되었다.

 

import React, { useState } from 'react';

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

  const handleIncrement = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={handleIncrement}>증가</button>
    </div>
  );
};

export default MyComponent;

 

 

render 내부에서 필요한 함수를 선언하는 경우 this 바인딩을 신경쓰지 않아도 되며 state가 객체가 아닌 원시값으로 관리되기 때문에 훨씬 사용하기가 편해졌다.

 

 

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

 

함수형 컴포넌트에는 클래스형 컴포넌트에 존재하는 생명주기 메서드가 없다. 함수형 컴포넌트는 props를 받아 단순하게 리액트 요소를 반환하기 때문이다. 따라서 함수형 컴포넌트는 다양한 훅을 활용해서 앞서 말한 생명주기 메서드들의 효과와 "비슷"한 효과를 내게 한다.

 

함수형 컴포넌트 <- 렌더링된 값을 고정

클래스형 컴포넌트 <- 렌더링된 값이 고정되지 않음

 

 

 

https://overreacted.io/how-are-function-components-different-from-classes/

 

How Are Function Components Different from Classes? — overreacted

How do React function components differ from React classes? For a while, the canonical answer has been that classes provide access to more features (like state). With Hooks, that’s not true anymore. Maybe you’ve heard one of them is better for performa

overreacted.io

 

함수형 컴포넌트와 클래스형 컴포넌트로 구성된 아래 코드 두개를 보자. 해당 코드 두개는 같은 방식으로 동작하는 것처럼 보인다.

 

 

둘다 버튼을 누른 경우 3초뒤에 props에 있는 user값을 alert로 표현해준다.

 

함수형 컴포넌트 : 클릭했던 시점의 props값을 기준으로 메시지를 출력

클래스형 컴포넌트 : alert를 띄우는 시점에서의  props값을 기준으로 메시지를 출력

 

 

클래스형 컴포넌트는  props의 값을 항상 this로부터 가져온다. 즉 컴포넌트 인스턴스의 멤버는 변경가능하기 때문에 이런 현상이 일어나게 된다.

 

물론 아래처럼 this.props를 조금 일찍 부르고 이를 함수에 인수로 넘기는 방법이 존재한다.

class ProfilePage extends React.Component {
  showMessage = (user) => {
    alert('Followed ' + user);
  };
 
  handleClick = () => {
    const {user} = this.props;
    setTimeout(() => this.showMessage(user), 3000);
  };
 
  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

 

 

하지만 이는 props가 늘어날수록 코드가 매우 복잡하게 된다.

 

혹은 아래처럼 render에 필요한 값을 넣는 방법이 있다. render()함수가 실행되는 순간에 모든 값을 미리 넣어두는 방법인 것이다.

class ProfilePage extends React.Component {
  render() {
    // Capture the props!
    const props = this.props;
 
    // Note: we are *inside render*.
    // These aren't class methods.
    const showMessage = () => {
      alert('Followed ' + props.user);
    };
 
    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };
 
    return <button onClick={handleClick}>Follow</button>;
  }
}

 

 

 

하지만.. 이런 방식을 사용할꺼면 클래스형 컴포넌트 방식과 거리도 멀고 렌더링 될때마다 함수가 다시 생성되고 할당되기 때문에 성능에도 악영향을 미치게 된다.

 

함수형 컴포넌트는 props를 인수로 받기 때문에 컴포넌트는 그 값을 변경할 수 없기 때문에 그대로 사용하게 되며 state에도 그대로 적용되게 된다.

 

(그래서 useState를 한 함수내에서 여러개를 바꿔도 바뀐 state값이 적용되지 않는다.. 궁금증이 완전히 풀렸다..!)

 

 

클래스형 컴포넌트의 미래

클래스형 컴포넌트가 사라질 계획은 아마 없을 것이다. 리액트 커뮤니티에는 이미 엄청난 수의 클래스형 컴포넌트가 있고 이를 모두 바꾸는건 쉽지 않다. 따라서 클래스형 컴포넌트를 종료시키기는 쉽지 않을 것이다.

 

그게 아니라면 굳이 클래스형 컴포넌트를 작성할 필요는 없어보인다. 함수형 컴포넌트를 작성하는 것이 좋긴 하지만 리액트에 익숙해졌다면 리액트의 오랜 역사동안 작성된 코드들의 흐름을 알기위해 클래스형 컴포넌트의 지식또한 필요하다고 저자는 말하고 있다.

 

 

 

728x90