React useEffect 완벽 가이드: 함수형 컴포넌트의 사이드 이펙트 관리법

ClydeHan·2024년 9월 2일
5

useEffect

useEffect React basic Hook

이미지 출처: copycat.dev

📌 useEffect란?

useEffect는 React에서 가장 중요한 훅 중 하나로, 함수형 컴포넌트에서 사이드 이펙트(side effects)를 처리할 수 있게 해주는 도구이다. 사이드 이펙트란 컴포넌트의 렌더링과 직접적으로 관련되지 않은 작업을 말하며, 서버에서 데이터를 가져오거나, DOM을 직접 수정하거나, 구독(subscription) 설정과 같은 작업들이 이에 해당한다. useEffect를 통해 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 할 수 있어, 컴포넌트 생명주기와 관련된 작업을 효과적으로 관리할 수 있다.


📌 왜 useEffect를 사용해야 하는가?

useEffect는 React의 함수형 컴포넌트에서 사이드 이펙트를 처리하기 위해 설계되었다. 기존의 클래스형 컴포넌트에서는 컴포넌트의 생명주기 메서드를 통해 특정 시점에서 코드를 실행할 수 있었다. 예를 들어, 컴포넌트가 마운트되거나 업데이트될 때 componentDidMount, componentDidUpdate 같은 메서드를 사용했다. 하지만 함수형 컴포넌트는 이러한 생명주기 메서드가 없기 때문에, 이와 유사한 기능을 수행할 수 있는 메커니즘이 필요했다. 이 문제를 해결하기 위해 React 팀은 useEffect를 도입했다.


💡 함수형 컴포넌트의 단순성 유지

함수형 컴포넌트는 클래스형 컴포넌트에 비해 코드가 단순하고 가독성이 높다는 장점이 있다. 그러나 상태 관리와 사이드 이펙트 관리를 위해 생명주기 메서드를 직접 구현해야 한다면, 이러한 단순성이 사라질 수 있다. useEffect는 이러한 문제를 해결하면서도 함수형 컴포넌트의 단순성을 유지할 수 있게 해준다.


💡 모든 사이드 이펙트를 하나의 훅으로 관리

useEffect는 데이터 가져오기, DOM 업데이트, 구독 설정 등 다양한 사이드 이펙트를 하나의 훅에서 관리할 수 있게 해준다. 이는 클래스형 컴포넌트에서 각 작업을 다른 생명주기 메서드에서 처리해야 했던 것과 비교해 훨씬 효율적이다.


💡 코드 중복 감소

클래스형 컴포넌트에서는 동일한 작업을 여러 생명주기 메서드에서 처리해야 할 경우가 있었다. 예를 들어, 컴포넌트가 마운트될 때와 업데이트될 때 동일한 API 호출이 필요하다면, componentDidMountcomponentDidUpdate에서 동일한 코드를 작성해야 했다. useEffect를 사용하면 이 모든 작업을 하나의 함수 안에 통합할 수 있어 코드 중복을 줄일 수 있다.


💡 클린업을 쉽게 처리

useEffect는 사이드 이펙트로 발생한 리소스들을 쉽게 정리(cleanup)할 수 있게 해준다. 예를 들어, 타이머를 설정하거나 외부 데이터 소스에 구독을 설정하는 경우, 컴포넌트가 언마운트될 때 해당 리소스를 해제해야 한다. useEffect는 이를 단순하게 처리할 수 있는 방법을 제공한다. 이는 컴포넌트의 메모리 누수나 불필요한 리소스 사용을 방지하는 데 도움이 된다.


📌 useEffect의 기본 사용법

useEffect는 다음과 같은 형식으로 사용된다.

import React, { useEffect } from 'react';

function ExampleComponent() {
  useEffect(() => {
    // 여기서 실행할 코드를 작성한다.
  });

  return <div>Example</div>;
}

이 코드는 컴포넌트가 처음 렌더링된 후에 실행된다. 기본적으로 useEffect는 매 렌더링 이후에 실행되지만, 의존성 배열을 통해 실행 시점을 제어할 수 있다.


💡 useEffect에서의 의존성 배열

useEffect는 React 컴포넌트가 렌더링된 후에 특정 작업을 수행하기 위해 사용하는 훅이다. 이때 의존성 배열을 통해 특정 상태나 props의 변경에 따라 useEffect를 재실행할지를 결정할 수 있다.

useEffect(() => {
  // 어떤 작업 수행
}, [dependency1, dependency2]);

