[Next.js] 동적으로 이미지 Placeholder 구현하기

yii·2025년 4월 30일
post-thumbnail

웹에서 이미지는 사용자 경험에 매우 큰 영향을 미칩니다. Next.js는 이미지 최적화를 위한 <Image> 컴포넌트를 제공하지만, 기본 설정만으로는 사용자가 이미지가 로드되기 전까지 하얀 공간을 보게 되는 문제가 발생할 수 있습니다. 이를 해결하기 위해 흔히 로딩 중인 이미지의 흐릿한 버전(blurred preview)을 먼저 보여주는 방식이 사용되며, Next.js는 이를 위해 placeholder="blur" 속성과 함께 blurDataURL을 지원합니다. 하지만 동적으로 로딩되는 이미지나 외부 이미지의 경우, 이 기능을 적용하기 위해 추가적인 작업이 필요합니다.

기존의 이미지 처리 방식

const ProductCard = ({
  id,
  image,
  name,
  description,
  price,
  link,
}: ProductCardProps) => {
  const [isMounted, setIsMounted] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  if (!isMounted) return null;

  const handleImageLoad = () => {
    setIsLoading(false);
  };
  return (
    <Link href={/products/${id}} key={id}>
      <div className="flex h-[300px] flex-col">
        <div className="group relative h-[200px] w-[200px] overflow-hidden rounded-lg shadow-md">
          {isLoading && (
            <div
              className="absolute inset-0 rounded-lg"
              style={{
                background: 'linear-gradient(90deg, #f3f4f6, #e5e7eb, #f3f4f6)',
                backgroundSize: '200% 200%',
                animation: 'gradient 3s ease infinite',
              }}
            />
          )}
          <Image
            src={image}
            alt="상품 예시 이미지"
            width={200}
            height={200}
            onLoad={handleImageLoad} // 이미지가 로드되었을 때 호출
            className="h-full w-full object-fill transition-transform duration-300 ease-in-out group-hover:scale-110"
          />
        </div>

isMounted: 컴포넌트가 클라이언트에서 마운트되었는지 확인
isLoading: 이미지 로딩 상태 추적

위 두 상태로 이미지 로딩 상태를 관리하고, onLoad 이벤트를 통해 이벤트 로딩 완료를 감지합니다. 그 전까지는 이미지 로딩 UI를 보여주는 방법으로 구현했습니다.
하지만 이 방법은 추가적인 상태 관리가 필요하며 코드의 복잡성이 증가한다는 단점이 있습니다. 또 위 방법(클라이언트 마운트 후에만 렌더링, 즉 useEffect로 isMounted 상태를 관리하고, 마운트 전에는 null 반환)은 hydration 에러를 우회하는 데에는 일시적으로 효과가 있지만, 근본적인 문제를 해결하지 못하고 새로운 문제를 만들 수 있습니다.
또 서버에서는 아무것도 렌더링하지 않으므로, 서버와 클라이언트가 항상 다르게 렌더링되는 상황을 만듭니다.

Image 컴포넌트의 placeholder 속성

placeholder = 'empty' // "empty" | "blur" | "data:image/..."

Next.js에서는 이미지 최적화를 위해 Image 컴포넌트를 제공합니다. placeholder 속성에 "blur"라는 값을 넣어주면, 정적 파일의 경우 자동으로 흐릿하게 placeholder을 넣어줍니다. 이미지 로딩 중 흐릿하게 볼 수 있습니다.

이때 "blur"옵션을 쓸 때 외부 도메인이나 public에서 동적으로 이미지를 불러오는 경우라면 blurDataURL을 필수적으로 작성해야 합니다. blurDataURL은 블러 이미지의 주소로 생각하면 된다. base64를 기반으로 인코딩된 최대 10픽셀짜리 이미지여야 합니다.

이미지 미리보기 블러 라이브러리 Plaiceholder

Plaiceholder라는 라이브러리를 사용하면 동적 이미지를 흐릿한 base64 이미지로 인코딩하여 보다 로딩 화면을 예쁘게 구현할 수 있습니다!

해결 및 개선

export const getServerSideProps = async () => {
  try {
    const products = await fetchProducts();
    const productsWithBlur = await Promise.all(
      products.map(async product => {
        // 파일 시스템 절대 경로 생성
        const imagePath = path.join(process.cwd(), 'public', product.image);
        // 이미지 파일 읽기
        const buffer = await fs.readFile(imagePath);
        const { base64 } = await getPlaiceholder(buffer);
        return { ...product, blurDataURL: base64 };
      }),
    );

    return { props: { products: productsWithBlur } };
  } catch (error) {
    console.error('상품 정보 불러오기에 실패하였습니다:', error);
    return {
      props: { products: [] },
    };
  }
};

이 코드는 getServerSideProps 안에서 모든 상품 이미지에 대한 blurDataURL을 사전 생성하여 클라이언트에 전달합니다. 클라이언트에서는 로딩 중에도 흐릿한 이미지가 먼저 표시되어 사용자 경험이 크게 향상됩니다.

이미지 파일을 Buffer(binary 데이터) 로 읽어온 뒤 plaiceholder 라이브러리를 사용해 이미지를 분석하고, blurDataURL용 base64 문자열을 생성합니다. 매우 저해상도 이미지이므로 크기가 작고, 초기 로딩시 placeholder로 사용하기 적합합니다.

이를 통해 초기 HTML에 포함되어 hydration mismatch 방지하고 이미지 로딩 전 흐릿한 배경으로 콘텐츠 이동 최소화하도록 개선하였습니다.

참고

https://velog.io/@stu442/Next.js-%EB%8F%99%EC%A0%81-Placeholder-%EA%B5%AC%ED%98%84
https://velog.io/@stu442/Next.js-%EB%8F%99%EC%A0%81-Placeholder-%EA%B5%AC%ED%98%84
https://haruisshort.tistory.com/302

profile
프론트엔드 개발자

0개의 댓글