[ 살펴보기 ] NextJS - Image component

[ 살펴보기 ] NextJS - Image component

·

10 min read

NextJS Image component는 img element로 render될 때 자동으로 img element에 srcset을 추가해주고 접속하는 사용자 device에 따라 최적화된 image를 제공할 수 있도록 도와준다.

우선 srcset 속성이 어떻게 동작하는지 살펴보자. Frontend 개발시 사용자에게 image를 제공할 때 고려해야 하는 사항은 많지만 그 중에서도 중요한 점 하나를 꼽으라면 어떤 식으로 다양한 크기의 device로 접속하는 user에게 적절한 image를 제공하느냐 일 것이다.

만약 큰 모니터를 위해 2k 사이즈의 이미지가 s3와 같은 저장소에 있다고 치자, 만약 유저가 2k 사이즈가 필요한 큰 모니터에서 web applicaiton을 사용한다면 필요한 사이즈의 이미지가 사용되었다고 할 수 있지만 만약 유저가 phone과 같이 작은 devide로 web application을 사용한다면 굳이 2k 사이즈의 이미지를 제공할 필요는 없을 것이다.

그렇기에 사용자 device에 맞는 image를 어떻게 제공할 것인가는 frontend 개발 시 고민해야 할 사항 중 하나다. 그리고 사용자 device에 가장 적절한 image를 제공하기 위해 사용할 수 있는 여러 방법이 있지만 대표적인 방법 중 하나가 img element의 srcset 속성을 사용하는 것이다.

NextJS Image component를 살펴보기 전 srcset을 통해 어떻게 사용자의 device에 맞게 image를 제공하는지 우선 살펴보자.

SRCSET

Img element에 srcset 속성을 사용하여 개발자가 아래 코드와 같이 width별 이미지를 선언해 두면 browser가 접속하는 user의 device에 맞게 적절한 image를 load하여 사용한다. 즉, 적절한 image의 선택권을 browser에 위임한다.

<img
  srcset="600.jpg 600w, 1000.jpg 1000w, 2000.jpg 2000w"
  src="1200.jpg"
  alt="Test"
/>

위의 코드에서 600w, 1000w, 2000w과 같이 숫자 뒤에 나오는 w는 각 image width에 대한 정보다. browser는 이 정보를 기반으로 사용자의 device에 따라 적절한 image를 load하여 사용한다. 여기서 src prop으로 설정한 image는 srcset으로 설정한 image를 사용할 수 없을 때 default로 사용되는 image다.

여기서 주의할 점은 browser가 사용자의 device에 따라 적절한 image를 선택할 때 단순 device의 물리적인 width로 선택을 하는 것이 아닌 기기의 resolution에 영향을 받으므로 테스트 할 때 의도했던 것과 다른 image가 load되어도 혼란에 빠지지 말자.

예를들어 위의 코드를 chrome 개발 도구를 열어 iPhone 12화면으로 실행시켜 본다면 1000.jpg가 load되어 사용된다. device width는 390이기에 600.jpg가 load되어야 할 거라고 생각할 수 있지만 iPhone 12의 실제 resolution은 2532 x 1170이다 ( 길이 x 넓이 ) 그렇기에 600.jpg가 아니라 1000.jpg 이미지가 load되어 사용된 것이다.

만약 browser가 이미지를 선택할 때 image width 정보가 아닌 사용자 device의 device pixel ratio를 기반으로 image를 선택하게 만들고 싶다면 w descriptor 대신 x descriptor를 사용한다.

<img
  srcset="600.jpg 1x, 1000.jpg 1.5x, 2000.jpg 2x"
  src="1200.jpg"
  alt="Test"
/>

참고로 device pixel ratio ( DPR ) 란 device의 physical resolution의 logical resolution의 ratio를 뜻한다. 즉, 위의 예제에서 보았듯이 device의 resolution이 device의 물리적인 크기보다 크다면 DPR이 1보다 크다는 이야기이다.

Website를 기준으로 예를 들자면 DPR ( Device Pixel Ratio )가 1인 device는 css의 1 pixel이 실제 device의 1 pixel과 일치한다. 하지만 DPR가 2인 device에서 css의 1x1 pixel은 실제 device에선 4 pixel ( 2x2 )로 표현된다. 즉, DRP가 2인 device에서 100x100 css pixels은 실제 device에서 200x200 pixels로 표현되는 것이다.

