React Effect 정리: 주문을 바꿨는데 이전 위치가 다시 보이는 이유
Effect가 시작한 배송 위치 구독을 의존성 변경과 화면 제거 전에 끝내고, 타이머·이벤트·요청의 정리 계약을 판단한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
Effect가 외부 연결, 타이머, 이벤트 감시를 시작했다면 그 작업을 멈추는 정리 함수도 같은 Effect에서 반환한다. React는 의존성이 바뀌어 새 Effect 설정을 실행하기 전 이전 정리를 실행하고, 컴포넌트가 화면에서 제거될 때 마지막 정리를 실행한다.
React가 정리 함수를 호출하는 시점과 외부 작업을 실제로 멈추는 방법은 구분한다. 타이머는
clearInterval, 이벤트 감시는 removeEventListener, 구독은 해당 API의 구독 취소 함수처럼 시작한
자원의 계약에 맞는 반대 동작을 사용한다.
이 글을 관통하는 상황: 새 주문을 골라도 이전 위치가 돌아온다
구독은 새 정보가 생길 때마다 callback 함수를 호출하도록 등록하고, 더 필요 없을 때 등록을 취소하는
방식이다. 다음 subscribeToDelivery는 실제 배송 서비스 대신 1초마다 위치를 보내는 작은 대역이다.
import { useEffect, useState } from 'react'
function subscribeToDelivery(orderId, onPosition) {
let step = 0
console.log(`${orderId} 구독 시작`)
const intervalId = setInterval(() => {
step += 1
const position = `${orderId} 위치 ${step}`
console.log(`${position} 수신`)
onPosition(position)
}, 1000)
return () => {
clearInterval(intervalId)
console.log(`${orderId} 구독 취소`)
}
}
function DeliveryTracker({ orderId }) {
const [position, setPosition] = useState('위치 확인 중')
useEffect(() => {
subscribeToDelivery(orderId, setPosition)
}, [orderId])
return <p>{position}</p>
}
export default function App() {
const [orderId, setOrderId] = useState('order-101')
const [showTracker, setShowTracker] = useState(true)
return (
<section>
<select value={orderId} onChange={event => setOrderId(event.target.value)}>
<option value="order-101">주문 101</option>
<option value="order-202">주문 202</option>
</select>
<button type="button" onClick={() => setShowTracker(show => !show)}>
{showTracker ? '위치 숨기기' : '위치 보기'}
</button>
{showTracker && <DeliveryTracker orderId={orderId} />}
</section>
)
}처음에는 order-101 위치 1, order-101 위치 2처럼 숫자가 올라간다. 주문 202를 선택하면 새 구독이
시작되지만 이전 주문 101의 반복 타이머도 살아 있다. 두 callback이 모두 setPosition을 호출하므로
새 주문 위치를 이전 주문 위치가 다시 덮어쓸 수 있다. 타이머 실행 순서는 보장되지 않으므로 매번
정확히 교대로 보인다고 가정하지 않는다. 위치를 숨겨 DeliveryTracker를 화면에서 제거해도
콘솔에서는 이전 callback이 계속 실행될 수 있다.
개발 Strict Mode에서는 처음 시작할 때의 로그 수가 달라질 수 있다. 개수를 맞추기보다 주문을 바꾼
뒤에도 주문 101의 수신 로그가 계속 생기는지를 본다. 잘못된 구현은 취소 함수의 참조를 버렸으므로
코드만 교체해도 이미 시작한 타이머를 회수할 수 없다. 실패를 확인한 뒤에는 브라우저 페이지를
완전히 새로고침해 기존 타이머를 끝내고 교정본을 초기 상태에서 검증한다.
잘못된 판단은 “orderId가 바뀌면 Effect가 다시 실행되므로 이전 구독도 교체된다”는 것이다. React는
외부 API가 setInterval을 썼는지, 어떤 구독 취소 함수를 제공하는지 알 수 없다. 이전 작업을 멈추는
방법을 Effect가 반환하지 않았으므로 새 작업만 하나 더 시작된다.
새 Effect를 시작해도 이전 외부 작업은 자동으로 끝나지 않는다
위 구독 함수는 구독 취소 함수를 이미 반환한다. 그러나 잘못된 Effect는 그 반환값을 버렸다.
useEffect(() => {
subscribeToDelivery(orderId, setPosition)
}, [orderId])새 orderId로 설정을 실행하는 것과 이전 orderId의 외부 작업을 멈추는 것은 별도 행동이다. React가
후자를 수행하게 하려면 Effect에서 정리 함수를 반환해야 한다.
정리는 새 설정 전과 화면 제거 때 실행된다
잘못된 Effect를 다음 코드로 교체한다.
useEffect(() => {
const unsubscribe = subscribeToDelivery(orderId, setPosition)
return unsubscribe
}, [orderId])unsubscribe는 구독을 취소하는 함수다. Effect에서 그대로 반환하면 순서는 다음처럼 닫힌다.
order-101 구독 시작
orderId 변경
order-101 구독 취소
order-202 구독 시작
위치 화면 제거
order-202 구독 취소정리 함수는 화면 제거 때만 실행되는 종료 callback이 아니다. 이전 동기화를 멈춘 뒤 다음 동기화를 시작하게 하는 경계이기도 하다.
주문 202를 선택한 직후 첫 202 위치가 도착하기 전까지는 상태에 마지막 101 위치 문자열이 잠시 남아 있을 수 있다. 이것은 이전 callback이 다시 실행된 결과가 아니다. 이 글에서 확인할 정리 실패는 첫 202 위치를 받은 뒤에도 101 수신 로그가 생기고 화면을 다시 덮는가이다. 주문 변경 즉시 “위치 확인 중”을 보여 주는 UI 정책은 별도로 설계할 수 있다.
시작한 자원의 계약에 맞는 반대 동작을 둔다
| Effect가 시작한 일 | 정리에서 대응할 일 | 함께 보관할 값 |
|---|---|---|
setInterval(callback, delay) | 반환된 ID로 clearInterval(id) 호출 | interval ID |
addEventListener(type, listener, options) | 같은 조건에 맞는 listener 제거 | listener 함수와 캡처 조건 |
subscribe(key, callback) | 라이브러리가 반환한 unsubscribe() 호출 | 구독 취소 함수 |
fetch(url, { signal }) | AbortController.abort() 또는 오래된 결과 무시 | controller 또는 무시 여부 |
interval ID는 실행 중인 반복 타이머를 식별하는 값이다. 브라우저 setInterval은 이 ID를 반환하고
clearInterval이 같은 타이머의 반복 실행을 멈춘다.
이벤트 listener는 이벤트가 발생할 때 호출하도록 등록한 함수다. 제거할 때는 등록할 때와 같은 이벤트 종류·listener 함수·캡처 조건이 맞아야 한다. 캡처 조건은 이벤트가 대상까지 전달되는 어느 단계에서 listener를 호출할지 정하는 등록 조건이다. 정리 안에서 모양만 같은 새 함수를 만들거나 다른 캡처 조건을 사용하면 등록했던 listener가 제거되지 않는다.
요청은 중단과 오래된 결과 무시를 구분한다
다만 모든 비동기 API가 실제 작업 중단을 지원하는 것은 아니다. 중단할 수 없다면 이전 작업 자체는 끝까지 실행되더라도 그 결과가 현재 화면 상태를 덮지 않게 무시해야 한다. 반대로 결과만 무시하는 플래그는 네트워크 전송이나 서버 처리를 실제로 취소했다는 뜻이 아니다.
따라서 요청 정리에서는 두 질문을 나눈다.
- 이 API는 진행 중인 작업을 실제로 중단할 수 있는가?
- 중단 성공 여부와 별개로 오래된 결과가 현재 상태에 반영되지 않게 했는가?
개발의 추가 주기는 정리를 검사하는 신호다
개발 콘솔에서 구독 시작 → 구독 취소 → 구독 시작이 보인다고 정리를 막거나 첫 실행 여부를 기록한
플래그로 이후 설정을 건너뛰지 않는다. 사용자가 한 번의 설정과 설정 → 정리 → 설정을 구분할 수
없도록 외부 상태를 정상적으로 되돌리는 것이 목표다.
주문 변경과 화면 제거를 함께 검증한다
잘못된 구현의 타이머를 확인한 뒤 페이지를 완전히 새로고침하고 교정한 Effect로 다음 순서를 확인한다.
- 주문 101 위치가 갱신되는 동안 주문 202를 선택한다.
- 101 구독 취소 뒤 202 구독 시작이 기록되는지 확인한다.
- 첫 202 위치를 받은 뒤 101 수신 로그나 위치가 다시 나타나지 않는지 확인한다.
- 위치를 숨긴 뒤 202 위치 callback이 더 실행되지 않는지 확인한다.
문제 주문을 바꿔도 이전 주문 위치가 다시 화면에 나타남
잘못된 판단 새 의존성으로 Effect가 실행되면 이전 외부 작업도 자동 교체됨
관찰 두 주문의 타이머 callback이 모두 setPosition을 호출하며 서로 덮을 수 있음
원인 구독 API가 반환한 취소 함수를 Effect에서 버림
행동 취소 함수를 Effect 정리 함수로 반환
검증 이전 구독 취소 후 새 구독 시작, 화면 제거 뒤 마지막 구독 취소풀 리퀘스트에서는 다음을 확인한다.
- Effect가 외부에 연결, 타이머, listener, 구독이나 요청을 남기는가?
- 시작한 API가 제공하는 실제 정지·해제 방법을 확인했는가?
- 의존성이 바뀔 때 이전 정리 뒤 새 설정 순서로 결과가 맞는가?
- 컴포넌트를 화면에서 제거한 뒤 callback이나 요청 결과가 상태를 바꾸지 않는가?
- 개발의 추가 설정·정리 주기를 숨기지 않고 반복해도 같은 결과가 되는가?
기억할 축은 짧다.
시작한 외부 작업마다 멈추는 방법을 같은 Effect에 둔다.
다음 글에서는 정리를 했어도 비동기 callback이 자신이 만들어진 Render의 이전 state와 props를 읽을 수 있는 이유를 JavaScript 함수가 바깥 값을 기억하는 동작과 연결해 살펴본다.
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01주문 번호가 바뀐 뒤 이전 주문 위치가 화면에 다시 나타날 수 있는 직접 원인은 무엇인가?
정답
이전 orderId로 시작한 구독의 타이머를 멈추지 않아 이전 callback과 새 callback이 모두 상태를 갱신하기 때문이다.
관련 설명 다시 읽기왜 그런가
새 Effect를 시작하는 것만으로 이전 Effect가 만든 외부 작업이 자동으로 사라지지는 않는다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02orderId가 바뀌거나 배송 위치 컴포넌트가 화면에서 사라질 때 React는 정리 함수를 언제 실행하는가?
정답
orderId 변경 뒤 새 설정을 실행하기 전에 이전 정리를 실행하고, 화면에서 제거될 때 마지막 정리를 실행한다.
관련 설명 다시 읽기왜 그런가
Effect를 한 번의 시작과 정지 주기로 보면 의존성 변경과 화면 제거를 같은 규칙으로 설명할 수 있다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03모든 Effect 정리 함수가 같은 코드를 실행해야 하는가?
정답
아니다. Effect가 시작한 외부 자원의 API 계약에 맞춰 타이머 취소, listener 제거, 구독 취소, 요청 중단이나 결과 무시를 선택한다.
관련 설명 다시 읽기왜 그런가
React는 정리 함수를 호출하지만 어떤 자원을 어떻게 멈출지는 외부 시스템의 계약이 결정한다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
04개발 환경에서 시작→정리→시작 로그가 보이면 정리 실행을 막아 한 번만 시작하게 해야 할까?
정답
아니다. 현재 React의 개발 Strict Mode는 정리가 대칭적인지 확인하려고 추가 설정·정리 주기를 실행할 수 있으므로, 한 번만 실행되게 숨기기보다 반복 후에도 같은 결과가 되게 한다.
관련 설명 다시 읽기왜 그런가
사용자가 설정 한 번과 설정→정리→설정을 구분할 수 없어야 안전한 Effect다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- Lifecycle of Reactive EffectsReact · 공식 문서 · 확인 2026-08-08
- Synchronizing with EffectsReact · 공식 문서 · 확인 2026-08-08
- useEffectReact · 공식 문서 · 확인 2026-08-08
- Window setInterval methodMDN Web Docs · 공식 문서 · 확인 2026-08-08
- EventTarget removeEventListener methodMDN Web Docs · 공식 문서 · 확인 2026-08-08
- AbortControllerMDN Web Docs · 공식 문서 · 확인 2026-08-08