React · 2025-12-09

useEffect 올바르게 사용하는 방법과 흔한 실수

React의 useEffect 동작 원리, 의존성 배열 관리, 비동기 처리와 정리(cleanup)로 인한 메모리 누수 방지 등 핵심 원칙을 정리한 글

작성일 : 2025-12-09 ㆍ 작성자 : 관리자
post
목차

개요

useEffect는 컴포넌트의 부수효과(side effect)를 처리하는 핵심 훅이다. 그러나 의존성 배열 관리나 비동기 작업 정리 부분에서 실수가 잦아 예기치 않은 재렌더링, 메모리 누수, 상태 갱신 오류가 발생한다. 이 글은 처음 접하는 사람도 이해하기 쉽도록 기본 원리부터 흔한 실수, 올바른 패턴까지 예제를 통해 설명한다.

useEffect 기본 원리

시그니처와 동작 시점

useEffect는 렌더링 후 실행된다. 기본 형태는 렌더링마다 실행되며, 두 번째 인자로 전달한 의존성 배열(deps)에 따라 실행 빈도를 제어한다.

의존성 배열의 의미

의존성 배열에 값을 넣으면 그 값이 변경될 때만 effect가 재실행된다. 비워두면 마운트 시 한 번만 실행되고, 배열을 생략하면 매 렌더링마다 실행된다. 정확한 값들을 넣는 것이 중요하다.

자주 하는 실수와 원인

  • 의존성 배열을 비우거나 누락해서 stale state 발생
  • 비동기 작업 취소를 하지 않아 메모리 누수 또는 경고 발생
  • 불필요한 의존성으로 무한 루프 유발
  • 함수나 객체를 직접 deps에 넣어 매 렌더링마다 재생성되는 문제

예제와 해결책

잘못된 예: 의존성 누락

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

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log('count changed:', count)
  }, [])
  return <button onClick={() => setCount(c => c + 1)>{count}</button>
}

위 코드에서는 count를 의존성 배열에 넣지 않아 콘솔에 최신 값이 찍히지 않는다. 의도한 동작이라도 의존성은 정확히 적어야 한다.

올바른 예

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

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log('count changed:', count)
  }, [count])
  return <button onClick={() => setCount(c => c + 1)>{count}</button>
}

비동기 작업과 cleanup

비동기 작업(fetch, setTimeout, 구독 등)은 컴포넌트가 언마운트되기 전에 정리해야 한다. 정리를 하지 않으면 메모리 누수나 "Can't perform a React state update on an unmounted component" 경고가 발생한다.

취소 가능한 패턴

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

function DataLoader({ url }) {
  const [data, setData] = useState(null)
  useEffect(() => {
    let cancelled = false
    fetch(url)
      .then(res => res.json())
      .then(json => {
        if (!cancelled) setData(json)
      })
    return () => { cancelled = true }
  }, [url])
  return <div>{data ? 'Loaded' : 'Loading...'}</div>
}

AbortController를 사용하면 네트워크 요청 자체를 취소할 수 있어 더 안전하다.

함수와 객체를 deps에 넣을 때

함수나 객체는 매 렌더링마다 재생성된다. 이를 그대로 deps에 넣으면 effect가 불필요하게 재실행된다. useCallback이나 useMemo를 사용해 참조를 안정화하거나, 필요한 값만 deps에 넣어 관리한다.

예시: useCallback 사용

import React, { useCallback, useEffect } from 'react'

function Parent({ value }) {
  const handle = useCallback(() => { console.log(value) }, [value])
  useEffect(() => {
    // handle은 value가 바뀔 때만 변경됨
  }, [handle])
}

lint 규칙과 신뢰성

eslint-plugin-react-hooks의 권장 규칙을 따르면 의존성 누락을 줄일 수 있다. 하지만 자동으로 모든 문제를 해결해주지는 않으므로 의도적인 제외는 주석으로 이유를 남긴다.

체크리스트

  • 의존성 배열에 사용된 모든 값이 포함되어 있는지 확인
  • 비동기 작업은 취소 가능한 패턴으로 처리
  • 함수/객체는 useCallback/useMemo로 안정화 고려
  • 빈 배열은 마운트 전용 효과인지 확실히 판단
  • eslint 훅 규칙을 활성화하고 예외는 주석으로 설명

마무리

useEffect를 정확히 이해하면 의도치 않은 재실행과 메모리 누수를 줄일 수 있다. 핵심은 의존성 배열을 명확히 관리하고 비동기 작업을 정리하는 습관이다. 위 체크리스트를 작업 흐름에 적용하면 안정적인 컴포넌트를 만들 수 있다.

react useEffect 사용법 useEffect 의존성 배열 오류 리액트 useEffect 메모리 누수 React 훅 useEffect cleanup 비동기 효과 취소 useCallback useMemo eslint-plugin-react-hooks