이렇듯 DPR이 높은 device는 image나 다른 Visual element를 보다 잘 표현할 수 있기에 높은 DPR을 가진 device에 보다 큰 이미지를 제공하고 싶다면 위의 예제와 같이 2x와 같이 x descriptor를 사용한다.

참고로 개발자 도구 console에서 window.devicePixelRatio method를 통해 현재 device의 DPR를 확인할 수 있다.

Size prop

srcset을 통해 responsive image를 제공할 때 고려해야 할 사항이 하나 더 있다. 이미지의 사이즈가 viewport에 따라 달라질 수도 있다는 사실이다.

예를들어 mobile에서는 image의 width가 화면 크기의 100%로 display하다가 table에서는 50% 그리고 large screen에서는 30%로 image width가 줄어드는 UI라면 여기에 대한 힌트 역시 browser에게 제공해야 한다. 왜냐하면 browser는 현재 device screen의 resolution 정보와 우리가 설정한 srcset 정보를 통해 가장 적절한 이미지를 선택할 뿐 해당 이미지가 실제로 UI에서 어느 정도의 width를 차지하는지는 모르기 때문이다.

만약 width가 화면의 30% 크기면 되는 image를 표현하는데 device screen resolution과 우리가 설정한 serset 기준으로만 이미지를 선정하면 불필요하게 큰 이미지를 사용할 수 있다. 그렇기에 Image가 viewport마다 width가 변한다면 breakpoint에 따라 image의 width가 어떻게 변하는지에 대한 힌트 역시 추가로 제공해줘야 한다.

<img
  className="w-[50vw] md:w-[30vw] lg:w-[20vw]"
  srcSet="/image/600.jpg 600w, /image/1000.jpg 1000w, /image/2000.jpg 2000w"
  sizes="(min-width: 1024px) 20vw, (min-width: 768px) 30vw, 50vw"
  src="1200.jpg"
  alt="Test"
/>

위의 예제에서 img width를 tailwindcss로 설정한 간단한 예제이다. 해당 img의 width는 default로 50vw, viewport가 lg (1024)보다 크면 30vw, xl ( 1280 )보다 크면 20vw이 적용된다.

그리고 sizes prop을 통해 default image width는 50vw, viewport가 768px이상일 때는 30vw, 1024px이상일 때는 20vw라고 추가로 힌트를 주고 있다.

위의 예제를 다시 개발자 도구를 통해 iPhone 12화면으로 load해보면 이번에는 1000.jpg가 아닌 600.jpg를 load하는 것을 볼 수 있다. sizes prop을 통해 768px 이하 화면에서 image width는 50vw를 차지한다고 추가 힌트를 주고 있으므로 browser가 1000.jpg보다 600.jpg 이미지가 적절하다고 판단한 것이다.

이제 NextJS에서 제공하는 Image compoennt를 살펴볼 기본 준비가 되었다. 본격적으로 NextJS Image component를 살펴보자.

NextJS Image Component

테스트를 위해 AWS s3 bucket에서 2k.jpg라는 이미지를 NestJS Image component를 통해 load한다고 가정해보자. 만약 local image가 아닌 remote origin에서 image를 load한다면 next.config.js에 다음과 같이 image를 load하는 remore origin의 정보를 추가해주어야 한다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "...s3...amazonaws.com",
        port: "",
        pathname: "/**",
      },
    ],
  }
};

export default nextConfig;
import Image from "next/image";

...
return (
    <Image
      width={500}
      height={500}
      style={{ objectFit: "cover" }}
      src="/image/2k.jpg"
      alt="Test"
    />
)
...

위의 예제에서 볼 수 있듯이 remote origin에서 image를 load할 땐 기본적으로 width와 height값을 지정해준다. Image component의 width, height property는 image의 width, height를 설정할 뿐만 아니라 image 로딩 전후에 발생할 수 있는 layout shift를 방지하기 위해도 사용된다.

