next/image - Error handling
2023-07-03회사에서 리팩토링을 진행하던 중, 이미지 렌더링과 관련된 이슈를 발견했어요.
기존 코드의 한계
문제가 된 코드는 이렇게 생겼어요.
<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 변수 상태의 문제고, 후자는 네트워크 요청이 일어난 뒤에야 알 수 있는 문제거든요. 이 간극을 메우려면 브라우저가 이미지 로드에 실패했을 때를 별도로 처리해야 해요.
onError로 폴백 처리하기
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);
}
}}
작은 처리지만, 프로덕션에서는 챙겨두는 편이 안전해요.