2026-09-10: 설명과 코드 예시를 보완했습니다. 아래 예시는 글의 설계 의도를 전달하기 위한 것이며, 원 프로젝트에 반영된 변경 내역과는 구분합니다.

팝업은 열렸는데 내용이 없다. 이미지가 도착하기 전에 팝업의 틀부터 나타난 것이다. 개발 중에는 캐시 덕분에 지나치기 쉽고, 느린 연결에서는 그대로 드러난다.
이미지가 준비될 때까지 숨겨두면 될 것 같았다. 그런데 팝업의 자식이 꼭 img 하나인 것은 아니다. 제목과 설명 사이에 이미지가 들어갈 수도 있고, 여러 컴포넌트 안에 나뉘어 있을 수도 있다. 팝업마다 그 구조를 알아야 한다면 공통으로 쓰기 어렵다.
그래서 상위 요소의 onLoadCapture에서 이미지 로딩을 확인하려 했다. 여기서 숨기는 방법은 구분해야 한다. 화면에서 보이지 않는 것과 DOM에 아예 없는 것은 다르다.
마운트하지 않으면 load도 기다릴 수 없다
loaded가 false일 때 null을 반환하고, 자식 이미지의 load 이벤트로 loaded를 true로 만들려 하면 순환이 생긴다. 이미지 자체가 DOM에 없으니 이벤트도 받을 수 없다.
이 예시에서는 팝업의 틀과 로딩 안내를 먼저 보여준다. 이미지도 마운트하되 준비될 때까지 숨겨두고, 여러 이미지의 완료 여부와 실패를 함께 확인한다.
콘텐츠 준비 상태를 다루는 예시
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
export function ImageReadyContent({ children }: { children: ReactNode }) {
const root = useRef<HTMLDivElement>(null);
const [state, setState] = useState({ settled: false, failed: false });
const check = useCallback(() => {
const images = Array.from(root.current?.querySelectorAll('img') ?? []);
setState({
settled: images.every(image => image.complete),
failed: images.some(image => image.complete && image.naturalWidth === 0),
});
}, []);
useEffect(check, [check, children]);
return (
<section aria-busy={!state.settled}>
{!state.settled && <p role="status">이미지를 불러오고 있습니다.</p>}
{state.settled && state.failed && <p role="status">일부 이미지를 불러오지 못했습니다.</p>}
<div
ref={root}
onLoadCapture={check}
onErrorCapture={check}
style={{ visibility: state.settled ? 'visible' : 'hidden' }}
>
{children}
</div>
</section>
);
}
onLoadCapture는 자식 이미지에서 발생한 이벤트를 상위 요소에서 확인하는 데 쓴다. 이벤트 하나가 왔다고 모든 이미지가 준비된 것은 아니므로 각각의 상태를 다시 읽는다.
캐시된 이미지도 고려하기 위해 마운트 후 complete를 확인한다. complete는 로드 실패에서도 true일 수 있어 naturalWidth를 함께 확인한다. MDN 이미지 complete
<ImageReadyContent key={bannerUrl}>
<img src={bannerUrl} alt="이벤트 안내" width={640} height={320} loading="eager" />
</ImageReadyContent>
예시의 범위
이미지 집합이 바뀌면 key를 바꿔 준비 상태를 초기화한다. 이 코드는 마운트된 DOM의 일반 img를 대상으로 하며 배경 이미지, 폰트, Shadow DOM이나 iframe 안의 리소스까지 추적하지 않는다. 자식 컴포넌트가 나중에 이미지를 추가하는 구조라면 별도의 준비 상태 계약이 필요하다.
숨겨진 상태에서 로딩을 기다리는 이미지에는 지연 로딩을 섞지 않는 편이 단순하다. 완료되지 않는 요청을 위한 시간 제한과 재시도 정책은 제품에서 추가로 정해야 한다.
실제 Dialog가 닫힌 동안 자식을 제거한다면, 열기 전에 준비하려는 이미지는 별도의 preload 계층에서 다뤄야 한다. Dialog의 포커스 관리와 닫기 동작 역시 이 콘텐츠 예시와 별개다.
이미지는 보이지 않는 동안에도 마운트되어 있어야 하고, 로딩이 실패해도 사용자는 결과를 알 수 있어야 한다. 자식의 레이아웃을 매번 뜯어보지 않고도 그 상태를 확인하려던 것이 이 컴포넌트의 출발점이었다. 팝업을 열었는데 아무것도 없는 시간만큼은, 사용자에게 설명이 있는 시간으로 바꾸고 싶었다.
Assisted by AI
© uiwwsw · 본인 소유의 글·이미지에 대한 무단 복제·재게시를 허용하지 않습니다. 개인 열람·원문 링크 공유·검색 색인과 제한적인 미리보기는 허용합니다. 법령상 이용 예외, 기존 이용 허락, 코드·제삼자 자료의 기존 라이선스는 유지합니다. 콘텐츠 이용 안내