여기서 주의할 점은 만약 tailwindcss와 함께 사용하고 있다면 위에서 설정한 width와 height이 최종 render되는 실제 img element의 width와 height와 일치하지 않을 수 있다. Tailwindcss와 함께 사용할 시 img element에 max-width:100%가 추가되고 height가 auto로 override되기에 Image component에 전달한 500이란 수치만큼 height가 적용되지 않을 수 있고 width역시 max-width:100%의 영향으로 viewport가 500보다 작아질 때는 width를 500으로 유지하지 않고 viewport의 max width에 맞추어 줄어든다.

만약 height는 직접 설정해주고 싶다면 tailwindcss class utility를 통해 height를 설정해준다.

...
return (
    <Image
      className="h-[200px] object-cover"
      width={500}
      height={200}
      src="/image/2k.jpg"
      alt="Test"
    />
)

앞서 언급했듯이 Image component의 height prop은 image의 height 뿐만 아니라 layout shift 방지를 위해서도 사용되니 tailwind를 쓰더라도 함께 선언해주는 것이 좋다.

Image component가 실제 render된 img element를 개발자 도구를 통해 확인해보자.

<img 
    ...
    alt="Test" 
    width="500" 
    height="500" 
    style="color:transparent;object-fit:cover" 
    srcset="/_next/image?url=%2Fimage%2F2k.jpg&amp;w=640&amp;q=75 1x, 
            /_next/image?url=%2Fimage%2F2k.jpg&amp;w=1080&amp;q=75 2x" 
    src="/_next/image?url=%2Fimage%2F2k.jpg&amp;w=1080&amp;q=75"
>

개발자 도구에서 img element를 확인해보면 NextJS Image component가 render될 때 srcset 속성을 생성해 자동으로 image element에 적용시켜 주는 것을 확인할 수 있다.

NextJS default로 .next/cache/images 폴더에 cache image를 보관한다. 물론 이미지를 미리 만들어 놓지는 않고 유저가 image를 사용하는 page에 접근했을 때 위의 srcset list 중 유저가 접속한 device 기준 적절한 image가 존재한다면 해당 image를 제공하고 그렇지 않으면 .next/cache/images 폴더에 image를 새로 생성하여 제공한다.

만약 Image width, height를 parent element에 맞추고 싶다면 fill property를 true로 설정해준다.

...
return (
    <div className="relative w-[300px] h-[300px]">
        <Image
          fill
          className="object-cover"
          src="/image/2k.jpg"
          alt="Test"
        />
    </div>
)

다만 fill을 통해 image를 parent 크기에 맞출 때 parent element는 relative position이여야 한다. 또한 Image component에 fill이 사용되었을 때 srcset은 아래와 같이 default로 설정된 모든 deviceSizes에 맞게 생성되어 적용된다. ( default deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840] )

<img 
  ...
  srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=640&amp;q=75 640w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=750&amp;q=75 750w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=828&amp;q=75 828w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1080&amp;q=75 1080w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1200&amp;q=75 1200w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1920&amp;q=75 1920w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=2048&amp;q=75 2048w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75 3840w" 
  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75">

Image component에 sizes prop을 제공하는 방법은 일반 img element와 동일하다.

...
<Image
  fill
  className="object-cover"
  src="/image/2k.jpg"
  sizes="(min-width: 1024px) 30vw, (min-width: 768px) 50vw, 100vw"
  alt="Test"
/>
...

다만 sizes prop이 제공된 Image component는 sizes에 설정된 값에 따라서 img element srcset list에 deviceSizes의 width 뿐만 아니라 imageSizes의 일부 width가 추가될 수도 있다. ( default imageSizes : [16, 32, 48, 64, 96, 128, 256, 384] )

<img 
  ...
  sizes="(min-width: 1024px) 30vw, (min-width: 768px) 50vw, 100vw" 
  srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=256&amp;q=75 256w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=384&amp;q=75 384w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=640&amp;q=75 640w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=750&amp;q=75 750w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=828&amp;q=75 828w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1080&amp;q=75 1080w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1200&amp;q=75 1200w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1920&amp;q=75 1920w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=2048&amp;q=75 2048w, 
          /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75 3840w" 
  src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75">

