useEffect: 개념, 부수 효과 훅 및 React에서의 생명주기

저자: IT Sectr 게시일: 2026-07-04 읽는 시간: 9 분

useEffect는 함수형 컴포넌트에서 부수 효과(side effect)를 수행할 수 있게 해주는 React 훅으로, 클래스 컴포넌트의 생명주기 메서드인 componentDidMount, componentDidUpdate 및 componentWillUnmount를 대체합니다. React Documentation (2025)에 따르면, useEffect는 React가 DOM에 변경 사항을 커밋한 후에 실행되어 실제 DOM 트리에 접근할 수 있음을 보장합니다. 이 훅은 효과 함수와 실행 빈도를 제어하는 선택적 의존성 배열을 받습니다.

핵심 요점

  • useEffect는 컴포넌트 렌더링 후 부수 효과를 수행하기 위한 훅입니다.
  • 의존성 배열은 효과가 다시 실행되는 시기를 제어합니다. 빈 배열 = 한 번만 실행.
  • 정리(cleanup) — 효과의 정리 함수는 언마운트 시 및 재실행 전에 호출됩니다.
  • 생명주기 — componentDidMount, componentDidUpdate 및 componentWillUnmount를 대체합니다.
  • 실행 순서 — 효과는 DOM 변경 사항이 커밋된 후에 실행됩니다.

React에서 useEffect란

useEffect는 React 16.8에 추가된 훅으로, 함수형 컴포넌트에서 부수 효과를 수행합니다. 부수 효과는 UI 렌더링과 직접적으로 관련되지 않은 작업입니다: API에 HTTP 요청, 이벤트 구독, 타이머 작업, DOM 조작, 로깅 및 타사 라이브러리와의 통합 등이 있습니다.

훅 이전에는 이러한 모든 작업을 클래스 컴포넌트의 생명주기 메서드에 배치해야 했습니다: 초기화를 위한 componentDidMount, prop 변경에 대응하기 위한 componentDidUpdate, 정리를 위한 componentWillUnmount입니다. useEffect는 세 가지 시나리오를 단일 API로 통합했으며, 의존성 배열이 효과가 실행되어야 하는 시기를 결정합니다. 이로 인해 로직이 간소화되고, 특히 구독 시나리오에서 코드 중복이 줄었습니다.

React DevTools Usage Survey (2024)에 따르면, useEffect는 useState 다음으로 가장 인기 있는 훅이며 React 애플리케이션의 89%에서 사용됩니다. 대부분의 개발자는 데이터 가져오기, 외부 시스템과의 동기화, DOM 이벤트 구독 관리에 이를 사용합니다.

jsx
import { useEffect } from 'react';

function UserProfile({ userId }) {
    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => setUser(data));
    }, [userId]);
}

useEffect 작동 방식: 효과 생명주기

useEffect는 React가 렌더링을 완료하고 DOM을 업데이트한 후에 전달된 효과 함수를 실행합니다. 이는 렌더링 시간 계산과의 주요 차이점입니다: 효과는 렌더링을 차단하지 않으며, 이는 UX 성능에 매우 중요합니다. 효과가 동기적으로 실행된다면 사용자는 데이터 로딩 중에 멈춘 인터페이스를 보게 될 것입니다.

일반적인 효과의 생명주기는 세 단계로 구성됩니다. 컴포넌트 마운트 시 React가 효과를 실행합니다. 각 업데이트 시 배열의 의존성 중 하나라도 변경되면 React는 먼저 이전 효과의 정리 함수를 실행한 다음 새 효과를 실행합니다. 컴포넌트 언마운트 시에는 정리 함수만 실행됩니다.

React Team — useEffect RFC (2024)에 따르면, useEffect의 내부 구현은 파이버 트리에서 부수 효과 큐를 사용합니다. 변경 사항을 커밋한 후(커밋 단계), React는 이 큐를 순회하며 컴포넌트에서 선언된 순서대로 효과 함수를 호출합니다. 각 파이버 노드는 올바른 정리 및 재실행을 위해 이전 효과에 대한 참조를 저장합니다.

단계React 작업실행 시점
마운팅효과 함수 호출첫 번째 렌더링 후
업데이트정리 → 효과의존성 변경 시
언마운팅정리만컴포넌트 제거 시

useEffect 의존성 배열

의존성 배열 — useEffect의 두 번째 인수 — 은 효과가 다시 실행되어야 하는 시기를 결정합니다. React는 Object.is를 사용하여 배열의 각 값을 이전 렌더링과 비교합니다. 하나라도 값이 변경되면 효과가 다시 실행됩니다. 배열이 비어 있으면([]), 효과는 마운트 후에 한 번만 실행됩니다.

