여러 단계의 비동기 UI 업데이트를 setLoading → setProgress → setLoading(false)로 흩뿌리지 않고 async generator의 yield 시퀀스로 시간 순서대로 표현. 원본 트릭은 @ericclemmons.
type FlowEvent<P extends string, D> = { phase: P; data: D }
async function* loadingFlow(
signal: AbortSignal,
): AsyncGenerator<FlowEvent<'starting' | 'slow' | 'done', string>> {
yield { phase: 'starting', data: 'Starting…' }
await wait(1000, signal)
yield { phase: 'slow', data: 'Taking longer than usual' }
await wait(2000, signal)
yield { phase: 'done', data: 'Got it 🎉' }
}
원본의 빈 자리 → 소비 레이어 책임
원본 데모는 4가지가 비어 있음. hook이든 actor든 wrapping 레이어에서 채워야 함:
- cancellation: generator에
AbortSignal주입 →await대상(wait,fetch)이 signal-aware해야 진짜 취소됨 - 재진입: 새 실행 시 직전 iterator abort
- error path: generator 내부 throw → 상태 노출
- unmount/teardown: cleanup으로 in-flight iterator abort
- stale closure: 최신 클로저 참조 (hook은 ref, actor는 input)
- typed:
phase/data모두 좁힘
어디에 맞는가
generator는 시간이 코드의 한 방향(↓)으로만 흐르는 모델. 그 모양에 맞는 시나리오:
- 멀티 단계 loading 메시지 (위
loadingFlow) - progressive search — tier별로 점진적 결과 yield, tier 경계가 자연스러운 cancellation point, debounce 불필요
- optimistic mutation —
{ phase: 'optimistic' | 'reconciled' | 'rolledBack', data }전이를 4개 콜백 대신 한 함수로
useClickOutside — ref callback 방식 정리
- ref callback은 DOM 노드 참조만 전달할 뿐, 내부에서 등록한 이벤트 리스너 등의 사이드이펙트는 자동 해제되지 않음
- React 18 이하: unmount 시
callback(null)호출되지만, 동일 함수 참조가 없어removeEventListener곤란 → 별도 ref 저장 필요 - React 19:
useEffect처럼 cleanup function return을 공식 지원 →return () => removeEventListener(...)패턴 사용 가능 - stale closure 방지는
handlerRef.current = handler패턴으로 해결 - 결론: React 19라면 ref callback + cleanup return이
useEffect방식의 깔끔한 대안
function useClickOutside(handler) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
return useCallback((node) => {
const handleClick = (e) => {
if (!node.contains(e.target)) handlerRef.current();
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
}
// 사용
function Modal({ onClose }) {
const ref = useClickOutside(onClose);
return <div ref={ref}>...</div>;
}