‘useEffect는 언제 호출될까?’로 시작된 useEffect 정리

정주·2025년 10월 31일

리액트

목록 보기
1/2
post-thumbnail

들어가며

혹시 useEffect가 언제 호출되는지 모르고 얼레벌레 사용하는 사람 있나요..?

이상 자기소개였습니다.
이제는 더 이상 물러날 곳이 없다!
이번에는 useEffect를 낱낱이 파헤쳐 보겠습니다.

useEffect 란?

useEffect외부 시스템과 컴포넌트를 동기화하는 React 훅입니다.

여기서 외부 시스템이란, React가 직접 제어하지 않는 시스템을 의미합니다.

그렇다면 리액트가 제어하지 않는 시스템에는 어떤 것들이 있을까요?

컴포넌트가 화면에 표시되는 동안 네트워크, 브라우저 API, 서드파티 라이브러리 등과 연결해야 할 때가 있습니다.

이러한 것들은 모두 리액트 바깥에서 동작하므로 외부 시스템으로 볼 수 있습니다.

또한 브라우저의 전역 상태(localStorage, sessionStorage 등) 역시 React의 상태 관리 흐름과는 별개로 동작하기 때문에, 외부 시스템입니다.

언제 useEffect를 사용할까?

useEffect컴포넌트와 외부 시스템 사이의 동기화가 필요할 때 사용합니다.
아래 표를 보면 어떤 상황에서 useEffect를 활용해야 하는지 한눈에 확인할 수 있습니다.

구분설명예시
네트워크 연결서버와 지속적 연결WebSocket, SSE
브라우저 APIDOM 이벤트, 타이머addEventListener, setInterval
외부 데이터fetch, axiosAPI 호출
서드파티 객체외부 라이브러리Chart.js, Mapbox
내부 상태 로직리액트 상태 기반 UIsetState

useEffect 구조

import { useEffect } from 'react';

function test (){

  useEffect(() =>{
  // Setup 
  	return // Cleanup
  	},[ //Dependencies 
  ])

	return(
	...
	)
}
  • 컴포넌트를 외부 시스템과 연결해야하기 때문에 렌더링할때 가장 최상위 레벨에서 useEffect를 호출해야 합니다.

    • 가장 최상위 레벨에서 호출한다...? 이건 useEffect뿐만 아니라 다른 훅에서도 동일하게 적용되는데요.
      -> 여기에서 가장 최상위 레벨이란 조건문, 반복문, 함수 안쪽이 아닌 곳입니다.
      간단하게 depth가 가장 얉은 곳에서 호출한다고 생각하면 됩니다.
      이렇게 최상위 레벨에서 호출하는 이유는 리액트가 훅 호출 순서를 기준으로 상태를 관리하기 때문입니다!
      만약 최상위 레벨이 아닌곳에 훅들이 존재하면 리액트는 렌더링 순서를 파악하지 못하고 Invalid hook call 에러를 발생시킵니다.

        // 올바른 사용법⭕️ 
        function Component({ active }) {
          useEffect(() => {
            if (active) console.log('Effect 실행!');
          }, [active]);
        }
      
        // 잘못된 사용법❌ 
        function Component({ active }) {
          if (active) { 
            useEffect(() => {  // ❌ 조건문 안에서 훅 호출 금지
              console.log('Effect 실행!');
            }, []);
          }
        }
  • useEffect는 쉽게 setup, cleanup, dependencies로 이뤄져있는데요.

    • setup: effect 내부 로직, 외부 시스템과 연결
    • cleanup: setup에서 만든 리소스를 정리
    • dependencies: 배열안의 값들이 바뀌면 setupcleanup 순으로 재실행
  • useEffect 자체는 반환값이 없지만, setup 함수 내에서 선택적으로 cleanup 함수를 반환할 수 있습니다.

실행(setup) 함수

useEffect(() => {
  // setup 예시 
  const id = setInterval(() => console.log('tick'), 1000);
}, []);
  • setupeffect의 로직이 포함된 함수로, 외부 시스템과 컴포넌트를 연결합니다.

  • 실행 시점: 컴포넌트가 DOM에 추가되고 commit 단계가 끝난 후, 비동기적으로 실행됩니다.

    • 리액트 컴포넌트의 렌더링 과정

      1. Render Phase
      JSX를 계산해서 가상 DOM(Virtual DOM)을 만듭니다.
      → 아직 실제 DOM에는 반영되지 않았습니다.
      → 여기서는 순수 계산만 하고, 브라우저 화면에는 아무 것도 나타나지 않아요!
      2. Commit Phase
      → 가상 DOM에서 실제 DOM으로 변경 사항을 반영합니다.
      → 브라우저가 실제 화면을 그리기(paint) 직전 단계까지 완료합니다.
      따라서 DOM 조작이나 외부 API 호출은 반드시 useEffect 안에서 수행해야 합니다.

  • 의존성 배열이 변경될 때마다 실행됩니다.

  • 필요시 clean up 함수를 반환하여 setup에서 생성한 리소스를 정리합니다.

