리액트 스터디 4주차 - useEffect

Nomore·2025년 9월 10일
0

ReactStudy

목록 보기
5/7

리액트 스터디 4주차 — useEffect 핵심 가이드 (실행 조건 · 의존성 배열 · 클린업 · 주의점)

useEffect는 컴포넌트가 React 바깥의 시스템(DOM 이벤트, 네트워크, 타이머, 외부 라이브러리 등)과 동기화할 때 쓰는 훅입니다. 본 글에서는 전체 API를 모두 다루지 않고, 실무에서 가장 자주 마주치는 네 가지 축—언제 실행되는가, 의존성 배열을 어떻게 적는가, 클린업은 언제/왜 필요한가, 무엇을 주의해야 하는가—만 정확히 정리합니다.


1) useEffect가 언제 실행되는가

useEffect화면이 실제로 바뀐 다음에 실행됩니다.
조금 더 풀어서 말하면, React는 먼저 화면을 어떻게 바꿀지 계산(렌더)하고, 그 결과를 실제 DOM에 반영(커밋)합니다. 사용자가 바뀐 화면을 볼 수 있는 상태가 된 뒤에야 useEffect 안의 코드가 실행됩니다.

또한 useEffect는 값이 바뀔 때마다 이전 동기화를 멈추고(정리/클린업) 새 값으로 다시 시작합니다. 즉, “시작 → 중지 → 다시 시작”의 흐름으로 동작합니다.

정리하면 다음과 같습니다.

  • 커밋 후 실행: 화면(DOM)이 실제로 업데이트된 다음 useEffect가 실행됩니다.
  • 재실행: 의존성 배열에 적은 값이 바뀌면, 이전 효과를 정리한 뒤 최신 값으로 다시 실행됩니다.
  • 언마운트 시 정리: 컴포넌트가 화면에서 사라질 때 마지막으로 정리를 수행합니다.

간단 예시:

import { useEffect, useState } from "react";

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

  useEffect(() => {
    // 이 로그는 화면에 count 값이 반영된 "뒤에" 찍힙니다.
    console.log("useEffect 실행 (커밋 이후):", count);

    // 다음 렌더에서 count가 바뀌면 먼저 이곳이 정리(cleanup)된 뒤,
    // 새 count 값으로 다시 위의 코드가 실행됩니다.
    return () => {
      console.log("이전 효과 정리:", count);
    };
  }, [count]);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {count}
    </button>
  );
}

위 코드를 클릭해 count가 증가하면, 먼저 버튼의 숫자가 화면에서 바뀌고(커밋), 그 다음에 useEffect가 실행됩니다. 이후 다시 눌러 값이 바뀌면, 이전 효과를 정리한 뒤 새 값으로 효과가 재실행됩니다.


2) 의존성 배열(deps)의 의미와 작성 원칙

의존성 배열은 이 효과가 어떤 값에 ‘반응’할지를 선언합니다.

  • useEffect(fn) — 의존성 생략: 매 렌더 후 실행
  • useEffect(fn, []) — 빈 배열: 초기 1회 실행 + 언마운트 시 클린업
  • useEffect(fn, [a, b]) — 배열의 값이 바뀔 때만 실행

원칙은 단순합니다. 효과 내부에서 읽는 ‘반응형 값’(프롭/상태/컨텍스트/함수/메모된 값 등)은 모두 의존성에 기입해야 합니다. 누락하면 과거 값을 잡아두는 stale closure 문제가 발생합니다. 이를 자동 점검하기 위해 eslint-plugin-react-hooksreact-hooks/exhaustive-deps 규칙 사용을 권장합니다.

의존성이 너무 자주 바뀌어 불필요한 재실행이 생긴다면, 콜백/값을 useCallback/useMemo안정화해 의존성 변화를 줄이십시오(의존성 누락의 대체가 아닙니다).

간단 예시:

useEffect(() => {
  // props.userId, token, onLoaded를 읽으므로 모두 deps에 명시
  // 읽는 값이 바뀌면 이전 동기화를 중단하고 최신 값으로 재실행
  fetchUser(props.userId, token).then(onLoaded);
}, [props.userId, token, onLoaded]);

3) 클린업(cleanup): 언제, 왜, 어떻게

동기화를 멈추는 방법입니다. useEffect에서 함수를 반환하면 React가 다음 시점에 그 함수를 호출합니다.

1) 다음 실행 직전(의존성이 바뀌어 재실행되기 전)
2) 컴포넌트 언마운트 시

이를 통해 이벤트 리스너 제거, 타이머 해제, 요청 취소 등 누수(leak)를 방지합니다.

이벤트 리스너 예시:

useEffect(() => {
	function onResize() {
    // ...
    }
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize); // 정리
}, []); // 한 번만 등록

네트워크 요청 취소(경쟁 상태 방지) 예시:

useEffect(() => {
	const ac = new AbortController();
	(async () => {
		const res = await fetch(`/api/items?q=${query}`, { signal: ac.signal });
		// ...
	})();
	return () => ac.abort(); // 최신 검색어로 전환되면 이전 요청 중단
}, [query]);

4) 실무에서 자주 생기는 주의점

① 불필요한 Effect 남용
“값이 바뀌면 상태를 또 세팅” 같은 내부 로직은 종종 Effect 없이 계산/파생 상태로 처리할 수 있습니다. 외부 시스템과의 동기화가 아니라면 Effect를 제거하는 편이 코드가 단순해지고 오류도 줄어듭니다.

② 비동기 함수 직접 전달 금지
useEffect(async () => { ... })처럼 async 함수를 곧장 전달하지 마십시오. 반환값이 Promise가 되어 클린업 시그니처와 충돌합니다. 내부에서 즉시 실행하는 async 함수를 정의해 호출하십시오(상기 예시 참고).

③ 이벤트 vs 효과의 경계
사용자 상호작용에 즉시 반응할 일은 이벤트 핸들러에, 외부 시스템과의 지속적 동기화는 Effect에 두면 설계가 선명해집니다.


마무리

  • useEffect렌더 후 외부 시스템과의 상태를 시작/중지하는 도구입니다.
  • 의존성 배열에는 효과가 읽는 모든 반응형 값을 정직하게 적습니다.
  • 반환 함수로 클린업을 구현하면 재실행·언마운트 시 누수를 막습니다.
  • 효과가 정말 필요한지, 이벤트/계산으로 대체할 수 없는지를 먼저 점검하십시오.

0개의 댓글