프로그래밍/ReactJS

React 개발의 핵심: useEffect 훅 완전 정복하기

shimdh 2025. 10. 14. 19:44
728x90

React 함수형 컴포넌트에서 부수 효과(side effects)를 관리하는 것은 웹 애플리케이션 개발의 핵심적인 부분입니다. useEffect 훅은 React에서 가장 강력하고 자주 사용되는 도구 중 하나로, 데이터 가져오기, 구독 설정, DOM 직접 조작 등 다양한 부수 효과를 함수형 컴포넌트 안에서 안전하고 효율적으로 처리할 수 있도록 돕습니다. 이 글에서는 useEffect 훅의 기본 개념부터 실용적인 활용법, 그리고 흔히 겪는 문제점과 해결책까지 심도 있게 다루어 보겠습니다. useEffect를 제대로 이해하고 활용하는 것은 React 컴포넌트의 라이프사이클을 효과적으로 관리하고, 애플리케이션의 성능과 안정성을 최적화하는 데 필수적입니다.

728x90

useEffect 훅이란 무엇인가요?

useEffect 훅은 함수형 컴포넌트에서 React의 라이프사이클 기능을 사용할 수 있게 해주는 도구입니다. 기존 클래스형 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 라이프사이클 메서드를 하나의 훅으로 통합하여 더 간결하고 직관적인 방식으로 부수 효과를 관리할 수 있게 해줍니다.

부수 효과(Side Effects)의 이해

그렇다면 "부수 효과"란 정확히 무엇을 의미할까요? React 컴포넌트 내부에서 외부 세계와 상호 작용하는 모든 작업을 부수 효과라고 할 수 있습니다. 이는 순수한 상태나 props만으로 렌더링이 결정되는 React의 철학과 대비되는 부분으로, 컴포넌트의 렌더링 외부에서 발생하는 변화를 의미합니다. 대표적인 예시는 다음과 같습니다:

  • 데이터 가져오기 (Data Fetching): 외부 API로부터 데이터를 요청하거나 전송하는 작업. 이는 가장 흔한 부수 효과 중 하나입니다.
  • 구독 (Subscriptions): WebSocket을 통해 실시간 메시지를 받거나, 특정 이벤트에 대한 리스너를 설정하는 작업.
  • 타이머 (Timers): setInterval 또는 setTimeout과 같이 시간 기반의 작업을 설정하는 작업.
  • DOM 조작 (DOM Manipulation): React의 일반적인 렌더링 프로세스 외부에서 직접 DOM 요소를 변경하는 작업 (예: 포커스 설정, 스크롤 위치 조작).

이러한 부수 효과를 적절히 관리하지 않으면 메모리 누수나 불필요한 리렌더링이 발생할 수 있으므로, useEffect가 필수적입니다.

useEffect의 기본 구문과 동작 방식

useEffect를 사용하는 기본 구문은 매우 간단합니다.

import React, { useEffect } from 'react';

const MyComponent = () => {
  useEffect(() => {
    // 여기에 부수 효과 로직을 정의합니다

    return () => {
      // 선택적으로 정리(cleanup) 코드를 여기에 정의합니다
    };
  }, [/* 의존성 배열 */]);

  return <div>My Component</div>;
};

useEffect 훅은 두 개의 인수를 받습니다:

  1. 첫 번째 인수는 함수: 이 함수 안에 실제로 수행할 부수 효과 로직을 정의합니다. 이 함수는 컴포넌트가 렌더링된 후에 실행되며, 선택적으로 cleanup 함수를 반환할 수 있습니다.
  2. 두 번째 인수는 의존성 배열 (Dependency Array): 이 배열은 useEffect가 언제 실행되어야 하는지를 결정합니다. 이 배열이 useEffect 훅의 가장 중요한 부분 중 하나입니다.

의존성 배열의 역할

의존성 배열은 useEffect의 동작을 정교하게 제어합니다.

  • 빈 배열 ([]): useEffect는 컴포넌트가 처음 렌더링된 후에 단 한 번만 실행됩니다. 이는 클래스형 컴포넌트의 componentDidMount와 유사한 동작을 합니다. 주로 초기 데이터 로딩이나 한 번만 필요한 전역 이벤트 리스너 설정 등에 사용됩니다.
  • 변수 포함 ([변수1, 변수2, ...]): 배열 안에 포함된 변수들 중 하나라도 값이 변경될 때마다 useEffect가 다시 실행됩니다. 이는 componentDidUpdate와 유사하며, 특정 상태나 props의 변화에 따라 부수 효과를 재실행해야 할 때 유용합니다.
  • 의존성 배열 생략: 의존성 배열을 생략하면 useEffect는 컴포넌트가 렌더링될 때마다 (props나 상태가 변경될 때마다) 실행됩니다. 이는 무한 루프나 불필요한 연산을 유발할 수 있으므로, 특별한 경우가 아니라면 의존성 배열을 명시적으로 사용하는 것이 좋습니다.

useEffect의 실용적인 예시

1. 컴포넌트 마운트 시 데이터 가져오기

가장 흔한 useEffect 사용 사례 중 하나는 컴포넌트가 마운트될 때 외부 API로부터 데이터를 가져오는 것입니다.

import React, { useState, useEffect } from 'react';

