데굴데굴

[Next.js] Next.js 13 <Image> plaiceholder 라이브러리 적용기 본문

Programming

[Next.js] Next.js 13 <Image> plaiceholder 라이브러리 적용기

aemaaeng 2023. 9. 21. 11:24

Next.js 13으로 프로젝트를 하던 중 이미지가 로딩될 때 layout shift가 생기는 걸 발견했다.

Network - Fast 3G 옵션으로 테스트한 화면

layout shift의 원인

Next.js에서 제공하는 <Image> 컴포넌트를 사용하려면 무조건 너비와 높이를 지정해서 넘겨줘야 한다.
현재 내 프로젝트에서는 이미지를 동적으로 불러오고 있어 그 크기를 미리 알 수 없었기 때문에 아래처럼 써놓은 상태였다.

    <Image
        src={src}
        alt={src}
        width={0}
        height={0}
        sizes="65vw"
        style={{ height: "auto" }}
    />

코드 참고

 

How to set the next/image component to 100% height

I have a Next.js app, and I need an image that fills the full height of its container while automatically deciding its width based on its aspect ratio. I have tried the following: <Image src...

stackoverflow.com

<Image> 컴포넌트는 width, height값으로 이미지 크기를 예상하여 렌더링한다. 위 코드에서는 두 값이 전부 0이기 때문에 실제 이미지가 로딩될 때 밀림 현상이 생기는 건 당연한 것이었다.

 

이런 밀림 현상도 그렇고 이미지가 로딩될 때 빈 화면이 보이고 있는데 사진이 있다는 표시로 블러 이미지가 미리 표시되면 좋을 것 같아 방법을 찾아보게 되었다.

placeholder

Next.js의 <Image> 컴포넌트는 이미지가 로딩되기 전에 보여줄 placeholder 속성을 기본으로 제공한다.
기본값은 "empty"이다.

"blur"옵션을 쓸 때에는 blurDataURL이 필요하다. blurDataURL은 블러 이미지의 주소로 생각하면 된다. base64를 기반으로 인코딩된 최대 10픽셀짜리 이미지여야 한다.

이미지를 정적으로 불러오는 경우 blurDataURL이 자동으로 생성되기 때문에 placeholder="blur"만 해줘도 알아서 잘 동작한다.
아래는 공식 문서에 링크된 예제 코드이다. github 바로가기

import Image from 'next/image'
import ViewSource from '../components/view-source'
import mountains from '../public/mountains.jpg'

const PlaceholderBlur = () => (
  <div>
    <ViewSource pathname="pages/placeholder.tsx" />
    <h1>Image Component With Placeholder Blur</h1>
    <Image
      alt="Mountains"
      src={mountains}
      placeholder="blur"
      width={700}
      height={475}
      style={{
        maxWidth: '100%',
        height: 'auto',
      }}
    />
  </div>
)

export default PlaceholderBlur

이미지를 외부 도메인이나 public 폴더에서 동적으로 불러오는 경우에는 blurDataURL필수적으로 작성해줘야 하는데 이 때 공식 문서에서 제안하는 것이 plaiceholder 라이브러리이다.

For dynamic images, you must provide the blurDataURL property. Solutions such as Plaiceholder can help with base64 generation.

plaiceholder 라이브러리

이미지 placeholder를 만들어주는 라이브러리이다. 부가적으로 이미지의 메타데이터 정보도 받아 활용할 수 있다.

공식 문서 바로가기

설치

sharp 라이브러리를 기반으로 동작하기 때문에 설치되어 있지 않다면 같이 설치해야 한다.

npm install sharp
npm install plaiceholder

Next.js 설정

Next.js에서 plaiceholder를 쓰려면 몇 가지 단계를 더 거쳐야 한다. 공식 문서
우선, 추가적으로 @plaiceholder/next 패키지를 설치한다.

npm install @plaiceholder/next

이제 config 파일을 변경해야 하는데 이 때 파일의 확장자는 무조건 .mjs 혹은 .ts여야 한다.
기존 config 파일에 require문이 있다면 import로 바꿔주고 export하는 nextConfig 변수를 withPlaiceholder로 감싸준다.

나는 아래처럼 작성했다.

// next.config.mjs
import withPlaiceholder from "@plaiceholder/next";
import withImages from "next-images";
/** @type {import('next').NextConfig} */

const nextConfig = withImages({
  experimental: {
    appDir: true,
  },
  images: {
    formats: ["image/avif", "image/webp"],
  },
  swcMinify: true,
});

export default withPlaiceholder(nextConfig);

적용

기본 사용법

getPlaiceholder(input, options)

input: 가공되지 않은 Buffer 이미지 주소 [필수]

프로젝트에 적용한 코드

공식 문서에 예제 코드가 잘 나와있다.
나는 아래 링크를 가장 많이 참고했다.

 

Upgrading to 3.0 | Plaiceholder

Beautiful image placeholders, without the hassle.

plaiceholder.co

// utils/getBase64.ts
import fs from "node:fs/promises";
import path from "node:path";
import { getPlaiceholder } from "plaiceholder";

const getBase64 = 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 },
  };
};

