C.W.K.
Stream
Lesson 02 of 07 · published

useEffect — 실제로 필요할 때 (와 아닐 때)

~18 min · useeffect, synchronization, side-effects

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React 코드베이스에 할 수 있는 가장 큰 개선은 있으면 안 될 useEffect 제거. React 공식 문서가 'You Might Not Need an Effect' 라는 페이지 통째로 출하. 읽어. 그 다음 다시 읽어.

useEffect 가 실제로 뭘 위한 거

useEffect 가 React state 를 React 외부 무언가 와 동기화: DOM API (focus, scroll), 브라우저 API (이벤트 리스너, 타이머, geolocation), 네트워크 연결 (구독, websocket), React 모르는 서드파티 라이브러리. Side effect 가 순수하게 React 안 (파생 state 계산, 다른 state 업데이트) 이면 effect 안 원함.

Dependency array

  • useEffect(fn) — 매 render 후 돔. 매 render 마다 의도적으로 돌리는 게 아니면 거의 항상 잘못.
  • useEffect(fn, []) — 마운트 후 한 번 (cleanup 반환되면 unmount 시도).
  • useEffect(fn, [a, b]) — 마운트 후 + a 또는 b 가 reference equality 로 바뀔 때마다.

ESLint 룰 react-hooks/exhaustive-deps 가 빠진 의존성 플래그. 가볍게 suppress 마 — 빠진 dep 은 보통 버그.

Cleanup 함수

Effect 에서 함수 반환해서 cleanup. React 가 effect 재실행 전과 unmount 시 호출. 이게 이벤트 리스너 제거, 구독 취소, fetch abort 방식.

StrictMode 더블 호출

개발에서 React 의 <StrictMode> 가 의도적으로 모든 컴포넌트 두 번 마운트. 마운트 전용 effect 가 돔, cleanup, 다시 돔. 안전하게 idempotent 하지 않은 effect 들 surface 시키는 도구 — effect 가 더블 호출에서 깨지면 버그 (보통 cleanup 빠짐 또는 애초에 effect 면 안 되는 side effect). 프로덕션은 한 번.

Effect 면 안 되는 effect

React 문서가 명시적으로 부르는 흔한 anti-pattern 다섯:

  1. 렌더링용 데이터 변환 → render 에서 계산.
  2. 비싼 계산 캐싱 → useMemo 사용 (또는 Track 7 의 React Compiler 신뢰).
  3. Prop 바뀔 때 state 리셋 → 컴포넌트에 key 사용.
  4. 다른 state 변경 기반 state 조정 → render 중에 의존 state 계산.
  5. 부모에게 변경 알림 → state 안정 후가 아니라 이벤트 핸들러에서 부모 콜백 호출.
Effect 의 유일한 일이 setState 호출이면 보통 effect 필요 없어. setState 가 변경을 일으킨 이벤트 핸들러나 계산에 있어야 함. Effect 는 외부 세계와 동기화용 — React state 업데이트 체인용 아님.

Code

캐논 이벤트-리스너 effect·tsx
import { useEffect, useState } from "react";

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);

    // Cleanup — unmount 시 + 이 effect 다음 재실행 전에 돔.
    return () => window.removeEventListener("resize", onResize);
  }, []); // 마운트만

  return <p>Width: {width}px</p>;
}
AbortController 가진 fetch — race-safe·tsx
import { useEffect, useState } from "react";

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<{ name: string } | null>(null);

  useEffect(() => {
    const ctrl = new AbortController();
    fetch(`/api/users/${userId}`, { signal: ctrl.signal })
      .then((r) => r.json())
      .then(setUser)
      .catch((e) => {
        if (e.name !== "AbortError") throw e;
      });

    // userId 가 fetch 중에 바뀌면 이전 요청 abort —
    // stale 데이터가 state 에 안 들어옴.
    return () => ctrl.abort();
  }, [userId]);

  if (!user) return <p>Loading…</p>;
  return <p>{user.name}</p>;
}
Effect 면 안 되는 effect·tsx
// WRONG — effect 통해 setState 체이닝.
function Wrong({ firstName, lastName }: { firstName: string; lastName: string }) {
  const [fullName, setFullName] = useState("");
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`); // 매번 re-render 트리거
  }, [firstName, lastName]);
  return <p>{fullName}</p>;
}

// RIGHT — render 에서 derive.
function Right({ firstName, lastName }: { firstName: string; lastName: string }) {
  const fullName = `${firstName} ${lastName}`;
  return <p>{fullName}</p>;
}

External links

Exercise

Bootstrap 프로젝트에서 useEffect 하나 가져와 (또는 새로 작성 — 단순 document.title updater 도 OK). 처음엔 cleanup 없이 작성, 페이지 나갈 때 무슨 일 일어나는지 관찰. 그 다음 cleanup 추가. <StrictMode> 로 돌려서 더블 호출 동작 확인. 그 다음 자문: 이게 effect 가 *필요한* 건가, render 시 계산이면 충분한가?
Hint
useEffect(() => { document.title = Count: ${count}; }, [count]) 는 실제 effect — document 만지니까, React 외부. 옛 title 복원하는 cleanup 추가가 다듬은 버전. 다만 render 에서 Count: ${count} 계산하면 그냥 plain JSX — 인라인 렌더면 effect 필요 없음.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.