ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Next.js - next/image blur 사용하기
    React.js & Next.js 2022. 10. 19. 22:47

    이전 게시글과 이어집니다!

     

     

    Next.js - SSG, getStaticProps

    Data Fetching: getStaticProps | Next.js Data Fetching: getStaticProps | Next.js Fetch data and generate static pages with `getStaticProps`. Learn more about this API for data fetching in Next.js. n..

    junheedot.tistory.com

     

    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 | Next.js

    Enable Image Optimization with the built-in Image component.

    nextjs.org

    만약 우리가 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. If src is an object from a static import and the imported image is .jpg, .png, .webp, or .avif, then blurDataURL 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 with base64 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;
      1. a file path to an image inside the root public directory, referenced from the base URL (/).
      2. a remote image URL.
    • options: (optional)
      • dir: a file path to your preferred static assets directory; where local images are resolved from (default: ./public)
      • size: an integer (between 4 and 64) 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==

    댓글

Designed by Tistory.