여기서 dependency1dependency2는 의존성 배열이다. 이 배열에 포함된 값들 중 하나라도 변경되면, useEffect 내의 콜백 함수가 다시 실행된다.

  • 빈 배열 ([]): 컴포넌트가 처음 마운트될 때만 useEffect가 실행되고, 이후에는 재실행되지 않는다. 이를 통해 초기화 작업 등을 수행할 수 있다.
  • 배열 생략 (undefined): 배열을 생략하면, 컴포넌트가 렌더링될 때마다 useEffect가 실행된다.
  • 특정 값 포함 ([stateA, propB]): 배열에 포함된 값들이 변경될 때만 useEffect가 실행된다. 이로 인해 필요한 경우에만 부수 효과(Side Effect)가 발생하도록 제어할 수 있다.

💡 useEffect의 정리(clean-up) 함수

useEffect 안에서 반환되는 함수는 컴포넌트가 언마운트될 때 혹은 다음 렌더링 전에 실행된다. 이를 통해 타이머나 구독(subscription) 같은 리소스를 정리할 수 있다.

useEffect(() => {
  const timer = setTimeout(() => {
    console.log('타이머 실행');
  }, 1000);

  return () => {
    clearTimeout(timer); // 컴포넌트가 언마운트되기 전에 타이머를 정리
  };
}, []);

위 예제에서 clearTimeout(timer)은 컴포넌트가 언마운트될 때 호출되어 타이머를 정리한다. 이처럼 useEffect는 리소스를 효율적으로 관리하는 데 중요한 역할을 한다.


📌 useEffect의 동작 원리

💡 렌더링 후 실행

useEffect는 React의 렌더링 단계가 모두 끝난 후에 실행된다. React는 컴포넌트를 렌더링할 때, 먼저 모든 컴포넌트의 렌더링을 완료한 후, 각 컴포넌트에 설정된 useEffect를 실행한다. 이 때문에 useEffect는 DOM에 접근하거나, 서버에서 데이터를 가져오는 작업에 적합하다. 이는 브라우저가 실제로 화면을 업데이트한 후에 이펙트가 실행되기 때문에, 렌더링 과정에 영향을 주지 않기 때문이다.


💡 클린업 함수

때로는 useEffect가 수행한 작업을 정리(cleanup)해야 할 필요가 있다. 예를 들어, 구독을 설정한 경우 컴포넌트가 언마운트될 때 구독을 해제해야 한다. 이를 위해 useEffect는 클린업 함수를 반환할 수 있다. 이 함수는 컴포넌트가 언마운트되거나 이펙트가 다시 실행되기 전에 호출된다.

useEffect(() => {
  const subscription = someAPI.subscribe();

  return () => {
    // cleanup 함수
    subscription.unsubscribe();
  };
}, []);

이 코드에서 구독은 컴포넌트가 마운트될 때 설정되고, 컴포넌트가 언마운트될 때 해제된다.


📌 useEffect의 실행 시점

💡 컴포넌트가 마운트될 때

useEffect는 컴포넌트가 처음 화면에 렌더링될 때 실행된다. 이때 초기 데이터 로드, 구독 설정 등의 작업을 수행할 수 있다.

💡 컴포넌트가 업데이트될 때

useEffect의존성 배열에 포함된 값이 변경될 때마다 실행된다. 이를 통해 특정 값이 변경될 때만 특정 작업을 수행하도록 제어할 수 있다. 예를 들어, 사용자가 입력한 값이 변경될 때마다 서버로 API 요청을 보내고 싶다면, 해당 값을 의존성 배열에 추가하면 된다.

💡 컴포넌트가 언마운트될 때

useEffect는 컴포넌트가 언마운트되기 직전에 정리(cleanup) 함수를 호출한다. 이때 구독 해제, 타이머 제거 등의 작업을 수행할 수 있다. 이를 통해 메모리 누수나 불필요한 작업을 방지할 수 있다.


📌 렌더링 이후에 useEffect가 실행되는 이유

리액트는 컴포넌트가 렌더링된 이후에 useEffect를 실행하는데, 이에는 중요한 이유가 있다. 만약 렌더링 전에 useEffect가 실행된다면, 가상 돔이 Side Effect와 함께 처리되기 때문에, 성능 저하가 발생할 수 있다.

예를 들어, 렌더링 과정에서 state가 변경되면 추가적인 렌더링이 필요하게 된다. 따라서, useEffect를 렌더링 이전에 실행하면 한 번에 처리할 수 있는 작업을 두 번의 렌더링 과정으로 나누게 되어 비효율적이다.

