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


1. 이미지 블러 효과 적용
기존에 이미지가 로드되는 동안 깜빡임이 있어
좀 더 자연스러운 이미지 로드를 위해 Next Image
에서 지원하는 blur
기능을 적용했다.
우선 blur
를 적용하려면 이미지의 base64
소스를 얻어야 한다.
이미지를 base64
변환하기 위해
Next 공식문서에서도 권장하는대로 plaiceholder
라이브러리를 다운받았다.
이후
typescriptimport 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}/> // ...
이렇게 추가해주었다.
결과는?

그런데 뭔가 뚝뚝 끊기는 느낌이 났다;;
빈 화면보다는 낫지만, 애니메이션을 주기로 했다.
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;
cssimg[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;}
결과는?

그냥 blur만 했을 때보다는 훨씬 부드럽다.
이렇게 기분 좋게 다른 탭으로 전환을 했는데
문제가 생겼다.
useState
로 했기 때문에 화면 전환마다 애니메이션이 동작했다.

이미 로드가 완료된 이미지는 굳이 blur 애니메이션이 동작할 필요가 없기 때문에
처리를 하기로 했다.
3. 이미 로드된 이미지는 애니메이션 빼기
이미지를 한 번 가져오면 브라우저단 캐싱이 만료되기 전까지는 다시 로드하지 않으니깐,
이미지를 로드한 적이 있는지 판별하는 로직을 구현해야 했다.
몇몇 이미지는 지속적으로 최적화를 하여 수정하고 있었기 때문에
cache-control
의 max-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;
결과

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