const UserList = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const result = await response.json();
        setUsers(result);
      } catch (error) {
        console.error("데이터 가져오기 오류:", error);
      }
    };

    fetchData();
  }, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행

  return (
    <div>
      <h2>사용자 목록</h2>
      {users.length > 0 ? (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      ) : (
        <p>사용자 데이터를 불러오는 중입니다...</p>
      )}
    </div>
  );
};

export default UserList;

이 예시에서는 useEffect의 두 번째 인수로 빈 배열 []을 사용하여, fetchData 함수가 컴포넌트가 처음 렌더링된 후에만 한 번 실행되도록 합니다. 데이터를 성공적으로 가져오면 setUsers를 통해 상태를 업데이트하고, 이는 컴포넌트의 리렌더링을 유발하여 사용자 목록을 화면에 표시합니다.

2. 특정 상태 변경에 반응하기

useEffect는 특정 상태(state)나 속성(props)의 변화에 따라 동작을 수행해야 할 때도 유용합니다.

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    // count 값이 변경될 때마다 이 로그가 출력됩니다.
    console.log(`당신은 ${count}번 클릭했습니다.`);

    // 정리(cleanup) 함수: 다음 useEffect 실행 전 또는 컴포넌트 언마운트 시 호출
    return () => {
      console.log(`count ${count}에 대한 정리 작업`);
    };
  }, [count]); // count가 변경될 때마다 실행

  return (
    <div>
      <p>클릭 횟수: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        클릭하세요
      </button>
    </div>
  );
};

export default Counter;

여기서 useEffect는 의존성 배열에 [count]를 포함하고 있습니다. 따라서 count 상태 변수가 변경될 때마다 useEffect 내부의 로직이 다시 실행됩니다. cleanup 함수는 이전 count 값에 대한 로그를 출력하며, 이는 상태 변화 시 이전 효과를 정리하는 데 유용합니다.

3. 부수 효과 정리 (Cleanup)

useEffect의 강력한 기능 중 하나는 부수 효과를 "정리(cleanup)"할 수 있다는 것입니다. 구독 해제, 타이머 해제 등 컴포넌트가 언마운트되거나 다음 useEffect가 실행되기 전에 반드시 수행해야 하는 작업은 useEffect 콜백 함수에서 함수를 반환함으로써 정의할 수 있습니다.

import React, { useEffect, useState } from 'react';

const Timer = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    console.log("타이머 시작!");
    const timerId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    // 정리(cleanup) 함수 반환
    return () => {
      console.log("타이머 정리!");
      clearInterval(timerId); // 컴포넌트 언마운트 시 타이머 해제
    };
  }, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행

  return (
    <div>
      <p>경과 시간: {seconds}초</p>
    </div>
  );
};

export default Timer;

이 예시에서 setInterval로 설정된 타이머는 useEffect 내부에서 시작됩니다. 그리고 반환되는 clearInterval(timerId) 함수는 컴포넌트가 언마운트될 때 (또는 의존성 배열이 변경되어 useEffect가 다시 실행되기 전에) 호출되어 불필요한 타이머 실행을 중지합니다. 이는 메모리 누수나 무한 루프를 방지하는 데 핵심적입니다. 만약 cleanup이 없으면 컴포넌트가 자주 언마운트/마운트될 때 타이머가 쌓여 성능 저하를 초래할 수 있습니다.

흔히 겪는 문제점과 해결책

useEffect를 사용할 때 개발자들이 자주 마주하는 문제들을 살펴보고, 이를 해결하는 방법을 알아보겠습니다. 이러한 문제는 대부분 의존성 배열의 오용이나 클로저 문제에서 비롯됩니다.

1. 무한 루프 (Infinite Loop)

  • 문제: 상태 업데이트가 useEffect 내부에서 발생하고, 의존성 배열에 그 상태가 포함되어 있으면 리렌더링 → useEffect 재실행 → 상태 업데이트의 무한 반복이 발생합니다.
  • 해결책: 의존성 배열을 정확히 관리하세요. ESLint의 react-hooks/exhaustive-deps 규칙을 활성화하여 누락된 의존성을 경고받을 수 있습니다. 필요 시 useCallback 훅으로 함수를 메모이제이션하세요.

2. 의존성 배열 누락

  • 문제: 배열을 생략하거나 잘못된 값만 포함하면 부수 효과가 예상치 않게 실행되거나 스킵됩니다.
  • 해결책: 모든 외부 변수(상태, props, 함수)를 배열에 포함하세요. 객체/배열은 참조 변경이 잦으므로 useMemouseCallback으로 안정화하세요.

3. 클로저 문제 (Stale Closure)

  • 문제: useEffect가 오래된 상태나 props 값을 캡처하여 최신 값이 반영되지 않습니다.
  • 해결책: 의존성 배열에 관련 변수를 포함하고, 함수 내부에서 setState의 함수형 업데이트(예: prev => prev + 1)를 사용하세요.

이러한 팁을 따르면 useEffect의 안정성을 크게 높일 수 있습니다.

마무리

useEffect 훅은 React의 함수형 컴포넌트를 더욱 강력하게 만들어주는 도구입니다. 기본 구문을 익히고 의존성 배열을 올바르게 활용하며, cleanup을 잊지 않도록 하세요. 실전에서 다양한 예시를 통해 연습하다 보면 자연스럽게 마스터할 수 있을 것입니다. React 개발을 시작하는 분들에게 이 글이 도움이 되기를 바랍니다! 

728x90