레시피 목록

next/image - Error handling

2023년 7월 3일 2분 읽기
Next.js

회사에서 리팩토링을 진행하던 중, 이미지 렌더링과 관련된 이슈를 발견했습니다.

null 체크만으로는 부족했다

문제가 된 코드는 이렇게 생겼습니다.

tsx
<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를 변경해 폴백 이미지로 교체할 수 있습니다.

tsx
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가 이미 기본 이미지 경로인지 확인해서 핸들러를 일찍 종료하는 방식으로 막을 수 있습니다.

tsx
onError={() => {
  if (imgSrc !== DEFAULT_PROFILE_IMG_URL) {
    setImgSrc(DEFAULT_PROFILE_IMG_URL);
  }
}}

작은 처리지만, 프로덕션에서는 챙겨두는 편이 안전할 것 같습니다.