위의 예제를 살펴보면 sizes prop을 포함하는 Image component를 render하면 srcset에 기존엔 없었던 256w, 384w에 대한 image 정보도 추가된 것을 확인할 수 있다. sizes prop을 사용하면 fill prop의 사용 유무와는 상관없이 위와 같이 deviceSizes와 imageSizes 값을 통해 srcset 값을 추가한다.

Image component를 사용할 때 src, alt 그리고 image 크기를 설정하는 property ( width, height 또는 fill )는 필수 property로 간주된다. 그 외에 optional로 사용할 수 있는 optional property에 대해 알아보자.

loader

Image component가 render될 때 srcset에 설정되는 image url을 생성 해주는 함수다. 따로 설정하지 않으면 NextJS default loader가 사용된다.

Src, width, quality를 parameter로 받으며 만약 image 최적화 및 저장을 위해 별개의 image server를 구축해서 운영하고 있거나 cloudniry, imagekit등과 같이 별개의 cloud service를 사용하고 있다면 그에 맞는 loader를 선언하여 Image component에 전달 해주어야 한다.

quality

loader 함수에 전달되는 quality param의 값을 설정한다. custom loader를 사용하고 있고 경우에 따라 다른 quality의 image를 제공해야 한다면 quality prop을 통해 loader 함수에 원하는 quality 값을 전달할 수 있다. ( default 75 )

placeholder

image가 load될 때 까지 보여줄 placeholder를 정한다. default는 empty ( 빈 화면 )이며 blur로 설정하거나 data:image/ 형태의 Data URL을 설정할 수도 있다. blur로 설정하면 blurDataURL이라는 별개의 prop으로 전달한 값이 placeholder로 사용된다. 다음은 documentation에서 가져온 blur와 blurDataURL를 사용하는 예제이다.

const keyStr =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

const triplet = (e1: number, e2: number, e3: number) =>
  keyStr.charAt(e1 >> 2) +
  keyStr.charAt(((e1 & 3) << 4) | (e2 >> 4)) +
  keyStr.charAt(((e2 & 15) << 2) | (e3 >> 6)) +
  keyStr.charAt(e3 & 63);

const rgbDataURL = (r: number, g: number, b: number) =>
  `data:image/gif;base64,R0lGODlhAQABAPAA${
    triplet(0, r, g) + triplet(b, 255, 255)
  }/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==`;

return (
    <Image
      ...
      src="/dog.jpg"
      placeholder="blur"
      blurDataURL={rgbDataURL(237, 181, 6)}
    />
)

위의 예제는 image가 load될 동안 blurDataURL prop에 전달한 값을 대신하여 보여준다. rgbDataURL함수는 전달 받은 rbg 값을 Data URL 형태로 변환하여 return하고 있다. 예제에서는 rgbDataURL 함수를 호출할 때 yellow color에 해당하는 rgb 값을 전달하여 호출하였으므로, image가 load되기 전까지 해당 영역에 yellow 색상의 background가 보여지다가 image가 load되면 image로 대체된다.

만약 local에 있는 image를 import하여 사용할 때 placeholder를 blur로 설정한다면 별도의 blurDataURL을 설정하지 않아도 NextJS에서 자동으로 blurDataURL 값을 추가하여 사용한다.

import profilePic from "/public/image/1200.jpg";

<Image
  ...
  src={profilePic}
  placeholder="blur"
/>

위의 예제는 import된 static image를 사용하고 있기에 별도의 blurDataURL를 설정해 주지 않아도 NextJS에서 자동으로 blur처리된 image를 blurDataURL 값으로 추가하여 사용한다. ( 단 image가 animation이 들어간 이미지가 아닌 static한 image여야 한다 - jpg, png, webp, avif )

Priority

NextJS Image component를 사용한 image는 기본적으로 lazy loading이 적용된다. 즉, image의 위치가 user가 보는 viewport의 어느 지점까지 도달할 때 image를 load한다.

이 때 priority prop을 true로 설정하면 lazy loading은 diabled되고 해당 image를 preload한다. Image component에 priority를 설정하고 개발자 도구를 통해 확인해 보면 head tag에 다음과 같이 image를 preload하는 구문이 추가된 것을 확인할 수 있다.

