logo

Blog

Next Image blur 전환 애니메이션 넣기

#nextjs#image placeholder#image blur#blog
ryxxn profileryxxn
2025.01.07
thumbnail

1. 이미지 블러 효과 적용

기존에 이미지가 로드되는 동안 깜빡임이 있어
좀 더 자연스러운 이미지 로드를 위해 Next Image에서 지원하는 blur 기능을 적용했다.

우선 blur를 적용하려면 이미지의 base64 소스를 얻어야 한다.
이미지를 base64 변환하기 위해
Next 공식문서에서도 권장하는대로 plaiceholder 라이브러리를 다운받았다.

이후

typescript
import fs from 'fs/promises';import path from 'path';import { getPlaiceholder } from 'plaiceholder';
export const getImage = async (src: string) => {  const buffer = await fs.readFile(path.join('./public', src));
  const {    metadata: { height, width },    ...plaiceholder  } = await getPlaiceholder(buffer, { size: 10 });
  return {    ...plaiceholder,    img: { src, height, width },  };};

이후

tsx
// ...
<Image  width={130}  height={90}  alt={title}  className="..."  src={thumbnail}  placeholder="blur"  blurDataURL={thumbnailBase64}/>
// ...

이렇게 추가해주었다.

결과는?

before add animation

그런데 뭔가 뚝뚝 끊기는 느낌이 났다;;
빈 화면보다는 낫지만, 애니메이션을 주기로 했다.


2. blur 이미지에 애니메이션 주기

우선.. 로드가 됐다는 것을 확인하려면 클라이언트측에서 확인해야 하므로
useState로 로드 상태를 관리하고
data-loaded 속성으로 transition을 주었다.

tsx
'use client';
import Image, { ImageProps } from 'next/image';import React from 'react';
const BlurImage = (props: ImageProps) => {  const [isLoaded, setIsLoaded] = React.useState(false);
  return (    <Image      {...props}      data-loaded={String(isLoaded)}      onLoad={() => setIsLoaded(true)}    />  );};
export default BlurImage;
css
img[data-loaded='false'] {  /* 초기 블러 효과 */  transform: scale(1.2);  filter: blur(10px);}
img[data-loaded='true'] {  /* 블러 제거 */  transform: scale(1);  filter: blur(0);  transition: filter 0.3s, transform 0.3s;}

결과는?

after add animation

그냥 blur만 했을 때보다는 훨씬 부드럽다.

이렇게 기분 좋게 다른 탭으로 전환을 했는데
문제가 생겼다.

useState로 했기 때문에 화면 전환마다 애니메이션이 동작했다.

blur animation problem

이미 로드가 완료된 이미지는 굳이 blur 애니메이션이 동작할 필요가 없기 때문에
처리를 하기로 했다.


3. 이미 로드된 이미지는 애니메이션 빼기

이미지를 한 번 가져오면 브라우저단 캐싱이 만료되기 전까지는 다시 로드하지 않으니깐,
이미지를 로드한 적이 있는지 판별하는 로직을 구현해야 했다.

몇몇 이미지는 지속적으로 최적화를 하여 수정하고 있었기 때문에
cache-controlmax-age는 건드리지 않기로 했다.

그래서 브라우저의 이미지 캐싱 상태를 불러오고 싶었으나,
아쉽게도 판별할 방법이 없었다.

그래서 생각한 방법은
로드된 이미지 주소를 현재 브라우저 인메모리에서 관리하는 것이었다.

tsx
// ...
export const useImageLoad = (): ImageLoadContextType => {  const context = useContext(ImageLoadContext);  if (!context) {    throw new Error('useImageLoad must be used within an ImageLoadProvider');  }  return context;};
export const ImageLoadProvider: React.FC<React.PropsWithChildren> = ({  children,}) => {  const [loadedImages, setLoadedImages] = React.useState<Record<string, boolean>>({});
  // 로드된 이미지 주소 저장  const markAsLoaded = (src: string) => {    setLoadedImages((prev) => ({ ...prev, [src]: true }));  };
  return (    <ImageLoadContext.Provider value={{ loadedImages, markAsLoaded }}>      {children}    </ImageLoadContext.Provider>  );};

이렇게 Context API를 사용해서 BlurImage 컴포넌트에서 사용했다.

tsx
'use client';
import NextImage, { ImageProps } from 'next/image';import { useImageLoad } from './image-loaded-provider';
const BlurImage = (props: ImageProps) => {  const { loadedImages, markAsLoaded } = useImageLoad();
  const isLoaded = loadedImages[props.src as string] || false;
  return (    <NextImage      {...props}      data-loaded={String(isLoaded)}      onLoad={() => markAsLoaded(props.src as string)}    />  );};
export default BlurImage;

결과

solved result

이렇게까지 이미지 로드 애니메이션을 넣어야 할까 싶었지만..
기존 blur에서의 끊기는 전환보다는 훨씬 자연스러웠기에
이 정도 트레이드 오프는 충분히 감수할 만 하다고 생각했다.

Related Articles