useEffect 올바르게 사용하는 방법과 흔한 실수
React의 useEffect 동작 원리, 의존성 배열 관리, 비동기 처리와 정리(cleanup)로 인한 메모리 누수 방지 등 핵심 원칙을 정리한 글
목차
개요
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를 정확히 이해하면 의도치 않은 재실행과 메모리 누수를 줄일 수 있다. 핵심은 의존성 배열을 명확히 관리하고 비동기 작업을 정리하는 습관이다. 위 체크리스트를 작업 흐름에 적용하면 안정적인 컴포넌트를 만들 수 있다.