Next.js
React에서 이전 상태를 참조한 저장 알림이 사라지지 않았던 이유
저장 완료 토스트가 엉뚱한 시점에 꺼진 문제를 stale closure와 타이머 정리로 고친 짧은 디버깅 노트.
저장은 됐는데 알림이 이상하게 사라졌다
프로필 저장 화면에서 저장 버튼을 두 번 빠르게 누르면, 두 번째 “저장됨” 알림이 1초도 안 돼 사라졌다. 서버 저장 API는 정상이었다. 화면의 작은 타이머 문제라 대수롭지 않게 봤지만, 사용자는 저장 실패로 받아들일 수 있는 상태였다.
처음 코드는 성공할 때마다 setTimeout을 만들고 2초 뒤 setSaved(false)를 호출했다. 앞선 요청의 타이머가 살아 있어서, 나중 요청이 켠 메시지까지 꺼 버린 것이다. 콜백이 만들어진 시점의 상태와 작업을 계속 잡고 있는 현상을 stale closure로 이해하게 됐다.
타이머 하나만 유지하도록 바꿨다
타이머 ID는 화면에 그릴 값이 아니므로 useRef에 두었다. 새 성공 메시지를 보여 줄 때는 이전 타이머를 먼저 취소했다.
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const [saved, setSaved] = useState(false);
function showSavedNotice() {
if (hideTimer.current) clearTimeout(hideTimer.current);
setSaved(true);
hideTimer.current = setTimeout(() => {
setSaved(false);
hideTimer.current = null;
}, 2000);
}
useEffect(() => {
return () => {
if (hideTimer.current) clearTimeout(hideTimer.current);
};
}, []);
컴포넌트가 사라진 뒤에도 타이머가 실행되는 경우도 막기 위해 cleanup을 넣었다. 개발 환경 Strict Mode의 재실행을 억지로 피하는 코드를 추가하지는 않았다. 타이머를 정리하는 코드라면 여러 번 실행돼도 안전해야 한다고 기준을 바꿨다.
버튼 연타만 막으면 충분하지 않은 이유
저장 중에는 버튼을 비활성화했다. 그래도 네트워크 재시도, 다른 이벤트, 컴포넌트 재진입처럼 같은 완료 알림이 겹칠 경로는 남는다. UI 상태의 수명은 요청 수명과 별개로 설계해야 했다.
마무리 점검
- 성공 콜백이 예약한 작업을 다음 성공이 취소해야 하는가?
- unmount 뒤 실행될 타이머, 구독, 요청이 남는가?
- 빠르게 두 번 실행했을 때 메시지가 의도한 시간만큼 보이는가?
작은 토스트였지만, “나중에 실행되는 함수”는 언제 만들어졌는지부터 보는 습관을 남겼다.