본문 바로가기
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부터 줄여. 외부 시스템과 동기화할 때만 필요한 이유를 분명히 해 보자.

Effect는 React 밖의 시스템과 맞추는 도구야

Focus와 scroll 같은 DOM API는 effect의 대상이야. 이벤트 리스너와 타이머, 위치 정보, 네트워크 구독과 WebSocket, React를 모르는 서드파티 라이브러리도 여기에 들어가. 다른 상태에서 값을 계산하거나 상태 변경을 연달아 일으키는 일처럼 React 안에서 끝나는 작업에는 보통 effect가 필요하지 않아.

Dependency 배열은 실행 조건을 표현해

  • useEffect(fn)은 모든 렌더링 뒤에 실행돼.
  • useEffect(fn, [])은 마운트 뒤에 실행되고 unmount할 때 cleanup해.
  • useEffect(fn, [a, b])은 a나 b가 달라진 뒤 다시 실행돼.

react-hooks/exhaustive-deps 경고는 빠진 의존성 때문에 오래된 값을 읽을 수 있다는 신호야. 이유 없이 끄지 마.

Cleanup으로 이전 연결을 정리해

Effect에서 함수를 반환하면 React가 effect를 다시 실행하기 전과 컴포넌트를 제거할 때 호출해. 이벤트 리스너를 떼고, 구독을 해제하고, 진행 중인 fetch를 취소하는 코드를 여기에 둬.

StrictMode의 두 번 실행은 검사용이야

개발 모드에서는 StrictMode가 마운트, cleanup, 재마운트를 일부러 반복해. Cleanup이 빠졌거나 여러 번 실행해도 안전하지 않은 effect를 드러내기 위한 동작이야. 프로덕션에서는 한 번 실행되지만 개발 중 두 번 실행될 때 깨진다면 고쳐야 할 버그가 있는 거야.

Effect 대신 더 직접적인 방법을 써

렌더링용 데이터는 렌더링 중에 계산하고, 비싼 계산은 필요할 때 메모이제이션해. Prop이 바뀔 때 상태를 완전히 초기화해야 한다면 key를 바꾸는 방법을 검토해. 사용자 동작 뒤 부모에게 알려야 한다면 effect를 기다리지 말고 이벤트 핸들러에서 콜백을 호출해.

Effect의 유일한 일이 setState라면 설계를 다시 봐. 상태를 바꾼 사건이나 그 값의 계산 위치에서 직접 처리할 수 있는 경우가 많아.

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·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 중에 바뀌면 이전 요청을 취소해 —
    // 오래된 응답이 상태에 들어오지 않아.
    return () => ctrl.abort();
  }, [userId]);

  if (!user) return <p>Loading…</p>;
  return <p>{user.name}</p>;
}
effect가 아니어야 하는 상태 계산·tsx
// 잘못된 예: effect에서 파생 상태를 다시 저장해.
function Wrong({ firstName, lastName }: { firstName: string; lastName: string }) {
  const [fullName, setFullName] = useState("");
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`); // 매번 다시 렌더링돼
  }, [firstName, lastName]);
  return <p>{fullName}</p>;
}

// 올바른 예: 렌더링 중에 필요한 값을 바로 계산해.
function Right({ firstName, lastName }: { firstName: string; lastName: string }) {
  const fullName = `${firstName} ${lastName}`;
  return <p>{fullName}</p>;
}

External links

Exercise

창 크기를 구독하는 훅을 만들고 resize listener를 effect에서 등록한 뒤 cleanup에서 제거해. StrictMode에서 mount와 cleanup이 반복돼도 listener가 하나만 남는지 확인하고, 컴포넌트를 제거한 뒤 resize해도 state 변경이 일어나지 않게 해. 이어서 firstName과 lastName으로 fullName을 effect에서 저장하는 잘못된 예를 만든 뒤 렌더링 중 계산으로 바꿔 추가 렌더링이 사라지는지 비교해.
Hint
Event listener 수와 렌더링 로그를 함께 봐. 외부 브라우저 API와 동기화하는 첫 예제에는 effect가 필요하지만 두 state에서 계산할 수 있는 값에는 필요하지 않아.

Progress

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

댓글 0

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

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