export default getBase64;

node.js의 fs 모듈을 이용해 public 폴더의 이미지를 읽어와 Buffer를 생성한다.
10픽셀의 base64URL을 만들 것이기 때문에 options에는 {size: 10}을 전달했다.

이 함수에서는 base64URL에 더불어 이미지의 높이, 너비 정보도 함께 리턴하고 있기 때문에 이 정보를 <Image> 컴포넌트에 적용하면 layout shift 문제도 함께 해결할 수 있다.

// components/ImgWithPlaceholder.tsx
import getBase64 from "@/utils/getBase64";
import Image from "next/image";

async function ImgWithPlaceholder({ src }: { src: string }) {
  const { base64, img } = await getBase64(src);

  return (
    <Image
      src={src}
      alt={src}
      width={img.width}
      height={img.height}
      sizes="65vw"
      style={{ height: "auto" }}
      placeholder="blur"
      blurDataURL={base64}
    />
  );
}

export default ImgWithPlaceholder;

이렇게 만든 컴포넌트는 아래처럼 사용할 수 있다.

<ImgWithPlaceholder src={`/media/${data.message}`} />

결과 ✨

Network - Fast 3G 옵션으로 테스트한 화면

더 이상 밀림 현상도 발생하지 않고 이미지가 완전히 로딩되기 전 블러 이미지가 잘 보이는 것을 확인할 수 있다.

 

Lighthouse 결과에서 보였던 Cumulative Layout Shift도 사라졌다. 

전 / 후

주의할 점

plaiceholder브라우저에서는 동작하지 않는다.

Plaiceholder is a server-side library. It will not work in the browser.

따라서 Next.js 13에서 plaiceholder 라이브러리를 쓰려면 서버 컴포넌트에서 써야 한다.

내가 겪은 시행착오

내 프로젝트에서 plaiceholder 라이브러리를 적용하려 했던 컴포넌트의 상위 컴포넌트가 전부 클라이언트 컴포넌트여서 코드 작성 후에 아래 오류가 뜨면서 앱 실행이 안 됐다.

UnhandledSchemeError: Reading from "node:fs/promises" is not handled by plugins (Unhandled scheme).

 

이렇게 라이브러리를 적용 못하는건가 싶었는데 아래 기준에 따라 코드를 다시 한 번 살펴보니 상위 컴포넌트는 전부 클라이언트 컴포넌트로 굳이 쓸 필요가 없는 것들이었다.

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#when-to-use-server-and-client-components

클라이언트 컴포넌트로 써야 했던 이유는 프로젝트에서 CSS-in-JS 라이브러리로 사용되던 styled-components 때문이었다.
styled-components 내부에서 React.createContext()가 쓰이기 때문에 클라이언트 컴포넌트로 작성하지 않으면 오류가 발생한다. 따라서 스타일링을 위해서 서버 컴포넌트로 쓸 수 있는 것도 클라이언트 컴포넌트로 써야 했다.


관련 글

 

Next 13 에서의 CSS-in-JS with. styled-components

최근에 Next의 메이저 버전이 업데이트 되면서 app 이라는 디렉토리가 생기며 많은 변화가 생겼습니다 그중에서 현재 회사에서 사용중인게 styled-components

velog.io

 

이렇게 되면 서버 쪽에서만 작동하는 라이브러리 적용도 불가능할뿐더러 Next.js의 이점인 서버 사이드 렌더링을 전혀 활용할 수가 없게 된다.

 

정말 해결할 수 있는 방법이 없나 찾아봤지만 아래 github 링크에 있는 논의를 보고 CSS 스타일링 방식을 바꿔야겠다는 결정을 내리게 됐다.

 

Styled-components + next.js + 'use client' · Issue #4025 · styled-components/styled-components

Hello. I don't understand. Maybe it's some kind of mistake? Why should I write 'use client' every time I use styled-components? I may understand that next.js does not support context or elements fr...

github.com

 

다양한 CSS-in-JS 라이브러리가 있었지만 Next.js에서 썼을 때 또 어떤 문제가 발생할지 모르겠어서 우선은 module.css로 옮겼다. 이유는 가장 가볍고 확실하기 때문이다. (작업하다보니 styled-components의 중첩에 너무 익숙해졌던 터라 순수 css가 조금 불편하게 느껴져서 SCSS를 같이 써보는 것도 생각해보고 있다.)

 

이 문제는 다행히 프로젝트 초기 단계에서 발생했어서 옮기는 작업이 비교적 수월했지만 훨씬 더 큰 프로젝트에서 혹은 프로젝트가 막바지에 다다른 상황에서 이런 문제를 만나면 어떻게 해야 할 지 생각해보게 됐다. 이런 문제 상황에서 현명한 의사결정을 내리는 것도 개발자의 역할이라는 걸 배웠다. 

참고

 

[nextjs] 최소한의 이미지 최적화하기

Nextjs에서의 Image 사용에 있어 최소한의 최적화 시도는 해봐야지 않겠나라는 마음가짐으로 공부하고 적용한 과정을 기록해나가고 있다.

velog.io

 

Comments