Post

Hooks와 useEffect를 의도로 설계하기

useEffect 필요 조건, 책임 분리, 비동기/cleanup, 커스텀 훅 반환 패턴 정리

Hooks와 useEffect를 의도로 설계하기

Hooks와 Effect

— useEffect를 “언제 쓰는지”가 아니라 “왜 쓰는지”로 이해하기


서론: useEffect는 편의 API가 아니라 제약 API다

React 코드가 복잡해지는 순간을 보면 공통점이 있다.
useEffect가 늘어나 있고, 그 이유를 설명하기 어렵다.

  • “여기서 데이터 불러와야 해서”
  • “이 값 바뀌면 뭔가 해야 해서”
  • “렌더링 후니까 useEffect겠지”

이런 설명이 나오는 순간, 코드는 이미 위험하다.
useEffect는 무엇이든 넣어도 되는 만능 훅이 아니라,
‘렌더링 외부 세계와 연결할 때만 쓰는 API’
다.

이 글의 목표는 useEffect를
문법이 아니라 ‘의도 기반 도구’로 다시 이해하게 만드는 것이다.


1) Hooks 설계 철학 요약

흔한 오해

  • “Hooks는 클래스 생명주기를 대체한 것”
  • “useEffect = componentDidMount + componentDidUpdate”
  • “필요하면 아무 데서나 써도 되는 함수”

왜 이렇게 쓰면 안 되는가

Hooks의 핵심은 “편의성”이 아니라 일관성이다.

  • 항상 같은 순서로 호출
  • 렌더링 흐름 안에서만 동작
  • 상태와 UI를 동기적으로 맞추기 위한 규칙

useEffect는 이 흐름에서 의도적으로 분리된 예외다.
즉, 렌더링과 직접 관련 없는 작업만 여기로 보내라는 뜻이다.

더 나은 사고 흐름

  • Hooks = UI와 상태를 렌더링 중심으로 맞추기 위한 도구
  • useEffect = 렌더 결과를 외부 세계와 동기화하는 통로

실무 체크리스트

  • 이 로직은 “렌더링 결과”와 직접 관련 있는가
  • Hooks 호출 순서가 바뀔 여지는 없는가
  • 이 작업이 렌더 중 실행되면 안 되는 이유가 명확한가
  • useEffect를 “편의” 때문에 쓰고 있지 않은가
  • Effect가 없다면 UI가 잘못 표현되는가

2) useEffect가 필요한 경우 / 아닌 경우

흔한 오해

  • “값이 바뀌면 무조건 useEffect”
  • “비동기면 useEffect”
  • “렌더 뒤에 실행되니까 useEffect”

왜 이렇게 쓰면 안 되는가

많은 useEffect는 사실 불필요한 중간 단계다.

  • 파생값 계산
  • 단순 조건 분기
  • state 동기화

이런 것들을 useEffect로 처리하면
상태 → Effect → 상태라는 우회 경로가 생긴다.

더 나은 사고 흐름

useEffect가 필요한 경우는 딱 세 가지다.

  1. 외부 시스템과 동기화
    • API 요청
    • 브라우저 API (localStorage, event listener)
  2. 렌더링 결과를 기준으로 실행돼야 하는 작업
  3. React가 관리하지 않는 세계와의 연결

그 외 대부분은 렌더링 중 계산이 맞다.

실무 체크리스트

  • 이 로직은 외부 세계와 연결돼 있는가
  • Effect를 제거하면 UI가 잘못되는가
  • 파생값을 state+effect로 우회하고 있지 않은가
  • “렌더링 후”여야만 하는 이유를 설명할 수 있는가
  • 이 Effect는 React 밖의 무언가를 만지는가

3) 하나의 Effect = 하나의 책임

흔한 오해

  • “관련 있으니까 하나의 useEffect에”
  • “의존성 배열 관리하기 귀찮아서”
  • “어차피 한 번에 실행되니까”

