오늘 나는 useEffect Cleanup 및 Race Conditions을 이해했다 (usePopcorn에서 얻은 실제 교훈)
Source: Dev.to

빠른 타이핑이 내 앱을 망가뜨렸지만, 서서히 진실을 드러냈다
앱 초반에 이상한 현상을 발견했다:
- 느린 검색 → 올바른 결과
- 빠른 타이핑 → 오류 또는 잘못된 데이터
처음엔 API 문제인 줄 알았지만, 실제로는 레이스 컨디션이었다. 여러 요청이 동시에 발생하고, 오래된 응답이 최신 응답 뒤에 상태를 업데이트하는 경우가 있었다.
레이스 컨디션: 올바른 사고 모델
레이스 컨디션은 React가 응답을 순서대로 받지 못할 때 발생한다. 해결책은 요청을 “기다리게” 하거나 “지연”시키는 것이 아니라 구식 작업을 취소하는 것이다. 여기서 AbortController가 등장한다:
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort(); // cleanup
클린업을 통해 최신 요청만 완료될 수 있게 하여 앱을 안정화한다.
클린업 함수는 선택 사항이 아니다
예전에는 클린업 함수를 보너스처럼 여기곤 했다. 이제는 클린업이 효과(effects)의 생명 주기의 일부라는 것을 이해한다.
문서 제목을 바꾸는 간단한 예시:
document.title = `Movie | ${title}`;
return () => {
document.title = "usePopcorn";
};
클린업이 없으면 부수 효과가 이후 렌더링에 남아버린다. 클린업을 하면 React가 제어권을 유지한다.
클린업은 조용한 버그를 방지한다 (이벤트 리스너)
이벤트 리스너도 적절한 클린업이 필요하다:
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
클린업이 없을 때:
- 리스너가 누적됨
- 한 번의 키 입력이 여러 핸들러를 호출
클린업이 있을 때:
- 오래된 리스너가 제거됨
- 동작이 예측 가능하게 유지
이전에 고친 버그 — 오늘 비로소 이해한 버그
레이스 컨디션 문제는 이미 외부 도움을 받아 고쳤던 것이었다. 오늘은 단순히 고친 것이 아니라 그 뒤에 있는 시스템을 이해했다. 고치는 것에서 이해로 전환되는 순간이 진정한 학습이다.
이제 useEffect를 생각하는 방식
- 효과가 실행된다
- 클린업은 다음 효과가 실행되기 전에 실행된다
- 클린업은 언마운트 시에도 실행된다
- 비동기 효과는 항상 취소 가능해야 한다
이 흐름을 깨달은 뒤 useEffect는 더 이상 예측 불가능하게 느껴지지 않는다.
마무리 생각
오늘은 중요한 점을 다시 상기시켰다: 깨끗한 코드는 더 많이 쓰는 것이 아니라 필요한 것만 쓰는 것이다. 클린업 함수와 레이스 컨디션 처리는 고급 트릭이 아니라 React의 기본 스킬이다.
나는 아직 배우고 있고, 계속 개선하고 있으며, 이해를 다듬고 있다. 오늘의 깨달음 덕분에 여정이 더욱 가치 있게 느껴진다.