본문 바로가기
웹/프론트엔드

[Next.js] Next/Image layout:fill과 sizes에 대해

by 이민훈 2024. 3. 18.
import Head from "next/head";
import Image from "next/image";

const src =
  "https://i.namu.wiki/i/hJ1KU8XudUoCnAJaFnOGYDQSPOilQhEmve4Sv7usDD4mBCPf1bWEbEiN2y6HYaua2tZ9CfgV0ulrE4g4JOmOKGfrkfXxq028P3i_W-2cwK7jtlxeoHVGK7Dsu9wY1kDITXDsMGpxGj0QkpMlxWXfvw.webp";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Image src={src} alt="Image" width={300} height={300} />
    </>
  );
}

 

위의 소스 코드와 같이 간단한 이미지를 next/image로 렌더링한다고 생각해 봅시다.

 

 

서버에 이미지 리소스를 요청할 때 w=384라는 쿼리 파라미터가 붙은 것을 확인할 수 있습니다.

 

width가 384인 이미지를 달라고 요청한 거죠.

 

파일의 크기는 17614바이트네요.

 

 

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "i.namu.wiki",
      },
    ],
  },
};

export default nextConfig;

 

Next.js 공식 문서를 확인해보면 imageSizes, deviceSizes에 기본값이 위처럼 설정돼 있습니다.

 

breakPoints를 지정해 줄 수 있는 거죠.

 

그래서 width를 300으로 지정해 줬을 때 384사이즈에 해당하는 이미지를 요청하게 됩니다.

 

https://nextjs.org/docs/pages/api-reference/components/image

 

Components: <Image> | Next.js

Optimize Images in your Next.js Application using the built-in `next/image` Component.

nextjs.org

 

import Head from "next/head";
import Image from "next/image";

const src =
  "https://i.namu.wiki/i/hJ1KU8XudUoCnAJaFnOGYDQSPOilQhEmve4Sv7usDD4mBCPf1bWEbEiN2y6HYaua2tZ9CfgV0ulrE4g4JOmOKGfrkfXxq028P3i_W-2cwK7jtlxeoHVGK7Dsu9wY1kDITXDsMGpxGj0QkpMlxWXfvw.webp";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div style={{ position: "relative", width: "700px", height: "700px" }}>
        <Image src={src} alt="Image" layout="fill" />
      </div>
    </>
  );
}

 

코드를 위처럼 작성했을 땐 어떨지 확인해 볼까요?

 

 

1080크기의 이미지를 요청하고 있습니다.

 

 

크기도 43334바이트로 늘어났습니다.

 

layout속성을 fill로 지정해 주었을 때 image의 크기를 viewport크기에 맞춰 들고오기 때문에 그렇습니다.

 

이미지의 width를 직접 지정해 주기 어려운 경우,

 

예를 들어보면 width: "100%" 처럼 화면에 꽉 차게 이미지를 렌더링하고 싶은 경우가 있을 텐데

 

그런 경우에 부모 요소의 넓이를 100%로 지정하고 Image태그의 layout을 fill로 지정해 주게 됩니다.

 

 

실제로 뷰포트의 크기를 확인해 보면 832px이고,

 

832px은 828보다 큰값, 1080보다 작은값 이기에 1080사이즈의 이미지를 요청하게 됩니다.

 

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "i.namu.wiki",
      },
    ],
  },
};

export default nextConfig;

 

이번엔 이미지의 크기를 각각 100vw, 50vw로 렌더링하고 싶은 경우 아래와 같이 코드를 짜게 될 겁니다.

 

import Head from "next/head";
import Image from "next/image";