왜 이렇게 쓰면 안 되는가

Effect 안에 여러 책임이 섞이면:

  • 의존성 배열이 복잡해진다
  • 일부 로직만 다시 실행하기 어렵다
  • 수정 시 사이드 이펙트가 튀어나온다

더 나은 사고 흐름

useEffect는 작은 단위의 동기화 규칙이다.

  • 한 Effect = 하나의 이유로 실행
  • 의존성 배열 = “이 Effect가 언제 다시 필요해지는가”

실무 체크리스트

  • 이 Effect가 실행되는 이유를 한 문장으로 설명할 수 있는가
  • 서로 다른 이유의 로직이 섞여 있지 않은가
  • 의존성 배열이 “필요 조건”만 담고 있는가
  • 일부 로직만 수정할 때 Effect 전체를 건드려야 하는가
  • Effect 분리가 가독성을 높이는가

4) Effect 내부 비동기 처리 기준

흔한 오해

  • “async 붙이면 끝”
  • “에러 처리는 나중에”
  • “cleanup은 필요 없을 듯”

왜 이렇게 쓰면 안 되는가

Effect 내부 비동기는 다음 문제를 만든다.

  • race condition
  • 언마운트 후 setState
  • 오래된 응답이 최신 상태를 덮어씀

더 나은 사고 흐름

  • Effect는 비동기 작업의 시작과 종료를 책임
  • 결과 적용 전에 “이 Effect가 아직 유효한가”를 확인
  • cleanup은 옵션이 아니라 기본값
1
2
3
4
5
6
7
8
9
10
11
12
13
useEffect(() => {
  let cancelled = false;

  fetchData().then(result => {
    if (!cancelled) {
      setData(result);
    }
  });

  return () => {
    cancelled = true;
  };
}, [query]);

실무 체크리스트

  • 이 비동기 결과가 언제 무효가 되는가
  • 이전 요청이 나중 요청을 덮어쓸 가능성은 없는가
  • 언마운트 이후 setState 가능성은 차단됐는가
  • 에러/로딩 상태 책임이 명확한가
  • 이 로직을 custom hook으로 감출 수 있는가

5) Custom Hook 반환 설계 패턴

흔한 오해

  • “useEffect 많아지면 hook으로 빼자”
  • “그냥 코드 줄이려고”
  • “로직 숨기면 깔끔해지겠지”

왜 이렇게 쓰면 안 되는가

Custom Hook이:

  • 내부 구현을 그대로 노출하거나
  • 반환값이 난잡하거나
  • 책임이 불분명하면

복잡도는 이동할 뿐 줄지 않는다.

더 나은 사고 흐름

좋은 Custom Hook은:

  • 하나의 역할
  • 명확한 반환 구조
  • 사용자는 “무엇을 쓰는지”만 알면 된다

일반적인 패턴:

  • state
  • actions
  • (선택) derived
1
const { data, status, refetch } = useUsers();

실무 체크리스트

  • Hook 이름만 보고 역할이 떠오르는가
  • 반환값이 상태/행동으로 정리돼 있는가
  • 내부 useEffect 존재를 사용자가 몰라도 되는가
  • 여러 Effect가 있다면 책임이 분리돼 있는가
  • Hook 사용이 컴포넌트 복잡도를 실제로 줄였는가

결론: useEffect를 줄이는 게 아니라, 의도를 드러내라

좋은 React 코드는 useEffect가 많이 없는 코드가 아니라, 왜 필요한지 설명 가능한 코드다.

  • Effect는 렌더링의 예외다
  • 책임은 쪼갤수록 명확해진다
  • 비동기는 항상 수명 관리가 필요하다

useEffect를 쓰기 전에 항상 한 번만 자문하면 된다.

“이건 렌더링의 문제인가, 아니면 렌더링 의 문제인가?”


This post is licensed under CC BY 4.0 by the author.