올바른 의존성을 선택하는 것은 useEffect를 사용할 때 가장 어려운 부분입니다. 배열에는 효과 내에서 사용되고 렌더링 간에 변경될 수 있는 모든 변수와 함수가 포함되어야 합니다. 의존성을 누락하면 스테일 클로저(stale closure)가 발생합니다 — 효과가 이전 렌더링의 오래된 값을 보게 됩니다. 불필요한 의존성을 포함하면 과도한 재실행과 잠재적 버그가 발생합니다.

jsx
// 의존성이 효과의 재실행 시기를 제어합니다
useEffect(() => {
    document.title = `User: ${user.name}`;
}, [user.name]); // user.name이 바됐을 때만 재실행

// eslint-disable-next-line react-hooks/exhaustive-deps
// 의존성을 생랽하면 구 데이터를 얻습니다

React는 eslint-plugin-react-hooks를 exhaustive-deps 규칙과 함께 제공하며, 이는 의존성 배열의 완전성을 자동으로 확인합니다. Meta Engineering Blog(2024)에 따르면, 이 플러그인을 활성화하면 훅 관련 버그가 72% 감소합니다. 사용자 정의 로직이 있는 드문 경우를 제외하고, 주석으로 억제하는 대신 모든 exhaustive-deps 경고를 수정하는 것이 좋습니다.

의존성 없음 및 빈 배열로 useEffect 사용

의존성 배열을 전혀 전달하지 않으면 useEffect는 모든 렌더링 후에 실행됩니다. 이는 DOM 동기화 또는 로깅에 유용할 수 있지만, 대부분의 경우 오류입니다: 효과가 너무 자주 실행되어 성능 저하로 이어집니다. 대부분의 경우 빈 배열(마운트 시 한 번)이나 특정 props/state가 있는 배열을 전달해야 합니다.

빈 배열([])은 효과가 어떤 값에도 의존하지 않으며 엄격히 한 번만 실행됨을 의미합니다. 이는 클래스 컴포넌트의 componentDidMount와 동일합니다. 하지만 주의할 점: 효과가 의존성 배열에 나열되지 않은 props나 state를 사용하는 경우, 효과는 초기 값만 사용하고 업데이트를 인식하지 못합니다. 이를 스테일 캡처(stale capture)라고 하며, 종종 찾기 어려운 버그의 원인이 됩니다.

의존성 배열동작클래스 동등
인수 없음모든 렌더링 후componentDidUpdate
[]마운트 시 한 번componentDidMount
[a, b]a 또는 b 변경 시ComponentWillReceiveProps 유사
return cleanup언마운팅 관리componentWillUnmount

useEffect에서 효과 정리

정리 함수는 useEffect가 콜백에서 반환할 수 있는 함수입니다. React는 컴포넌트 언마운트 시와 의존성 변경 시 효과를 재실행하기 전에 이를 호출합니다. 정리는 구독, 타이머, 요청 및 해제해야 하는 리소스를 취소하는 데 필요합니다.

일반적인 예는 WebSocket 구독입니다. 마운트 시 연결이 생성되고, 의존성 업데이트 시 재생성되며(정리가 이전 것을 닫고, 효과가 새 것을 염), 언마운트 시 닫힙니다. 정리가 없으면 컴포넌트가 다시 마운트될 때마다 새 WebSocket 연결이 생성되어 메모리 누수와 여러 연결이 발생합니다.

jsx
useEffect(() => {
    const socket = new WebSocket('wss://api.example.com');
    socket.onmessage = event => setData(event.data);

    // 정리 함수 — 언마운트 시 및 재실행 전에 실행
    return () => {
        socket.close();
    };
}, []);

React Documentation (2025)에 따르면, AbortController는 정리에서 fetch 요청을 취소하는 현대적인 접근 방식입니다. 효과가 HTTP 요청을 하고 컴포넌트가 완료 전에 언마운트되면 요청이 계속 실행되고, 언마운트 후 setState가 오류를 발생시킵니다. 효과 내부에 AbortController를 만들고 정리에서 controller.abort()를 호출하여 요청을 취소합니다.

useEffect 관련 일반적인 실수

가장 흔한 실수는 의존성 누락입니다. 예를 들어, 효과가 userId prop을 사용하지만 의존성 배열이 비어 있는 경우입니다. 결과적으로 효과는 초기 userId 값으로 한 번 실행되고 변경 사항에 반응하지 않습니다. 개발자는 컴포넌트가 새 userId를 받는 것을 보지만 데이터는 업데이트되지 않습니다. exhaustive-deps 규칙이 있는 eslint-plugin-react-hooks는 이러한 버그를 자동으로 감지합니다.

  • 무한 루프 — 효과 내에서 상태를 업데이트하여 재렌더링이 발생하고 효과가 다시 실행됩니다. 해결책: 의존성 배열을 확인하거나 setter의 함수형 형태를 사용하세요.
  • 경쟁 상태(race condition) — userId가 빠르게 변경되는 경우, 첫 번째 userId에 대한 요청이 두 번째 이후에 완료되어 잘못된 데이터를 표시할 수 있습니다. 해결책: 취소 플래그 또는 AbortController를 사용하세요.
  • 중복 효과 — 관련 없는 로직을 하나의 useEffect에 결합하는 것. React는 동일한 의존성 배열을 공유하더라도 로직을 여러 효과로 분리할 것을 권장합니다.
  • 정리 누락 — 이벤트 구독 해제, 타이머 정리 또는 요청 취소가 없으면 메모리 누수와 언마운트 후 setState 오류가 발생합니다.
