Hooks와 useEffect를 의도로 설계하기
useEffect 필요 조건, 책임 분리, 비동기/cleanup, 커스텀 훅 반환 패턴 정리
Posted
By okorion
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가 필요한 경우는 딱 세 가지다.
- 외부 시스템과 동기화
- API 요청
- 브라우저 API (localStorage, event listener)
- 렌더링 결과를 기준으로 실행돼야 하는 작업
- 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은:
- 하나의 역할
- 명확한 반환 구조
- 사용자는 “무엇을 쓰는지”만 알면 된다
일반적인 패턴:
stateactions- (선택)
derived
1
const { data, status, refetch } = useUsers();
실무 체크리스트
- Hook 이름만 보고 역할이 떠오르는가
- 반환값이 상태/행동으로 정리돼 있는가
- 내부 useEffect 존재를 사용자가 몰라도 되는가
- 여러 Effect가 있다면 책임이 분리돼 있는가
- Hook 사용이 컴포넌트 복잡도를 실제로 줄였는가
결론: useEffect를 줄이는 게 아니라, 의도를 드러내라
좋은 React 코드는 useEffect가 많이 없는 코드가 아니라, 왜 필요한지 설명 가능한 코드다.
- Effect는 렌더링의 예외다
- 책임은 쪼갤수록 명확해진다
- 비동기는 항상 수명 관리가 필요하다
useEffect를 쓰기 전에 항상 한 번만 자문하면 된다.
“이건 렌더링의 문제인가, 아니면 렌더링 밖의 문제인가?”
- 참고: 클린코드 리액트(React)
This post is licensed under CC BY 4.0 by the author.