<head>
...
<link 
    rel="preload" 
    as="image" 
    imagesrcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=640&amp;q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1080&amp;q=75 2x" 
    fetchpriority="high"
>
...
</head>

unoptimized

만약 이미지가 매우 작은 사이즈로만 쓰여서 optimization 자체가 필요 없다면 unoptimized prop을 사용할 수 있다. unoptimized prop이 적용된 Image component는 optimization 과정을 거치지 않고 src에 설정한 이미지를 그대로 사용한다.

<Image
  ...
  unoptimized
  src={...}
/>

overrideSrc

Image component에 src property를 설정하면 component가 render될 때 img element에 srcset과 src 값이 자동으로 생성되어 적용된다. 만약에 render되는 img element의 src값을 수동으로 설정하고 싶다면 overrideSrc을 통해 설정이 가능하다.

<Image
  ...
  overrideSrc="/images/1200.jpg"
/>

next.config.js

Component에 전달하는 prop 뿐만 아니라 next.config.js 설정 파일에서도 Image component와 관련된 설정을 수정할 수 있다.

위에서 살펴본 바와 같이 image optimization & server를 따로 운영하고 있다면 loader prop을 통해 사용하는 image server의 요구사항에 맞는 custom loader function을 정의해 전달하였다.

만약 loader prop을 전달하지 않고 NextJS의 default loader 자체를 변경하고 싶다면 next.config.js 파일에서 변경할 수 있다.

const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './loaders/image-loader.js',
  },
};

export default nextConfig;

위의 예제와 같이 custom loader를 정의한 파일의 relative 경로 ( 프로젝트 root 기준으로 )를 기입해준다. 그리고 위에서 설정한 loadFile은 image load에 필요한 loader 함수를 default로 export 해주어야 하고 client component여야 한다.

'use client'

export default function customImageLoader({ src, width, quality }) {
  return `https://test.com/${src}?w=${width}&q=${quality || 75}`
}

Image component에 fill prop이나 sizes prop을 설정하면 component가 render될 때 img element의 srcset에 적용되는 width list는 다음과 같다.[640, 750, 828, 1080, 1200, 1920, 2048, 3840]

만약 해당 list를 변경하고 싶으면 deviceSizes property를 통해 변경할 수 있다.

const nextConfig = {
  images: {
     deviceSizes: [640, 1200, 1920, 3840],
  },
};

export default nextConfig;

deviceSizes를 위와 같이 설정하고 Image component가 생성하는 srcset를 확인해보면 다음과 같이 위에서 설정한 값에 맞추어 생성되는 것을 확인할 수 있다.

<img 
   ...
   srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=640&amp;q=75 640w,
           /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1200&amp;q=75 1200w, 
           /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=1920&amp;q=75 1920w, 
           /_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75 3840w" 
   src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1200.2625b324.jpg&amp;w=3840&amp;q=75"
>

imageSizes width list 역시 동일한 방법으로 수정 가능하다. default imageSizes는 다음과 같다. [16, 32, 48, 64, 96, 128, 256, 384] ( sizes prop이 제공된 Image component는 sizes에 설정된 값에 따라서 img element srcset list에 deviceSizes의 width 뿐만 아니라 imageSizes의 일부 width가 추가될 수도 있다. )

const nextConfig = {
  images: {
     imageSizes: [32, 64, 96, 128, 384],
  },
};

export default nextConfig;

formats

NextJS Default Image Optimization loader가 생성하는 image의 format을 결정한다. default는 image/webp format으로 설정되어 있으며 image/avif format으로도 설정이 가능하다.

만약 webp, avif 모두 설정되어 있으면 Request의 Acccept header를 통해 browser가 지원하는 format을 먼저 확인하고 둘 다 지원한다면 formats 설정에 먼저 선언한 image format을 사용한다.

const nextConfig = {
  images: {
     formats: ['image/webp', 'image/avif'],
  },
};

export default nextConfig;

만약 formats이 위의 예제처럼 설정되어 있고 브라우저가 두 format 모두 지원한다면 먼저 선언되어 있는 webp format으로 optimized image를 생성한다.