정의
이미지를 화면에 띄우려고 브라우저 메모리에 있는 데이터(blob)를 <img>에 물릴 때, URL.createObjectURL(blob)로 임시 주소를 만들어 쓴다. 이 임시 주소를 React 컴포넌트에서 useMemo로 생성하고 useEffect 정리 함수에서 URL.revokeObjectURL로 해제하면, 화면이 여러 번 렌더될 때 "이미 버린 주소를 다시 <img>에 세팅"하는 순서 꼬임이 생겨 이미지가 간헐적으로 안 뜬다. 이 문서는 그 레이스(경쟁 상태)가 왜 생기는지와, 서버가 URL을 주는 이미지는 애초에 blob·objectURL 없이 직접 URL로 렌더해 문제를 원천 제거하는 방법을 다룬다.
여기서 objectURL은 blob(브라우저 메모리에 든 이진 데이터 덩어리)을 가리키는 임시 문자열 주소이고, revokeObjectURL로 풀어 줘야 그 데이터가 메모리에서 해제된다. 레이스(race condition) 는 두 동작의 실행 순서가 보장되지 않아 결과가 들쭉날쭉해지는 상태를 말한다.
왜 필요한가
증상부터 보자. 이미지가 새로고침할 때마다 "어떤 땐 보이고 어떤 땐 안 보인다". 콘솔에 네트워크 에러는 없다. 서버·CDN을 아무리 봐도 파일은 멀쩡하다. 원인은 서버가 아니라 클라이언트의 렌더 타이밍이다.
React는 개발 모드(StrictMode)에서 컴포넌트를 일부러 두 번 렌더하고, 동시성 렌더에서는 커밋을 준비했다가 버리기도 한다. 이때 objectURL 생성이 useMemo(계산 캐시)에, 해제가 useEffect(정리 훅)에 나뉘어 있으면, "정리에서 revoke → 다음 렌더에서 그 URL을 다시 <img>에 세팅" 같은 순서가 어긋난다. <img>가 이미 해제된 blob 주소를 물면 빈 이미지가 뜬다. 재현이 들쭉날쭉해 디버깅이 특히 어렵다.
동작 원리
useMemo는 값을 재계산하지 않으려는 캐시일 뿐, 만들고 치우는 생명주기를 관리하는 도구가 아니다. 그래서 여기에 createObjectURL 같은 부작용을 넣으면 언제 해제될지를 React가 보장하지 않는다.
핵심은 생성과 해제가 서로 다른 훅에 있어 커밋/언마운트 사이클과 짝이 안 맞는 것이다. 반면 서버가 서빙하는 이미지(예: /media/photo/123 같은 프록시 경로)는 이미 안정적인 URL이므로, blob→objectURL 변환이라는 중간 단계 자체가 불필요하고 레이스의 원인이 된다.
실무 적용
원칙 1 — 서버 URL 이미지는 objectURL 없이 직접 렌더.
원칙 2 — objectURL이 불가피하면 생성·해제를 같은 effect에 페어링.
원칙 3 — prop 변경 시 state 리셋은 effect가 아니라 렌더 중 이전-prop 비교.
원칙 4 — 로드 실패 폴백은 <img onError> 스테이지 머신. <picture><source type="image/webp">는 브라우저가 webp를 지원하는지만 보고 소스를 고른다. 지원하는데 그 파일이 404면 폴백을 하지 않는다. 실제 로드 실패까지 폴백하려면 onError로 webp → 원본 → null 순서를 직접 몰아야 한다.
트레이드오프
- 직접 URL 렌더: 레이스가 사라지고 브라우저·CDN·Service Worker 캐시를 그대로 받는다. 대신 서버가 그 이미지를 URL로 서빙할 수 있어야 한다(대개 프록시 라우트로 해결).
- objectURL: 파일 선택 미리보기, 완전 오프라인 blob, 다운로드 트리거처럼 네트워크 URL이 없는 경우엔 여전히 정답이다. 이때는 생성·해제를 반드시 한 effect에 묶는다.
사용하면 안 되는 경우
- 서버가 이미 URL로 주는 이미지를 blob으로 감싸는 것 — 순수 손해(레이스 + 캐시 불가).
useMemo/useCallback안에서createObjectURL·구독·타이머 같은 부작용 실행 — 정리 시점이 보장되지 않는다.
흔한 실수
- 생성은
useMemo, 해제는useEffect로 분리 → StrictMode/동시성에서 간헐 깨짐. - 해제를 아예 안 해 blob이 메모리에 누적(누수).
- prop이 바뀔 때 상태 리셋을 effect로 처리해 추가 렌더·경고 유발.
<picture>가 404 폴백까지 해 줄 거라 오해.
관련 개념
- race-safe-async-ui-requests — 렌더·비동기 타이밍에 자원 수명이 얽힐 때의 일반 레이스
- server-image-proxy-transcoding-cache — 서버가 안정적 이미지 URL을 제공하는 프록시 계층
- ssr-hydration-mismatch — 서버 렌더 이미지엔 onError 폴백을 못 걸므로 원본 유지