React의 useEffect

Creating the dots·2022년 1월 16일
0

CS

목록 보기
13/19

React Lifecycle

모든 리액트 컴포넌트는 3단계의 생애주기를 갖는다.

  • Mounting: inserting elements into the DOM
  • Updating: involves methods for updating components in the DOM
  • Unmounting: removing a component from the DOM

모든 단계는 각 메소드를 가지며, 메소드를 사용해서 컴포넌트에 특정 작업을 수행할 수 있다. 클래스 컴포넌트의 경우 React.Component에서 extend하여 메서드를 사용한다.

Class components vs functional components

클래스 컴포넌트에서 생애주기 메소드를 사용했다면, 함수형 컴포넌트에서는 useEffect hook을 사용할 수 있다. 2019년 3월 리액트 16.8이 출시되며 함수형 컴포넌트에서 useState와 useEffect 등의 hook을 사용해 상태와 생애주기 메소드를 구현할 수 있게되었다.

먼저, 클래스 컴포넌트에서는 어떠한 주요 생애주기 메소드를 사용했는지 살펴보도록 하자.

Class components Lifecycle methods

  • componentDidMount
    • Mounting 단계의 메소드로 컴포넌트가 마운트되고 나서 DOM에 렌더링된 후에 호출된다.
    • interval 함수를 실행하거나 비동기 요청을 보낼때 유용하다.
      componentDidMount() {
         fetch(url).then(res => {
          // Handle response in the way you want.
          // Most often with editing state values.
        })
      }
  • componentDidUpdate

    • 최초 렌더링을 제외하고 컴포넌트 업데이트가 실행된 직후에 호출된다.
    • http 요청을 보낼때 유용하다.
    • setState 메소드를 componentDidUpdate 메소드 내에 작성할 수 있지만, 이때 setState가 호출되는 조건을 작성해주어 무한반복되지 않도록 해주어야한다.
      • 만약 조건을 따로 작성해주지 않으면, 컴포넌트가 업데이트되어 componentDidUpdate가 호출되고, setState가 실행되어 다시 컴포넌트가 업데이트되고, componentDidUpdate가 다시 호출되어 무한반복된다.
  • componentWillUnmount

    • 컴포넌트가 DOM에서 제거되기 전에 호출된다.
    • 이벤트 리스너를 삭제하거나, 요청을 취소하는 등의 모든 cleanup에 필요한 함수들이 이 안에서 실행된다.
    • componentWillUnmount 이후에는 DOM에서 삭제되어 렌더링되지 않으므로 setState는 이곳에 작성하면 안된다.

Function components useEffect

useEffect는 함수 컴포넌트에서 side effects가 발생하도록 하는데, 여기서 side effects란, 데이터 가져오기(data fetching), 구독설정(setting up a subscription), DOM 변경(manually changing the DOM) 등을 말한다.

useEffect는 명령형 또는 effect를 발생시키는 함수를 인자로 받는다. useEffect에 전달된 함수는 기본적으로 화면에 렌더링이 완료된 후에 수행되지만, 어떤 값이 변경되었을때만 실행되게 할 수도 있다.

쉽게 말하면, 위에서 확인한 클래스 컴포넌트의 3가지 생애주기 메소드들이 합쳐진 것이 useEffect라고 할 수 있다.

🔍 useEffect는 두번째 인자(dependencies)

useEffect는 두번째 인자로 무엇을 적어주는지에 따라 effect가 실행되는 경우를 제어할 수 있다.

  • 없는 경우 useEffect (()=> effect )
    컴포넌트가 렌더링 될때마다 effect가 호출된다.

  • 빈배열인 경우 useEffect (()=> effect, [])
    컴포넌트가 처음 마운트되어 렌더링되었을때에만 effect가 호출된다.

  • 배열의 요소가 있는 경우 useEffect (()=> effect, [value])
    특정값이 업데이트될때에만 effect가 호출된다.


다음으로 side effect에는 cleanup이 필요한 것과 필요하지 않은 것들이 있는데, 각각을 살펴보도록 하자.

🔍 cleanup이 필요하지 않은 경우