const src =
  "https://i.namu.wiki/i/hJ1KU8XudUoCnAJaFnOGYDQSPOilQhEmve4Sv7usDD4mBCPf1bWEbEiN2y6HYaua2tZ9CfgV0ulrE4g4JOmOKGfrkfXxq028P3i_W-2cwK7jtlxeoHVGK7Dsu9wY1kDITXDsMGpxGj0QkpMlxWXfvw.webp";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div style={{ position: "relative", width: "100vw", height: "300px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          style={{ objectFit: "cover" }}
        />
      </div>
      <div style={{ position: "relative", width: "50vw", height: "150px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          style={{ objectFit: "cover" }}
        />
      </div>
    </>
  );
}

 

100vw 크기로 렌더링한 이미지의 경우 1080사이즈의 이미지를 요청했고,

 

50vw 크기로 렌더링한 이미지의 경우도 1080사이즈의 이미지를 요청했습니다.

 

 

이유는 Image태그의 sizes속성 기본값이 100vw이기 때문인데,

 

한 마디로 layout:fill을 주게 될 경우 무조건 뷰포트 크기에 따라 이미지를 요청한다는 말이 됩니다.

 

이것을 방지하기 위해서 sizes프롭을 지정해 주어야 하는데, 

 

아래처럼 코드를 변경해 보겠습니다.

 

import Head from "next/head";
import Image from "next/image";

const src =
  "https://i.namu.wiki/i/hJ1KU8XudUoCnAJaFnOGYDQSPOilQhEmve4Sv7usDD4mBCPf1bWEbEiN2y6HYaua2tZ9CfgV0ulrE4g4JOmOKGfrkfXxq028P3i_W-2cwK7jtlxeoHVGK7Dsu9wY1kDITXDsMGpxGj0QkpMlxWXfvw.webp";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div style={{ position: "relative", width: "100vw", height: "300px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          sizes="100vw"
          style={{ objectFit: "cover" }}
        />
      </div>
      <div style={{ position: "relative", width: "50vw", height: "150px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          sizes="50vw"
          style={{ objectFit: "cover" }}
        />
      </div>
    </>
  );
}

 

큰 이미지의 경우 그대로 1080사이즈의 이미지를 요청했고,

 

 

작은 이미지의 경우 640사이즈의 이미지를 요청했네요.

 

 

다시한번 breakPoints를 확인해 봅시다.

 

지금 뷰포트의 크기는 832px입니다.

 

그래서 100vw의 경우 1080사이즈의 이미지를 요청했고,

 

50vw의 경우 832/2 = 416px이 됩니다.

 

그래서 384~640의 범위에 해당하기에 640사이즈의 이미지를 요청하게 됩니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "i.namu.wiki",
      },
    ],
  },
};

export default nextConfig;

 

마지막으로 20vw의 경우를 확인해보면

import Head from "next/head";
import Image from "next/image";

const src =
  "https://i.namu.wiki/i/hJ1KU8XudUoCnAJaFnOGYDQSPOilQhEmve4Sv7usDD4mBCPf1bWEbEiN2y6HYaua2tZ9CfgV0ulrE4g4JOmOKGfrkfXxq028P3i_W-2cwK7jtlxeoHVGK7Dsu9wY1kDITXDsMGpxGj0QkpMlxWXfvw.webp";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div style={{ position: "relative", width: "100vw", height: "300px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          sizes="100vw"
          style={{ objectFit: "cover" }}
        />
      </div>
      <div style={{ position: "relative", width: "20vw", height: "150px" }}>
        <Image
          src={src}
          alt="Image"
          layout="fill"
          sizes="20vw"
          style={{ objectFit: "cover" }}
        />
      </div>
    </>
  );
}

 

832/5 = 166.4px 이기에

 

128~256의 범위에 해당하는 256사이즈의 이미지를 요청하게 됩니다.

 

 

layout:fill을 사용할 경우 같은 이미지를 어떤 컴포넌트 내에서 렌더링하냐에 따라 적절한 sizes를 지정해 주어야

 

렌더링할 크기보다 한참 큰 이미지를 불러오는 일이 일어나지 않습니다.

 

물론 이미지를 크기에 맞게 변환하여 내려주는 것 자체가 리소스가 드는 일이기 때문에,

 

breakPoints를 과도하게 설정해주는 것이 좋지는 않습니다.

 

대체로 필요한 경우를 제외하고, 기본값을 사용하면 될 듯합니다.

 

당연하게도 원본 이미지가 600px이라면, 1080사이즈, 1200사이즈로 요청하더라도

 

원본 이미지가 내려오게 됩니다.

댓글