특히, Side Effect가 포함된 렌더링은 가상 돔을 자주 다시 렌더링하게 만들 수 있다. 게다가, 일부 Side Effect는 서버의 응답을 받아야만 다음 작업을 진행할 수 있는 경우도 있는데, 이런 경우 렌더링 과정이 지연될 수 있다.

이 때문에 리액트는 가상 돔 렌더링에서 Side Effect를 제외하고, 순수한 컴포넌트만을 사용해 렌더링을 진행한다. 이렇게 해야만 리액트의 핵심인 재조정 알고리즘이 효율적으로 작동할 수 있다.

또한, 렌더링 이후에 비동기 Side Effect를 처리하는 것은 사용자 경험에도 긍정적인 영향을 준다. 만약 서버와의 통신에 문제가 발생해도, 이로 인한 영향을 최소화할 수 있기 때문이다. 그래서 시간이 많이 걸리거나 비동기 작업을 처리할 때 useEffect가 특히 유용한 것이다.


📌 useEffect와 비동기 작업

useEffect 내에서 비동기 작업을 수행할 때는 주의가 필요하다. 비동기 작업을 직접 useEffect 함수 내에서 수행하지 말고, 비동기 함수를 따로 정의한 후 호출하는 것이 좋다. 이는 비동기 작업이 완료되기 전에 컴포넌트가 언마운트되거나, 다른 비동기 작업이 시작될 수 있기 때문이다.

useEffect(() => {
  async function fetchData() {
    const response = await fetch('/api/data');
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);

이 예제에서 fetchData 함수는 컴포넌트가 마운트될 때 한 번 호출되며, 데이터를 가져온 후 상태를 업데이트한다.


📌 useEffect의 잘못된 사용 예

💡 무한 루프 문제

만약 useEffect 내에서 상태를 변경하고, 그 상태가 의존성 배열에 포함되어 있으면, 이펙트가 무한히 반복해서 실행될 수 있다.

useEffect(() => {
  setCount(count + 1); // 잘못된 예시
}, [count]);

이 코드는 count가 변경될 때마다 useEffect가 실행되고, 다시 count를 변경하므로 무한 루프에 빠지게 된다. 이를 방지하려면 상태 변경과 관련된 작업은 조건부로 실행하거나, 적절한 의존성을 설정해야 한다.


💡 의존성 배열 생략으로 인한 버그

의존성 배열을 생략하면, useEffect는 모든 렌더링 후에 실행된다. 이는 의도하지 않은 사이드 이펙트를 초래할 수 있다.

useEffect(() => {
  fetchData(); // fetchData가 의존성 배열에 포함되어야 한다
});

이 코드는 매번 렌더링될 때마다 데이터를 가져오므로 성능 문제를 일으킬 수 있다. 따라서 의존성 배열을 정확히 설정하여 이펙트의 실행 빈도를 제어해야 한다.


📌 useEffect의 활용과 유용한 패턴

💡 데이터 가져오기

가장 일반적인 useEffect 사용 사례 중 하나는 컴포넌트가 마운트될 때 데이터를 가져오는 것이다.

useEffect(() => {
  async function fetchData() {
    const response = await fetch('/api/data');
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);

이 코드에서 useEffect는 컴포넌트가 처음 렌더링될 때 한 번 실행되며, 데이터를 가져오고 상태를 업데이트한다.


💡 구독 설정과 해제

구독이나 타이머 설정 등, 클린업이 필요한 작업도 useEffect로 관리할 수 있다.

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return () => clearInterval(id); // 컴포넌트 언마운트 시 타이머 해제
}, []);

이 코드는 1초마다 count를 증가시키고, 컴포넌트가 언마운트되면 타이머를 해제한다.


📌 useEffect를 사용할 때 고려해야 할 사항

  • 의존성 배열 관리: 의존성 배열에 포함된 값들은 이펙트가 재실행되는 트리거가 된다. 따라서 의존성을 정확히 설정해야 불필요한 재실행을 막을 수 있다.
  • 클린업 중요성: 구독이나 타이머 등 클린업이 필요한 작업은 반드시 정리 함수를 통해 적절히 해제해야 한다. 그렇지 않으면 메모리 누수나 의도하지 않은 부작용이 발생할 수 있다.
  • useEffect와 상태 업데이트: 상태를 업데이트하는 로직은 신중하게 설계해야 한다. 무한 루프에 빠지지 않도록 주의하고, 필요할 때만 상태를 업데이트해야 한다.

참고문헌

0개의 댓글