정리 (cleanup) 함수

useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000);
  // 정리함수 예시 
  return () => clearInterval(id); 
}, []);
  • 리액트가 컴포넌트를 언마운트하거나 effect를 다시 실행하기 직전에 자동으로 호출하는 별도의 단계입니다.

    cleanup 함수는 실행 함수를 제거하는 것이 아니라, 리소스를 정리하는 로직으로 리액트가 자동으로 호출합니다.

    • 리액트 컴포넌트의 생명주기
      • 마운트: 화면에 추가 → 살아있는 상태
      • 업데이트: props/state 변화 → 살아있는 상태
      • 언마운트: 화면에서 제거 → 종료 상태
    • cleanup의 실행 시점
      • 의존성 배열의 값이 변경되었을때
        → 먼저 정리함수(CleanUp) 실행 ⇒ 그 다음에 setup 다시 실행
      • 컴포넌트가 DOM에서 제거되어 언마운트 되었을 때

cleanup이 필요한 이유 : 메모리 누수, setup의 중복 실행을 방지

  • 컴포넌트가 언마운트될 때 cleanup이 없으면, 이전 effectsetup에서 생성한 자원(타이머, 이벤트 리스너 등)이 계속 메모리를 차지하게 됩니다.
  • 또한, 의존성이 바뀌면 setup함수가 다시 실행되는데 이때, 이전의 setup이 남아있어 중복 실행될 수 있습니다.
  • cleanup가 필요한 경우
    구분설명예시cleanup 필요 여부
    네트워크 연결서버와 지속적 연결WebSocket, SSE✅ 필요
    브라우저 APIDOM 이벤트, 타이머addEventListener, setInterval✅ 필요
    외부 데이터fetch, axiosAPI 호출✅ 필요
    서드파티 객체외부 라이브러리Chart.js, Mapbox✅ 필요
    내부 상태 로직리액트 상태 기반 UIsetState❌ 불필요
    • cleanupsetup에서 생성한 리소스만 정리해야됩니다.

의존성(dependecies) 배열

  • 의존성 배열은 선택할 수 있지만, 배열에 들어갈 항목은 고정된 값입니다.

    • 배열 안에 포함되는 값들은 effect 내부 코드에서 참조되는 모든 반응형 값(props, state, 컴포넌트 내부 변수/함수) 이어야 합니다.
  • 리액트의 린터(ESLint-plugin-react-hooks)는 누락된 의존성을 자동으로 검출하고 경고를 표시합니다.

    • 경고하는 이유는 의존성 누락 시 stale state(오래된 값 참조) 버그를 유발할 수 있기 때문입니다.
    • 코드 린터(Linter) : 코드를 작성하는 동안 실시간으로 문제를 찾고 해결하는데 도움을 줌
    • 의존성 검사 과정
    1. Lint 검사 → 먼저 코드에서 의존성 배열에 필요한 값이 누락되지 않았는지 확인합니다. (Linter)
    2. 반응값 의존성 검증 → 의존성 배열에 있는 값들이 실제로 존재하는지 체크 (반응값 의존성 검증)
    3. Object.js → 실제 의존성 값들을 모아 관리하는 객체를 만듦
    4. Object.js에서 이전 값과 비교 → 이전 렌더 때 값과 지금 값이 같은지 비교 (이전값과 비교)
    • 같음 → effect setup은 실행되지 않음 (effect setup 실행 X)

    • 다름 → effect setup 실행 (effect setup 실행)

      ⇒ 리액트는 의존성 배열의 각 항목이 이전 렌더링과 값이 달라졌는지(Object.is 기준) 비교해 다를 때만 effect를 재실행합니다.
      ⇒ 만약, 의존성 배열에 의존성이 없다면, 컴포넌트는 리렌더링될 때마다 실행될 수 있습니다.

  • useEffect 의존성 배열 사용 예시

//의존성 배열 사용X
useEffect(() => {
  // 모든 렌더링 후에 실행됩니다
});

