next/image - Error handling
회사 코드를 리팩토링하던 중 이미지 렌더링 문제를 발견했습니다.
null 체크만으로는 부족했다
문제가 된 코드는 이렇게 생겼습니다.
<Image
alt={"이미지"}
src={imgUrl ? imgUrl : DEFAULT_IMG_URL}
width={width}
height={height}
/>
API 응답으로 받은 imgUrl이 null이나 undefined이면 기본 이미지를 보여주고, 값이 있으면 그대로 렌더링하는 구조입니다. 얼핏 보면 충분해 보이지만, 이 방식은 "경로가 존재하는가"만 체크할 뿐입니다.
실제로 문제가 된 상황은 달랐습니다. 서버에서 "admin/img1" 같은 경로를 정상적으로 전달받아도, 해당 경로에 파일이 없거나 이미지가 손상된 경우가 있었습니다. imgUrl이 null이 아니니 조건문을 통과하고, 브라우저는 그 URL로 실제 HTTP 요청을 보냅니다. 응답이 404이거나 파싱에 실패하면 깨진 이미지 아이콘이 그대로 화면에 남게 되죠.
null 체크로는 JavaScript 변수의 상태만 알 수 있었습니다. 실제 파일을 불러올 수 있는지는 네트워크 요청 이후에야 알 수 있습니다. 따라서 이미지 로드 실패를 별도로 처리해야 했습니다.
로드에 실패하면 state를 바꿔 기본 이미지를 보여준다
next/image 컴포넌트는 onError 이벤트 핸들러를 제공합니다. 이미지 로드에 실패했을 때 호출되는 콜백입니다. 여기서 state를 바꾸면 폴백 이미지를 보여줄 수 있습니다.
import { useState } from "react";
import Image from "next/image";
const DEFAULT_PROFILE_IMG_URL = "/images/default-profile.png";
interface ImageProps {
alt: string;
src: string;
width: number;
height: number;
}
export default function ImageWithFallback({
alt,
src,
height,
width,
}: ImageProps) {
const [imgSrc, setImgSrc] = useState<string>(src);
return (
<Image
alt={alt}
src={imgSrc}
width={width}
height={height}
onError={() => setImgSrc(DEFAULT_PROFILE_IMG_URL)}
/>
);
}
동작 흐름은 단순합니다. imgSrc state를 props로 받은 src로 초기화한 뒤, 이미지 로드에 실패하면 onError 핸들러가 imgSrc를 기본 이미지 경로로 교체합니다. state가 바뀌면서 컴포넌트가 리렌더링되고, 이번엔 안전한 경로의 이미지가 표시되죠.
이제 두 가지 상황에 모두 대응할 수 있습니다. imgUrl이 null인 경우는 부모에서 기본 경로를 내려주면 되고, 경로는 있지만 실제 파일에 문제가 있는 경우는 onError가 자동으로 폴백 처리해줍니다.
폴백 이미지도 깨질 수 있다
폴백 이미지도 로드에 실패할 수 있습니다. 그렇게 되면 onError가 다시 호출되고, 또 onError가 호출되는 무한 루프가 생길 수 있죠. 간단하게는 현재 imgSrc가 이미 기본 이미지 경로인지 확인해서 핸들러를 일찍 종료하는 방식으로 막을 수 있습니다.
onError={() => {
if (imgSrc !== DEFAULT_PROFILE_IMG_URL) {
setImgSrc(DEFAULT_PROFILE_IMG_URL);
}
}}
작은 처리지만, 프로덕션에서는 챙겨두는 편이 안전할 것 같습니다.