2-6. Next Js의 Asset 최적화
Next/Image를 이용한 이미지 최적화
이미지는 일반적인 웹사이트 페이지 무게의 아주 큰 부분을 차지하며, 웹사이트의 LCP(Largest Contentful Paint) 성능에 큰 영향을 미칠 수 있음
Next.js는 자동으로 이미지 크기를 최적화 해주는 이미지 컴포넌트 Image컴포넌트를 제공
자동 이미지 최적화 기능
- 크기 최적화
각 긱기에 맞는 크기의 이미지를 자동으로 제공, WebP 및 AVIF와 같은 최신 이미지 형식을 사용 - 시각적 안정성
이미지가 로딩 될 때 레이아웃 이동을 자동으로 방지 - 빠른 페이지 로드
네이티브 브라우저 지연 로딩을 사용하여 이미지가 뷰포트에 들어올 때만 로드됨. 블러업 플레이스 홀더를 옵션으로 사용 가능. - 자산 유연성
원격 서버에 저장된 이미지도 포함하여 필요에 따라 이미지 크기 조정
로컬 이미지 사용
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image src={profilePic}
alt="Picture of the author"
// width={500} 자동 제공
// height={500} 자동 제공
// blurDataURL="data:..." 자동 제공
// placeholder="blur" // 로딩 중 블러업 옵션
/>
)
}
원격 이미지, 네트워크 이미지
import Image from 'next/image'
export default function Page() {
return (
<Image
src="<network-image>"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
원격 이미지 허용을 위한 설정
- next.config.js파일에 지원되는 URL 패턴을 정의하여 안전하게 이미지 최적화
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "fakestoreapi.com",
pathname: "/**",
},
],
formats: ["image/avif", "image/webp"],
},
};
이미지를 최적화해주는 원리
- lazy loading
스크린 내(뷰포트)에 노출되는 이미지만 로드하여 페이지 로드 속도 개선
Next.js에서 next/image컴포넌트를 사용하면 기본적으로 지원함 - 이미지 사이즈 최적화
Next.js는 Next.js서버에서 이미지를 다운로드 받아, 압축해서 파일의 사이즈를 줄인채로 저장함
최초 요청 시에 Next.js 서버에서 이미지를 용량이 작은 WebP, AVIF 포맷으로 변환하는 작업을 함
이후 요청에는 캐시 된 이미지가 전달되기 때문에 훨씬 빠른 이미지를 서빙할 수 있음
next.config.js
module.exports = {
images: {
// ...
formats: ['image/avif', 'image/webp'],
},
}
이 설정을 통해 AVIF, 혹은 WebP로 자동으로 변환해줌
AVIF, WebP 확장자
압축 효율이 뛰어나 많은 관심을 받고 있는 이미지 포맷
WebP는 2010년 AVIF는 2019년에 정식 개발된 이미지 포맷으로 AVIF가 WebP보다 조금 더 나은 압축 성능을 낸다고 알려져있으나, WebP를 지원하는 브라우저 범위가 조금 넓음
Next.js 서버의 이미지 최적화 모듈은 요청 헤더에 있는 Accept 헤더를 읽어서 요청한 브라우저에서 처리할 수 있는 이미지 형식으로 최적화하여 내려주게 됨
그렇기 때문에, AVIF를 사용하겠다고 설정해둬도, AVIF를 지원하지 않는 브라우저에게는 WebP포맷을, AVIF를 지원하는 브라우저에서는 AVIF포맷을 응답으로 내려주기 때문에 편하게 사용해도 좋음
Metadata를 통한 SEO 최적화
메타 데이터는 애플리케이션의 SEO 및 웹 공유성을 개선하기 위해 HTML 헤드 요소 내부의 메타 및 링크 태그를 정의하는데 사용됨
메타 데이터를 정의하기 위한 두 가지 방법
- Config 기반 메타데이터
layout.js또는 page.js파일에서 정적 메타데이터 객체 또는 동적 generateMetadata함수를 내보냄 - 파일 기반 메타데이터
라우트 세그먼트에 정적 또는 동적으로 생성된 특별한 파일을 추가
두 옵션 모두에서 Next.js는 페이지에 대한 관련 <head>요소를 자동으로 생성함
정적 메타데이터
정적 메타 데이터를 정의하려면 layout.js또는 page.js파일에서 Metadata객체를 내보냄
// layout.tsx | page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
동적 메타데이터
동적 값이 필요한 메타 데이터를 가져오려면 generateMetadata함수를 사용할 수 있음
// app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
): Promise<Metadata> {
// 경로 매개변수 읽기
const id = params.id
// 데이터 가져오기
const product = await fetch(`https://.../${id}`).then((res) => res.json())
return {
title: product.title,
}
}
export default function Page({ params, searchParams }: Props) {}
정적 및 동적 메타 데이터는 Server Components에서만 지원됨
파일 기반 메타데이터
- favicon.ico, apple-icon.jpg, icon.jpg
- opengraph-image.jpg및 twitter-image.jpg
- robots.txt
- sitemap.xml
파일 기반 메타 데이터가 더 높은 우선순위를 가지고, config 기반 메타 데이터를 덮어씀 - --기본 필드__
경로가 메타 데이터를 정의하지 않아도 항상 추가되는 두 개의 기본 메타 태그가 있음
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
순서
메타 데이터는 루트 세그먼트에서 최종 page.js 세그먼트에 가장 가까운 세그먼트까지 순서대로 평가 됨. 예를 들어,
app/layout.tsx (루트 레이아웃)
app/product/layout.tsx (상품 레이아웃)
app/product/[id]/page.tsx (상품 페이지)
덮어쓰기
평가 순서를 따르며, 동일한 경로의 여러 세그먼트에서 내보낸 Metadata 객체는 얕게 병합되어 경로의 최종 메타데이터 출력을 형성. 중복 키는 순서에 따라 교체됨.
이는 openGraph 및 robots와 같은 중첩된 필드를 가진 메타 데이터가 이전 세그먼트에서 정의된 경우, 이를 설정한 마지막 세그먼트에 의해 덮어씌워짐을 의미
필드 덮어쓰기
// app/layout.js
export const metadata = {
title: 'Acme',
openGraph: {
title: 'Acme',
description: 'Acme is a...',
},
}
// app/product/page.js
export const metadata = {
title: 'Product',
openGraph: {
title: 'Product',
},
}
// 출력:
// <title>Product</title>
// <meta property="og:title" content="Product" />
- app/layout.js의 title은 app/product/page.js의 title로 대체
- app/blog/page.js가 openGraph 메타데이터를 설정하기 때문에 app/layout.js의 모든 openGraph 필드는 app/product/page.js에서 교체됨
일부 중첩 필드를 세그먼트 간에 공유하면서 다른 필드를 덮어 쓰려면 별도의 변수로 추출할 수 있음
// app/shared-metadata.js
export const openGraphImage = { images: ['http://...'] }
// app/page.js
import { openGraphImage } from './shared-metadata'
export const metadata = {
openGraph: {
...openGraphImage,
title: 'Home',
},
}
// app/about/page.js
import { openGraphImage } from '../shared-metadata'
export const metadata = {
openGraph: {
...openGraphImage,
title: 'About',
},
}
필드 상속
// app/layout.js
export const metadata = {
title: 'Acme',
openGraph: {
title: 'Acme',
description: 'Acme is a...',
},
}
// app/about/page.js
export const metadata = {
title: 'About',
}
// 출력:
// <title>About</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme is a..." />
- app/layout.js의 title은 app/about/page.js의 title로 대체
- app/about/page.js가 openGraph 메타데이터를 설정하지 않기 때문에 app/layout.js의 모든 openGraph 필드는 app/about/page.js에 상속됨