// 의존성 배열사용 O - 의존성 없음 
useEffect(() => {
  // 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);

// 의존성 배열사용 O - 의존성 있음 
useEffect(() => {
 // 마운트될 때 실행되며, 또한 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);

그래서 useEffect는 언제 호출될까?

  • useEffect 실행 과정

    1. 처음 렌더 → 실행함수(setUp) 실행
    2. 의존성 변경 → 먼저 정리함수(cleanUp) 실행 ⇒ 그 다음에 setup 다시 실행
    3. 컴포넌트 언마운트 → cleanup 실행
  • 컴포넌트가 화면에 추가되었을때 === 컴포넌트 마운트 되었을 때

  • 의존성 배열이 변경되었을때 === 리렌더링이 발생했을 때

  • 컴포넌트가 화면에서 제거되었을 때 === 컴포넌트 언마운트 되었을 때

💡 useEffect에 관한 궁금증 정리

1. 의존성 배열이 없을 때와 빈 배열일 때의 차이는 뭘까?

의존성 배열이 없을 때는, 리액트가 어떤 값이 바뀌었는지를 추적하지 않기 때문에 모든 렌더링마다 useEffect가 실행됩니다.
따라서 useEffect 내부에서 상태를 변경하면 렌더링 → useEffect 실행 → 상태 변경 → 렌더링의 무한 루프가 생길 수 있습니다.

반면, 빈 배열([]) 을 전달하면 리액트는 “이 effect는 어떤 값에도 의존하지 않는구나”라고 판단해서 컴포넌트가 처음 마운트될 때 딱 한 번만 실행합니다. 이후에는 의존성 변경이 없기 때문에 재실행되지 않습니다.

2. useEffect 안에서 console.log 찍었을 때 왜 두 번 찍힐까?

리액트18에서 StrictMode가 켜진 개발 환경에서는 useEffect두 번 실행되는 경우가 있습니다.

그 이유는 리액트가 부수효과와 cleanup 함수가 올바르게 작성되었는지 검증하기 위해서입니다.

구체적으로, 컴포넌트가 마운트되면 useEffect가 실행되고, cleanup 함수가 호출된 뒤 다시 useEffect가 실행됩니다.

이렇게 해서 개발자가 작성한 cleanup 함수가 effect를 완전히 정리하는지, 부수효과가 안전하게 재실행될 수 있는지 확인할 수 있습니다.

실제 프로덕션 환경에서는 이 동작이 한 번만 실행됩니다

3. 의존성 배열에 객체나 함수를 넣으면 어떤 문제가 생길까?

객체와 함수는 참조 타입이기 때문에, 렌더링마다 새로운 객체나 함수가 생성되면 참조가 달라졌다고 판단되어 useEffect가 불필요하게 재실행될 수 있습니다.

  • 참조 타입: 변수에 값 자체가 아니라 값이 저장된 메모리 주소가 들어감

이로 인해 불필요한 API 호출이나 DOM조작이 반복되거나, 메모리 사용이 증가할 수 있습니다.

해결 방법으로는 useMemouseCallback을 사용해 참조를 고정시켜 불필요한 effect 재실행을 방지할 수 있습니다.

4. useEffect가 여러 개 있을 때 실행 순서는 어떻게 될까?

// useEffect가 여러 개 있을 때
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("");

  // 첫 번째 useEffect
  useEffect(() => {
    console.log("첫 번째 useEffect 실행");
    setCount(prev => prev + 1);
  }, []);

  // 두 번째 useEffect
  useEffect(() => {
    console.log("두 번째 useEffect 실행, count:", count);
    setMessage(`Count is ${count}`);
  }, [count]);

  // 세 번째 useEffect
  useEffect(() => {
    console.log("세 번째 useEffect 실행, value:", message);
  }, [message]);

컴포넌트에 useEffect가 여러 개 있을 경우, 각 useEffect독립적으로 동작합니다.

모든 useEffectcommit 단계 후 실행되며, jsx 파일에 작성된 코드를 기준으로 위에서부터 아래로 실행됩니다.

  • 예시 코드 실행 흐름
    1️⃣ 첫 번째 useEffect 실행 
    → console.log("첫 번째 useEffect 실행")
    → setCount(prev + 1) 
    → count 상태 업데이트

    2️⃣ 두 번째 useEffect 실행 (count 변경 감지) 
    → console.log("두 번째 useEffect 실행, count:", count)
    → setMessage(`Count is ${count}`) 
    → message 상태 업데이트

    3️⃣ 세 번째 useEffect 실행 (value 변경 시만 실행) 
    → console.log("세 번째 useEffect 실행, value:", value)

useEffect의존성 배열을 기준으로 실행 여부가 결정됩니다.

의존성 배열이 변경되면 해당 effect만 재실행되고, 나머지 useEffect는 영향받지 않습니다.

즉, 렌더링 후 순서대로 effect가 실행되지만, effect는 서로 독립적이므로 어던 effect의 실행이 다른 effect의 실행을 막지 않습니다.

5. 만약 useEffect 안에 비동기 함수를 직접 사용하면 어떤일이 생길까?

// 문제 상황
useEffect(async () => {
  const data = await fetchData();
  setData(data);
}, []);

useEffect 안에서 비동기 함수를 직접 async로 선언하면, useEffect 콜백이 Promise를 반환하게 되어 리액트가 이를 처리할 수 없습니다.

왜냐하면 리액트에서 useEffect 콜백 함수가 순수 동기 함수일 것으로 기대하기 때문입니다.

리액트는 콜백이 값을 반환하지 않거나 undefined를 반환할 것으로 예상합니다.

그런데 async를 붙이면 콜백 함수는 항상 Promise를 반환하게 되기 때문에 리액트에서 처리할 수 없습니다.

또한 비동기 작업이 완료되기 전에 화면이 렌더링되면, 데이터가 없는 상태로 화면이 그려지기 때문에 사용자 경험에 영향을 줄 수 있습니다.

따라서 리액트는 useEffect 안에서 별도의 async 함수를 선언하고 호출하는 방식을 권장합니다.

// 해결방법 
useEffect(() => {
  const fetchData = async () => {
    const data = await fetchSomething();
    setState(data);
  };
  
  fetchData();
}, []);

6. cleanup 함수를 작성하지 않으면 어떤 문제가 생길까?

cleanup 함수를 작성하지 않으면, 컴포넌트가 언마운트될 때 리소스를 정리하지 못해 메모리 누수가 발생할 수 있습니다.
또한 setInterval, 이벤트 리스너, WebSocket 연결 등은 계속 활성 상태로 남아 불필요한 동작을 유발할 수 있습니다.
따라서 useEffect에서는 생성한 리소스를 cleanup 함수에서 반드시 해제하여, 메모리 사용과 이벤트 실행을 안전하게 관리해야 합니다.

7. useEffect안에서 useState를 호출하면 어떻게 될까?

// 문제 상황
useEffect(() => {
  const [count, setCount] = useState(0); 
  setCount(count + 1);
});

useEffect안에서 useState를 호출하면 무한루프가 발생할 수 있습니다.
useEffect 안에서 useState를 호출하면, 상태가 변경되면서 컴포넌트가 다시 렌더링됩니다.

만약 의존성 배열이 잘못 설정되었거나 없으면, useEffect는 렌더링 후 다시 실행되고, 다시 state를 업데이트하는 순환이 발생하기 때문입니다.

따라서 useEffect 안에서 상태를 업데이트할 때는 의존성 배열을 정확히 관리하거나, 업데이트 조건을 제한하는 방식으로 무한 루프를 방지해야 합니다.

//해결방법
const [count, setCount] = useState(0); // ✅ 최상위에서 훅 호출

useEffect(() => {
  setCount(prev => prev + 1); // ✅ 상태 업데이트만 수행
}, []); // ✅ 의존성 배열로 실행 횟수 제어

8.useEffect 안에서 props를 읽는데 의존성 배열에 안 넣으면 왜 경고가 나올까?

//문제상황
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    // userId를 사용하는데 의존성 배열에 없음
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUserData(data));
  }, []); // ❌ userId가 바뀌어도 effect 재실행 안 됨

useEffect 안에서 props를 읽는데 의존성 배열에 포함하지 않으면, 해당 props가 변경되어도 effect가 재실행되지 않습니다.

이로 인해 effect는 최신 데이터를 기준으로 동작하지 않게 되고, 화면이나 부수효과가 예상과 다르게 동작할 수 있습니다.

리액트는 이러한 문제를 예방하기 위해 “missing dependency” 경고를 발생시킵니다.

따라서 useEffect에서 사용하는 모든 외부 값(props, state 등)은 의존성 배열에 포함시켜 effect가 항상 최신 데이터를 기준으로 실행되도록 해야 합니다

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUserData(data));
  }, [userId]); // ✅ userId 변경 시 effect 재실행

마치며

“useEffect는 언제 호출될까?” 질문의 시작으로 useEffect에 관한 궁금증을 모두 정리해 보았습니다.
글을 쓰면서 느낀 점은 "내가 리액트 렌더링 과정을 정확히 알고 있었나..?", "내가 useEffect의 실행 로직을 알고 있었나..?"라는 생각이 들었고 과거의 저는 정말 수박 겉핥기 식으로 코드를 작성했구나를 느낄 수 있었습니다. 비록 그동안 수박 겉핥기 식으로 useEffect를 사용했지만, 앞으로는 실행 시점과 cleanup, 의존성을 고려해서 useEffect를 사용할 수 있을 것 같습니다! 그리고 브라우저의 동작원리에 대해서 다시 공부하고 정리해야 될 것 같습니다..배움의 축복이 끊이질 않네요


참고자료

https://ko.react.dev/learn/render-and-commit
https://www.maeil-mail.kr/question/64
https://ko.react.dev/reference/react/useEffect
https://ko.react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development

profile
💡프론트엔드 공부 기록

1개의 댓글

comment-user-thumbnail
2025년 10월 31일

useEffect 완벽 이해 완료

답글 달기