-
Next.js - next/image blur 사용하기React.js & Next.js 2022. 10. 19. 22:47
이전 게시글과 이어집니다!
next/image 에서 제공하는 property인 placeholder=’blur’와 blurDataURL={ … }을 사용한다면, 블러를 통해 빈 이미지 공간을 이미지가 렌더링 될 때까지 효율적으로 제어할 수 있습니다.
<Image src={imgUrl} alt="Thumbnail of article" width="320" height="200" layout="responsive" placeholder="blur" blurDataURL={imgUrl} />
하지만 다음과 같이
src
프로퍼티와blurDataURL
에 동일한 경로의 이미지를 바로 넣는 경우, Next에 의해 처리된 이미지도 나오지만, blur를 위한 원본 이미지를 동시에 가져오는 이슈가 있었습니다.Next Image를 사용하는 이유는 Next에 의해 자동으로 압축되는 이미지 용량을 사용하기 위해서인데, blur로 인해 원본 이미지를 그대로 불러오고 있어서 고민이 됐습니다.
문서를 찾아보니, blurDataURL에 들어갈 이미지 파일은 원본 이미지 경로를 그대로 넣는 것이 아닌, base64로 처리된 이미지를 넣어줘야 한다는 것을 발견했습니다.
만약 우리가 Next Image에서 제공하는 blur 기능을 사용하고 싶다면, 원본 이미지 경로를 그대로 넣는 것이 아닌 base64로 처리된 이미지를 프로퍼티에 넣어줘야 합니다.
placeholder
A placeholder to use while the image is loading. Possible values are blur or empty. Defaults to empty.
When
blur
, the[blurDataURL](https://nextjs.org/docs/api-reference/next/image#blurdataurl)
property will be used as the placeholder. Ifsrc
is an object from a static import and the imported image is.jpg
,.png
,.webp
, or.avif
, thenblurDataURL
will be automatically populated.For dynamic images, you must provide the
[blurDataURL](https://nextjs.org/docs/api-reference/next/image#blurdataurl)
property. Solutions such as Plaiceholder can help withbase64
generation.When
empty
, there will be no placeholder while the image is loading, only empty space.Try it out:
Next.js 에서는 공식 문서에서 Plaiceholder 라는 라이브러리를 사용하여, base64를 만들 수 있다는 정보를 제공하고 있으므로 해당 라이브러리를 바탕으로 base64로 변환된 이미지를 만들기 위한 로직을 구현해야 합니다.
Prerequisites
Plaiceholder 를 사용하기 위해서는 먼저 두 라이브러리를 설치해야 합니다
$ yarn add sharp
$ yarn add plaiceholder
사용법
getPlaiceholder(src, options);
src
: string reference to an image. Can be either;- a file path to an image inside the root
public
directory, referenced from the base URL (/
). - a remote image URL.
- a file path to an image inside the root
options
: (optional)dir
: a file path to your preferred static assets directory; where local images are resolved from (default:./public
)size
: an integer (between4
and64
) to adjust the returned placeholder size (default:4
)- Sharp ConfigurationNOTE
brightness
: brightness multiplier (default:1
)format
: force output to a specified output (default:["png"]
)hue
: degrees for hue rotation (no default)lightness
: lightness addend (no default)removeAlpha
: remove alpha channel for transparent images (default:true
)- Note: this option is a no-op for Blurhash.
saturation
: saturation multiplier (default:1.2
)
- Plaiceholder has no plans to expand these options. If you need more control on the ouput, we recommend rolling your own Sharp-based LQIP solution.
- Under-the-hood, plaiceholder uses Sharp to transform images; a small selection of options have been exposed for further customization:
TL DR;
- src 에는 변환할 이미지의 path를 넣어줘야 합니다
- option을 통해 size를 지정할 수 있는데 기본 값은 4px입다
Example
기본적으로 예제 코드를 제공하지만, 단일 이미지를 정적인 asset 으로 가지고 있을 때를 가정하고 있기 때문에 큰 도움이 되지는 않았다. 우리는 pre-render를 위해 데이터 및 경로(path)로 가지고 있는 데이터를 base64로 변환해야 하기 때문이다.
Structure
📦 ├─ .eslintrc.json ├─ .gitignore ├─ .prettierrc ├─ README.md ├─ next.config.js ├─ package.json ├─ public │ ├─ favicon.ico │ └─ vercel.svg ├─ src │ ├─ components │ │ └─ Movie │ │ ├─ index.styles.tsx │ │ └─ index.tsx │ ├─ config │ │ └─ index.tsx │ ├─ hooks │ │ ├─ index.tsx │ │ └─ movie │ │ └─ index.tsx │ ├─ pages │ │ ├─ _app.tsx │ │ ├─ _document.tsx │ │ ├─ _error.tsx │ │ ├─ index.tsx │ │ ├─ movie │ │ │ ├─ [name].tsx │ │ │ ├─ index.tsx │ │ │ └─ spiderman │ │ │ └─ index.tsx │ ├─ styles │ │ └─ GlobalStyle.tsx │ └─ types │ ├─ index.ts │ └─ movie │ └─ index.ts ├─ tsconfig.json └─ yarn.lock
Code
먼저 getStaticProps 로 정적인 페이지를 만들 때의 상황을 가정합니다.
- spiderman 이라고 검색한 결과를 서버에 저장하고, 정적인 페이지로 가지고 싶을 때
// 🗂/pages/movie/spiderman.tsx export async function getStaticProps() { const res = await fetch( `https://api.themoviedb.org/3/search/movie?api_key=${REACT_APP_API_KEY}&language=en-US&query=spiderman` ); const errorCode = res.ok ? false : res.status; if (errorCode) { return { props: { errorCode } }; } const data: ImovieData = await res.json(); const posterWithBlurURL = await Promise.all( data.results.map(async (movie: ImovieResults) => { const { base64 } = await getPlaiceholder( movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : 'https://freesvg.org/img/1645699345cat.png' ); return { ...movie, blurDataURL: base64 }; }) ); return { props: { errorCode, data, blurData: posterWithBlurURL } }; }
// 🗂/types/movie export interface ImovieResults { adult: boolean; backdrop_path: string; genre_ids: string[]; id: number; original_language: string; original_title: string; overview: string; popularity: number; poster_path: string; release_date: string; title: string; video: boolean; vote_average: number; vote_count: number; } export interface blurResults extends ImovieResults { blurDataURL: string; } export interface ImovieData { page: number; results: ImovieResults[]; total_pages: number; total_results: number; }
살펴봐야 하는 부분은
posterWithBlurURL
변수이다.const posterWithBlurURL = await Promise.all( data.results.map(async (movie: ImovieResults) => { const { base64 } = await getPlaiceholder( movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : 'https://freesvg.org/img/1645699345cat.png' ); return { ...movie, blurDataURL: base64 }; }) );
fetch 함수를 통해 결과 값을 res 변수에 저장하고, 해당 값이 유효할 때 error를 바탕으로 반환하는 것이 아닌, .json() 메서드를 사용하여 직렬화한다.
ImovieData 인터페이스를 보면 알 수 있듯이 res 변수 내부의 results 라는 배열 객체를 순회하며 해당 프로퍼티(movie.poster_path)에 대한 값을 만든다.
반환(return)할 때, 우리가 getPlaiceholder 메서드를 통해 만든 base64 변환 이미지를 기존 movie 객체를 spread 연산자로 풀어주고, blurDataURL의 값으로 추가한다.
movie.poster_path 가 없는 경우도 있으므로, 해당 경우에는 빈 파일, 빈 이미지를 base64로 변환하는 것이 아닌, 다른 예시 이미지를 base64 변환 이미지로 갖고 있도록 처리해야 한다.
movie.poster_path 값이 비어 있을 경우, base64로 변환할 이미지가 없으면 에러를 발생시킨다
이후 getStaticProps 를 최종적으로 반환할 때, posterWithBlurURL을 리턴 값에 포함시킨다.
return { props: { errorCode, data, blurData: posterWithBlurURL } };
해당 파일에 대한 전체 흐름을 본다면 다음과 같다.
... interface IMoviePosts { errorCode: number | boolean; data: ImovieData; blurData: blurResults[]; } const MoviePosts = ({ errorCode, data, blurData }: IMoviePosts) => { if (errorCode !== false) { return <Error errorCode={errorCode} />; } if (data.results.length === 0) return <div>There is no data...</div>; return ( <> <Head> <title>Movie | Spiderman</title> <meta name="description" content="Movie | Spiderman"></meta> </Head> <MoviePost blurData={blurData} /> </> ); }; export default MoviePosts; export async function getStaticProps() { const res = await fetch( `https://api.themoviedb.org/3/search/movie?api_key=${REACT_APP_API_KEY}&language=en-US&query=spiderman` ); const errorCode = res.ok ? false : res.status; if (errorCode) { console.log('- errorCode:', errorCode); return { props: { errorCode } }; } const data: ImovieData = await res.json(); const posterWithBlurURL = await Promise.all( data.results.map(async (movie: ImovieResults) => { const { base64 } = await getPlaiceholder( movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : 'https://freesvg.org/img/1645699345cat.png' ); return { ...movie, blurDataURL: base64 }; }) ); return { props: { errorCode, data, blurData: posterWithBlurURL } }; }
// View를 위한 MoviePost 컴포넌트로 분리 interface IMoviePost { blurData: blurResults[]; } const MoviePost = ({ blurData }: IMoviePost) => { return ( <S.MoviePosts> <h1>Hello Movie Spiderman</h1> <button onClick={() => (location.href = '/movie')}>Back</button> <div className="movie-wrap"> {blurData?.map((movie) => ( <div className="movie" key={movie.id}> <div className="image-wrap"> <Image layout="fill" src={ movie.backdrop_path ? `https://image.tmdb.org/t/p/original/${movie.backdrop_path}` : `https://freesvg.org/img/1645699345cat.png` } alt={movie.title} placeholder="blur" blurDataURL={movie.blurDataURL} /> <span>{movie.title}</span> </div> </div> ))} </div> </S.MoviePosts> ); }; export default MoviePost;
컴포넌트로 뷰 로직을 분리하는 이유는 Next.js에서 styled-components를 pages 폴더에 구현할 수 없기 때문이다.
결과 확인
네트워크 탭 확인
data:xxx 로 처리된 부분
실제 요청 URL
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGPIrOzOqWhbuHYvw7yNBxK9w/7/+s+g7xSiJaG+eeNBBgY2UUev6LKyZgByfxCvAnvQuAAAAABJRU5ErkJggg==
'React.js & Next.js' 카테고리의 다른 글
예제로 배우는 react context (0) 2022.12.18 Next.js - 다양한 SNS 플랫폼에 대응할 수 있는 SEO 도입하기 (0) 2022.10.21 Next.js - middleware 사용하기 (로그인 연동하기) (0) 2022.09.25 Next.js - middleware 사용하기 (기본편) (0) 2022.09.25 Next.js - 스타일드 컴포넌트에 SSR 적용하기 (0) 2022.09.25