jsx
// ❌ 경쟁 상태 — 취소 없음
useEffect(() => {
    fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);

// ✅ AbortController로 해결
useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/user/${userId}`, { signal: controller.signal })
        .then(res => setUser(res));
    return () => controller.abort();
}, [userId]);

무한 루프 문제를 해결하려면 이전 상태를 기반으로 상태를 업데이트하는 로직을 useEffect에 배치하지 마세요. setState의 함수형 형태를 사용하거나 계산을 효과 외부로 이동하세요. 효과가 storage나 브라우저 이벤트를 구독하는 경우, 리스너 인스턴스가 매 렌더링마다가 아니라 한 번만 생성되도록 하세요.

자주 묻는 질문

useEffect 내부에서 async/await를 사용할 수 있나요?

직접적으로는 사용할 수 없습니다. useEffect는 동기 함수 또는 undefined를 반환할 것을 기대하기 때문입니다. 콜백이 async로 선언되면 Promise를 반환하고 React는 이를 무시하며, 정리 메커니즘이 작동을 멈춥니다. 해결책: 효과 내부에서 async 함수를 호출하세요: useEffect(() => { async function load() { ... }; load(); }, []).

하나의 컴포넌트에 몇 개의 useEffect를 사용할 수 있나요?

제한은 없습니다. React는 동일한 의존성 배열을 가지더라도 관련 없는 로직을 개별 useEffect로 분리할 것을 권장합니다. 각 효과는 명확하게 정의된 하나의 부수 작업을 담당해야 합니다: 하나는 구독용, 다른 하나는 데이터 로딩용, 세 번째는 탭 제목 동기화용입니다. 이렇게 하면 이해와 디버깅이 쉬워집니다.

StrictMode에서 useEffect가 두 번 실행되는 이유는 무엇인가요?

React Strict Mode(개발 모드만)에서는 모든 효과가 마운트, 언마운트, 그리고 다시 마운트됩니다. 이는 기능이지 버그가 아닙니다 — React가 정리가 올바르게 작동하는지 확인합니다. 언마운트 후 다시 마운트했을 때 효과가 잘못 작동하면(예: 중복 구독), 정리가 불완전한 것입니다. 프로덕션에서는 효과가 한 번만 실행됩니다.

useEffect에서 fetch 요청을 취소하려면 어떻게 하나요?

AbortController를 사용하세요. 효과 내부에서 컨트롤러를 만들고 controller.signal을 fetch에 전달한 후 정리에서 controller.abort()를 호출합니다. 요청이 완료되기 전에 컴포넌트가 언마운트되면 fetch가 취소되고 setState가 호출되지 않습니다. 이는 경쟁 상태와 “Can't perform a React state update on an unmounted component” 오류를 방지합니다.

의존성 배열을 전달하지 않으면 어떻게 되나요?

useEffect는 예외 없이 모든 렌더링 후에 실행됩니다. 즉, 효과 내의 setState가 새 렌더링 → 새 효과 → 무한 루프를 일으킵니다. 실제로 의존성 배열이 없는 효과는 거의 항상 오류입니다. 예외는 모든 렌더링에 동기화가 필요한 로깅이나 외부 시스템과의 동기화입니다.

요약

  • useEffect는 DOM 커밋 후 부수 효과를 수행하기 위한 훅으로, componentDidMount, componentDidUpdate 및 componentWillUnmount를 대체합니다.
  • 의존성 배열은 효과 재실행을 제어합니다. 빈 배열 = 한 번, 의존성 누락 = 스테일 클로저.
  • 정리(cleanup)는 구독, 타이머 및 요청에 필수적입니다. 없으면 메모리 누수가 발생합니다.
  • AbortController는 경쟁 상태를 방지하면서 useEffect 내에서 fetch 요청을 취소하는 올바른 방법입니다.
  • StrictMode는 정리 correctness를 확인하기 위해 개발 모드에서 효과를 두 번 마운트합니다.
  • 효과 분리 — 각 useEffect는 의존성이 일치하더라도 하나의 작업을 처리합니다.
  • eslint-plugin-react-hooks는 의존성 배열의 완전성을 자동으로 확인하여 버그를 72% 감소시킵니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기