avatar
yoosioff

2-3. 클래스 컴포넌트와 함수 컴포넌트

May 25
·
18 min read

0. 16.8버전 이전의 함수 컴포넌트

0.14 버전에서 소개된 함수 컴포넌트는 staless functional component, 이른바 무상태 함수 컴포넌트였다. 별도의 상태 없이 단순히 어떠한 요소를 정적으로 렌더링하는 것이 목적이었다.

즉, 함수 컴포넌트는 클래스 컴포넌트에서 별다른 생명주기 메서드나 상태가 필요없이 render만 하는 경우에만 제한적으로 사용됐다. 함수 컴포넌트가 각광받기 시장한 것은 16.8버전에서 hook이 등장한 이우였다.

함수 컴포넌트에서 hook이 등장한 이후 함수 컴포넌트에서 상태나 생명주기 메서드 비슷한 작업을 흉내낼 수 있게되자 상대적으로 보일러플레이트가 복잡한 클래스 컴포넌트보다 함수 컴포넌트를 많이 쓰기 시작했다.

1. 클래스 컴포넌트

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

  • React.Component

  • React.PureComponent

// Component 제네릭에 props, state를 순서로 넣어준다.
class SampleComponent extends React.Component<SampleProps, SampleState>
  • constructor(): 컴포넌트가 초기화되는 시점에 호출된다. 컴포넌트의 state를 초기화해주는 역할을 한다.

  • super(): 컴포넌트를 만들면서 상속받은 상위 컴포넌트에 접근할 수 있게 도와준다.

  • props: 함수 인자에 넣는 것과 비슷하게 컴포넌트에 특정 속성을 전달하는 용도로 쓰인다.

  • state: 클래스 컴포넌트 내부에서 관리하는 값을 의미한다. 값은 항상 객체여야만 하고, 해당 값에 변화가 있을 때 리렌더링이 발생한다.

  • method: 렌더링 함수 내부에서 사용되는 함수이며, 보통 DOM에서 발생하는 이벤트와 함꼐 사용된다.

    • constructor에서 this를 바인드 하는 방법: 생성된 함수에 bind를 활용해 강제로 this를 바인딩해야 한다. (일반적인 함수로 메서드를 만든다면 this가 undefined로 나온다. 생성자가 아닌 일반 함수로 호출하게 되면 this 전역 객체가 바인딩 되기 때문이다.

      this.handleClick = this.handleClick.bind(this)
      
    • 화살표 함수를 쓰는 방법: 실행 시점이 아닌 작성 시점에 this가 상위 스코프로 결정되는 화살표 함수를 사용한다면 굳이 바인딩을 하지 않아도 된다.

      const sampleFunction = () => {}
      
    • 렌더링 함수 내부에서 함수를 새롭게 만들어 전달하는 방법: 메서드 내부에서 새롭게 함수를 만들어서 전달하는 방법이다.

      <button onClick={()=> this.handleClick()}>증가</button>
      

      그러나 이 방법을 사용하게 되면 매번 렌더링이 일어날 때마다 새로운 함수를 생성해서 할당하게 되므로 최적화를 수행하기 매우 어려워진다.

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

클래스 컴포넌트를 사용하면서 가장 많이 언급되는 것이 바로 생명주기이다. 클래스 컴포넌트는 많은 코드가 생명주기 메서드에 의존하고 있다.

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

  • 마운트(mount): 컴포넌트가 마운트(생성)되는 시점

    • 가상 돔에 붙는 순간

  • 업데이트(update): 이미 생성된 컴포넌트 내용이 변경(업데이트)되는 시점

    • 상태나 UI가 변경이 될 때

  • 언마운트(unmount): 컴포넌트가 더이상 존재하지 않는 시점

    • 컴포넌트가 사라질 때, DOM 트리가 빠질 때

render()

리액트 클래스 컴포넌ㅌ의 유일한 필수 값으로 항상 쓰인다.

목적: 컴포넌트 UI를 렌더링 하기 위해서 쓰인다.

실행 시점: 마운트와 업데이트 과정에서 일어난다.

💡 한가지 주의해야 할 점은 render() 함수는 항상 순수해야 하며 부수 효과가 없어야 한다.

부수 효과가 있으면 return 값이 변경될 수 있기 때문에 화면에 나오는 부분을 예측할 수 있도록 순수해야 한다.

같은 입력값(props 또는 state)이 들어가면 항상 같은 결과물을 반환해야 한다는 뜻이다. 따라서 render() 내부에서 state를 직접 업데이트하는 this.setState를 호출해서는 안된다.

state를 변경하는 일은 클래스 컴포넌트의 메서드나 다른 생명주기 메서드 내부에서 발생해야 한다. 그러므로 이 함수는 항상 최대한 간결하고, 깔끔하게 작성하는 것이 좋다.

componentDidMount()

클래스 컴포넌트가 마운트되고 준비가 됐다면 그 다음으로 호출되는 메서드가 바로 componentDidMount()이다.

목적: render와 다르게 내부에서, state 값을 변경할 수 있기 때문에 UI를 업데이트 하기 위해 쓰인다.

실행 시점: 컴포넌트가 마운드되고 준비되는 즉시 실행된다.

💡 componentDidMount는 만능이 아니며 성능 문제를 일으킬 수 있음에 주의하자.

일반적으로 state를 다루는 것은 생성자(constructor)에서 하는 것이 좋다.
componentDidMount에서 this.setState를 허용하는 것은 다음과 같다.

  1. 생성자 함수에서 할 수 없는 것

  2. API 호출 후 업데이트

  3. DOM에 의존적인 작업

componentDidUpdate()

목적: 일반적으로 state나 props의 변화에 따라 DOM을 업데이트하는 등에 쓰인다.

실행 시점: 컴포넌트 업데이트가 일어난 이후에 바로 실행된다.

💡 그러나 적절한 조건문으로 감싸지 않는다면 this.setState가 계속해서 호출되는 일이 발생할 수 있다.

componentWillUnmount()

목적: 메모리 누수나 불필요한 작동을 막기 위한 클린업 함수를 호출하기 위한 최적의 메소드다.

실행 시점: 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출된다.

이 메서드 내에서는 this.setState를 호출할 수 없다, 다음과 같은 상황에서 주로 사용한다.

  1. 이벤트 제거 (window.removeEventListener)

  2. API 호출을 취소

  3. setInterval, setTimeout으로 생성된 타이머 제거

shouldComponentUpdate()

목적: state나 props 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막고 싶을 때 사용한다.

실행 시점: this.setState가 호출되면 리렌더링을 일으킨다. 그러나 해당 생명주기 메서드는 컴포넌트에 영향을 받지 않는 변화에 대해 정의할 수 있다.

💡 일반적으로 state 변화에 따라 컴포넌트가 리렌더링 되는 것은 굉장히 자연스러운 일이므로, 해당 메서드를 사용하는 것은 성능 최적화 상황에서만 고려해야 한다.

앞서 클래스 컴포넌트에는 두가지 유형, 즉 Component와 PureComponent가 있다. 이 둘의 차이점이 바로 해당 생명주기를 다루는데 있다.

Component의 경우 버튼을 누르는 대로 즉 state가 업데이트되는 대로 렌더링이 일어나지만, PureComponent는 state의 값이 업데이트되지 않아서 렌더링이 일어나지 않았다. PureComponent는 state 값에 대해 얕은 비교를 수행해 결과가 다를 때만 렌더링을 수행한다.

PureComponent는 먼저 얕은 비교만 수행하기 때문에, state가 객체와 같이 복잡한 구조의 데이터 변경은 갑지하지 못하기 때문에 개발자가 생각한대로 제대로 작동하지 않는다. 또한 애플리케이션에서 대다수의 컴포넌트가 PureComponent로 구성돼있다면 오히려 성능에 역효과를 미칠 수 있다.

PureComponent의 경우 객체의 참조값이 바뀌어야 업데이트 된다, hook의 경우는 객체의 참조값을 갈아치운다.

지금까지 언급한 생명주기 메서드 정리

렌더단계: virtualDOM에서 그려지는 과정

커밋단계: 실제 브라우저에 반영하는 단계

static getDerivedStateFromProps()

이제는 사라진 componentWillreceiveProps를 대체할 수 있는 메서드다.

실행 시점: render()를 호출하기 직전에 호출된다.

static으로 되어 있어 this에 접근할 수 없다. 여기서 반환하는 객체는 해당 객체의 내용이 모든 state로 들어가게 된다.

💡 명심해야 할 점은 해당 메서드도 마찬가지로 모든 redner() 실행 시에 호출된다는 점이다.

getSnapShotBeforeUpdate()

최근에 도입된 메서드중 하나로, componentWillUpdate()를 대체할 수 있는 메서드다.

실행 시점: DOM이 업데이트되기 직전에 호출된다.

다음과 같은 상황에서 주로 사용한다.

  1. 렌더링되기 전에 윈도우 크기를 조절

  2. 스크롤 위치 조정

⭐ getDerivedStateFromError()

목적: 자식 컴포넌트에서 에러가 발생했을 때 렌더링 하기 위해 사용한다.

실행 시점: 자식 컴포넌트에서 에러가 발생했을 때 호출된다.

getDerivedStateFromError(error)를 인수로 받는다. 여기서 error는 하위 컴포넌트에서 발생한 에러를 말한다.

💡 getDerivedStateFromError는 반드시 state 값을 반환해야 하며, 부수효과를 발생해서는 안된다.

그 이유는 getDerivedStateFromError의 실행시점 때문이다. 자식 컴포넌트에서 에러가 발생했을 때 어떻게 렌더링할 지 결정하는 용도로 제공되기 때문에 반드시 미리 정의해둔 state 값을 반환해야 한다.

또한 여기서 말하는 부수효과란 에러에 따른 상태 state를 반환하는 것 외의 모든 작업을 의미한다. (ex. console.error)

⭐ componentDidCatch()

해당 3가지 메서드 getSnapShotBeforeUpdate, getDerivedStateFromError, componentDidCatch는 아직 리액트 훅으로 구현돼 있지 않기 때문에 이 3가지 메서드가 필요하다면 반드기 플래스 컴포넌트를 사용해야 한다.

목적: 자식 컴포넌트에서 에러가 발생했을 때 렌더링 하기 위해 사용한다.

실행 시점: 자식 컴포넌트에서 에러가 발생했을 때 호출된다, getDerivedStateFromError에서 에러를 잡고 state를 결정한 이후에 실행된다.

componentDidCatch(error, 어떤 컴포넌트가 에러를 발생시켰는지 해당 컴포넌트의 정보를 가지고 있는 info)를 인자로 받는다.

💡 componentDidCatch는 getDerivedStateFromError에서 하지 못했던 부수 효과를 수행할 수 있다.

getDerivedStateFromError는 렌더 단계에서 실행되지만, componentDidCatch는 커밋단계에서 실행디기 때문이다.

일반적으로 앞의 두 메서드는 ErrorBoundary, 즉 에러 경계 컴포넌트를 만들기위해 많이 사용된다. 그러나 모든 에러를 잡아낼 수 있는 것은 아니다. ErrorBoundary 외부에 있는 에러는 잡을 수 없다.

이를 반대로 생각하여 ErrorBoundary를 여러 개 선언해서 컴포넌트 별로 에러 처리를 다르게 적용할 수 있다. 이렇게 하면 에러가 발생한 컴포넌트 트리 영역만 별도로 처리해서 애플리케이션 전체에 에러가 전파되어 표시되는 것을 방지할 수 있다.

해당 메서드는 주로 에러 로깅할 때 사용한다. (GA, Centry) → 특정 컴포넌트에서 어떠한 에러가 발생했는지 알 수 있기 때문

클래스 컴포넌트의 한계

  • 데이터 흐름을 추적하기 어렵다.

    • 데이터를 바꿀 수 있는 위치가 너무 많다.(DidUpdate, DidMount…)

  • 애플리케이션 내부 로직의 재사용이 어렵다.

    • 로직을 재사용하려면 HOC를 통해 해결이 가능하지만, 고차 컴포넌트를 많이 감싸게 되면 래퍼 지옥에 빠져들 위험성이 커진다.

  • 기능이 많아질수록 컴포넌트의 크기가 커진다.

  • 클래스는 함수에 비해 상대적으로 어렵다.

  • 코드 크기를 최적화하기 어렵다.

    • 번들 크기를 줄이는 게 어렵다.

  • 핫 리로딩을 하는 데 상대적으로 불리하다.

트리 쉐이킹 → 나무를 흔든다 → 나무에 걸려있는게 떨어진다

중복선언된 로직을 호이스팅 하여 미사용된 함수/변수를 지운다. (메모리 낭비이기 때문)

변수명을 바꿔준다 → 변수 명을 길게 한 것을 최소화하기 위해

핫 리로딩 → 변경사항이 일어나면, 앱을 다시 시작하지 않고서도 해당 변경된 코드만 업데이트하여 변경 사항을 빠르게 적용하는 기법

함수 컴포넌트

  1. render 내부에 필요한 함수를 선언할 때 this 바인딩을 조심할 필요가 없음

  2. state는 객체각 아닌 각각의 원시값으로 관리되어 훨씬 사용하기 편하다.

  3. 렌더링하는 코드인 return에서 굳이 this를 사용하지 않아도 props와 state에 접근할 수 있게 됐다.







안녕하세요 :)