DOM이 업데이트 된 후에 네트워크 요청을 보내거나, DOM을 변형하거나 로깅을 하는 등의 side effect는 cleanup이 필요하지 않다.

  • useEffect의 첫번째 인자에는 렌더링 이후 실행될 함수를 작성한다.
  • 리액트는 이 함수(effect라고 부르기로 함)를 기억해두었다가 DOM을 모두 업데이트하고 난 후 호출한다.
  • useEffect는 상태 변수(state)를 사용할 수 있도록 함수 컴포넌트 내에 작성한다.
  • useEffect의 두번째 인자를 적지 않으면 렌더링 될때마다 함수(첫번째 인자로 작성된 effect)가 실행된다.
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

🔍 cleanup이 필요한 경우

setInterval, setTimeout을 사용해 등록한 작업이나 effect에서 생성한 이벤트 리스너가 중복 생성되지 않도록 제거하거나, 구독을 취소하는 경우 필요하다.

cleanup 함수는 함수를 리턴하는 형태로 작성할 수 있으며, 컴포넌트가 unmount될때 실행되어 순서를 생각해보면 컴포넌트 unmount -> cleanup 함수 실행 -> 컴포넌트 mount -> effect 실행으로 설명할 수 있다.

React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. (공식문서)

🙋 다음 예시에서는 버튼을 누르면,

  • resourceType이 바뀌고,
  • <h1>태그에 resourceType이 나타난다. (렌더링 먼저)
  • console에 'clean up function working'이 찍힌다.
  • console에 'resource changed'가 찍힌다.
import React, { useState, useEffect } from 'react';

function Example() {
  const [resourceType, setResourceType] = useState('posts');
  
  useEffect(() => {
    console.log('resource changed');
    return () => {
      console.log('clean up function working');
    }
  },[resourcetype]);
  
  return (
    <>
      <div>
         <button onClick={ () => setResourceType('posts') }>Posts</button>
         <button onClick={ () => setResourceType('users') }>Users</button>
         <button onClick={ () => setResourceType('comments') }>Comments</button>
      </div>
      <h1>{resourceType}</h1>
    </>
  )
}

🙋 useEffect 내에 cleanup 함수를 작성하지 않았을때, 에러가 발생할 수 있는 경우는 다음과 같다.

  • Home 컴포넌트에서 useEffect로 fetch 요청을 보내서 상태를 업데이트한다.

  • New Blog 버튼을 클릭해 Home 컴포넌트가 unmount되고, New Blog 컴포넌트가 mount된다.

  • fetch 요청을 마친 뒤 상태를 업데이트하려고 했으나, 이미 Home 컴포넌트는 unmount되어 상태를 업데이트할 수 없다.

  • Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function)과 같은 에러가 발생한다.
    👉 unmount된 컴포넌트의 상태를 업데이트 할 수 없고, 현재 애플리케이션에서 메모리가 누수되고 있으므로 useEffect에서 호출하는 구독과 비동기 요청을 cleanup function으로 모두 취소시키라는 에러이다.

    ⭐ 이 에러는 fetch 요청을 취소하는 cleanup 함수를 통해 해결할 수 있으며, 이때, AbortController라는 웹 요청을 취소할 수 있는 객체 인터페이스를 사용한다. (axios 요청에서는 AbortController와 동일한 기능을 하는 cancelToken을 생성해 CancelToken.source 메소드를 사용한다. )

    • new AbortController()로 새로운 객체 인터페이스 생성
    • fetch 요청의 두번째 인자로 AbortSignal 옵션을 전달한다.
    • 만약 요청이 취소되면 catch 블록으로 들어가는데, 이때 에러 이름이 'AbortError'인 경우 또다시 setError로 상태변경을 시키지 않도록 분기를 나누어준다.
      • 만약 여기서 분기를 나눠주지 않으면 또다시 unmount된 컴포넌트의 상태를 변경하는 것이므로 같은 에러가 발생하게 된다.
    • cleanup 함수에서 AbortController.abort()를 호출해 요청을 취소시킨다.
//fetch
useEffect( () => {
  const abortCont = new AbortController(); // AbortController 생성
  
  setTimeout( ()=>{
    fetch( url, {signal: abortCont.signal} )
    .then( res => {
      if(!res.ok) throw Error('could not fetch');
      return res.json();
    })
    .then( data => {
      setData(data);
    })
    .catch( err => {
      if(err.name === 'AbortError'){
        console.log('fetch aborted');
      } else{
        setError(err.message);
      }
    })  
  }, 1000 )
  return () => abortCont.abort(); // DOM 요청이 완료되기 전에 취소
},[url] )

reference

profile
어제보다 나은 오늘을 만드는 중

0개의 댓글