본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 11장: 이미지, 폰트, 메타데이터 최적화
2026년 2월 9일·웹 개발·

11장: 이미지, 폰트, 메타데이터 최적화

Next.js의 이미지, 폰트, 메타데이터 최적화를 다룹니다. next/image, next/font, generateMetadata, OG 이미지 생성, Core Web Vitals 전략을 살펴봅니다.

17분1,034자9개 섹션
nextjsreacttypescriptfrontend
공유
nextjs-app-router11 / 13
12345678910111213
이전10장: 미들웨어와 Proxy 고급 패턴다음12장: 모니터링, 보안, 프로덕션 배포

10장에서 미들웨어와 Proxy 패턴을 다루었습니다. 이번 장에서는 사용자가 실제로 체감하는 성능, 즉 이미지 로딩, 폰트 렌더링, 검색 엔진 최적화를 위한 Next.js의 내장 최적화 도구들을 살펴봅니다.

next/image: 이미지 최적화

기본 사용법

next/image는 HTML <img> 태그를 대체하는 컴포넌트로, 자동 최적화를 제공합니다.

기본 이미지 사용
typescript
import Image from 'next/image';
import profilePic from '@/assets/profile.jpg';
 
export default function Profile() {
  return (
    <Image
      src={profilePic}
      alt="프로필 사진"
      width={300}
      height={300}
      placeholder="blur" // 빌드 시 자동 blur 이미지 생성
    />
  );
}

로컬 이미지를 import하면 width, height, blurDataURL이 자동으로 설정됩니다. 빌드 시점에 이미지를 분석하여 레이아웃 시프트를 방지합니다.

자동 포맷 변환과 반응형

next/image는 브라우저가 지원하는 최적의 포맷으로 자동 변환합니다. WebP를 지원하면 WebP로, AVIF를 지원하면 AVIF로 변환하여 파일 크기를 최대 50-80% 줄입니다.

반응형 이미지
typescript
<Image
  src="/hero.jpg"
  alt="히어로 이미지"
  fill                    // 부모 컨테이너를 채움
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  style={{ objectFit: 'cover' }}
/>

sizes 속성은 브라우저에게 이미지의 표시 크기를 알려주어, 적절한 크기의 이미지를 요청하게 합니다. 모바일에서 2000px 원본을 다운로드하는 낭비를 방지합니다.

Lazy Loading과 priority

기본적으로 모든 이미지는 Lazy Loading됩니다. 뷰포트에 들어오기 전까지 이미지를 로드하지 않아 초기 페이지 로딩이 빨라집니다.

하지만 LCP(Largest Contentful Paint) 요소인 이미지는 priority 속성을 추가해야 합니다. 이 속성은 Lazy Loading을 비활성화하고, <link rel="preload">를 생성하여 이미지를 먼저 로드합니다.

LCP 이미지에 priority 적용
typescript
export default function HeroSection() {
  return (
    <section>
      {/* 히어로 이미지: LCP 요소이므로 priority 추가 */}
      <Image
        src="/hero-banner.jpg"
        alt="메인 배너"
        width={1200}
        height={600}
        priority  // preload + eager loading
      />
 
      {/* 하단 이미지: lazy loading (기본값) */}
      <Image
        src="/secondary.jpg"
        alt="보조 이미지"
        width={600}
        height={400}
      />
    </section>
  );
}
Warning

priority를 남용하면 오히려 성능이 저하됩니다. 페이지당 1-2개의 Above-the-fold 이미지에만 적용하세요. 모든 이미지에 priority를 추가하면 브라우저의 리소스 우선순위가 무의미해집니다.

Next.js 15-16의 이미지 변경사항

버전변경 내용
15.0sharp가 기본 이미지 처리 라이브러리로 변경 (성능 향상)
15.0Content-Disposition: attachment가 기본값으로 변경
16.0이미지 캐시 TTL이 4시간으로 변경 (이전: 무제한)
16.0qualities 설정으로 이미지 품질 세밀 제어 가능
next.config.ts - 이미지 설정
typescript
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  images: {
    // 외부 이미지 호스트 허용
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/uploads/**',
      },
    ],
 
    // Next.js 16: 이미지 품질 세밀 제어
    qualities: [25, 50, 75, 90],
 
    // 포맷 우선순위
    formats: ['image/avif', 'image/webp'],
 
    // 캐시 TTL (초 단위, 기본 4시간)
    minimumCacheTTL: 60 * 60 * 24, // 24시간으로 변경
  },
};
 
export default nextConfig;

qualities 설정은 Next.js 16에서 추가된 기능으로, 이미지 요청 시 사용할 수 있는 품질 값을 제한합니다. 이를 통해 캐시 효율이 높아지고, 불필요한 품질 변형 생성을 방지합니다.

next/font: 폰트 최적화

폰트 로딩의 문제

웹 폰트를 최적화하지 않으면 두 가지 문제가 발생합니다.

  1. FOUT(Flash of Unstyled Text) - 시스템 폰트가 먼저 표시된 후 웹 폰트로 교체
  2. FOIT(Flash of Invisible Text) - 폰트가 로드될 때까지 텍스트가 보이지 않음

next/font는 빌드 시점에 폰트를 다운로드하여 정적 자산으로 셀프 호스팅함으로써, 런타임 네트워크 요청을 제거합니다.

Google Fonts 사용

src/lib/fonts.ts
typescript
import { Inter, JetBrains_Mono } from 'next/font/google';
 
export const inter = Inter({
  subsets: ['latin'],
  display: 'swap',      // FOUT 허용 (FOIT 방지)
  variable: '--font-inter',
});
 
export const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-code',
});

로컬 폰트: Variable Font 사용

한글 폰트인 Pretendard처럼 Google Fonts에 없는 폰트는 로컬에서 로드합니다. Variable Font를 사용하면 하나의 폰트 파일로 모든 굵기를 표현할 수 있어 파일 크기가 크게 줄어듭니다.

src/lib/fonts.ts - Pretendard
typescript
import localFont from 'next/font/local';
 
export const pretendard = localFont({
  src: [
    {
      path: '../assets/fonts/PretendardVariable.woff2',
      style: 'normal',
    },
  ],
  display: 'swap',
  variable: '--font-pretendard',
  weight: '100 900', // Variable Font 범위
  fallback: [
    '-apple-system',
    'BlinkMacSystemFont',
    'system-ui',
    'Roboto',
    'sans-serif',
  ],
});

레이아웃에 적용

app/layout.tsx
typescript
import { pretendard, jetbrainsMono } from '@/lib/fonts';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="ko"
      className={`${pretendard.variable} ${jetbrainsMono.variable}`}
    >
      <body className="font-sans">
        {children}
      </body>
    </html>
  );
}
globals.css - Tailwind 설정
css
@theme {
  --font-sans: var(--font-pretendard), -apple-system, BlinkMacSystemFont,
    system-ui, sans-serif;
  --font-mono: var(--font-code), 'Courier New', monospace;
}
Tip

font-display: swap은 폰트가 로드되기 전에 시스템 폰트를 표시하여 FOIT를 방지합니다. 텍스트가 잠깐 깜빡이는 것이 보이지 않는 것보다 낫다는 Web Vitals 철학에 기반합니다.

generateMetadata: 동적 메타데이터

기본 구조

App Router에서 메타데이터는 generateMetadata 함수로 생성합니다. 각 페이지에서 동적으로 메타데이터를 설정할 수 있습니다.

app/posts/[slug]/page.tsx
typescript
import type { Metadata } from 'next';
import { getPost } from '@/lib/content';
 
interface PageProps {
  params: Promise<{ slug: string }>;
}
 
export async function generateMetadata(
  { params }: PageProps
): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
 
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
      images: [
        {
          url: `/og/${post.slug}`,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
    },
  };
}

generateViewport 분리

Next.js 14부터 뷰포트 관련 메타데이터는 generateViewport로 분리되었습니다. generateMetadata에 뷰포트 설정을 넣으면 경고가 발생합니다.

app/layout.tsx - viewport 분리
typescript
import type { Metadata, Viewport } from 'next';
 
export const metadata: Metadata = {
  title: {
    template: '%s | My Blog',
    default: 'My Blog',
  },
  description: '기술 블로그',
};
 
// viewport는 별도 export
export const viewport: Viewport = {
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
    { media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
  ],
  width: 'device-width',
  initialScale: 1,
  maximumScale: 5,
};

JSON-LD 구조화 데이터

검색 엔진이 콘텐츠를 더 잘 이해할 수 있도록 JSON-LD(JavaScript Object Notation for Linked Data)를 추가합니다.

app/posts/[slug]/page.tsx - JSON-LD
typescript
import type { BlogPosting, WithContext } from 'schema-dts';
 
export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;
  const post = await getPost(slug);
 
  const jsonLd: WithContext<BlogPosting> = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.description,
    datePublished: post.date,
    dateModified: post.updated ?? post.date,
    author: {
      '@type': 'Person',
      name: 'Kreath',
      url: 'https://archive.kreathlab.com',
    },
    image: `https://archive.kreathlab.com/og/${post.slug}`,
    publisher: {
      '@type': 'Person',
      name: 'Kreath',
    },
  };
 
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
        {/* 본문 */}
      </article>
    </>
  );
}

OG 이미지 생성

ImageResponse API

Next.js의 ImageResponse는 JSX를 기반으로 동적 OG 이미지를 서버에서 생성합니다. Next.js 16.2에서 성능이 2-20배 향상되었습니다.

app/og/[slug]/route.tsx
typescript
import { ImageResponse } from 'next/og';
import { getPost } from '@/lib/content';
 
export const runtime = 'edge';
 
export async function GET(
  _request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;
  const post = await getPost(slug);
 
  // 폰트 로드
  const fontData = await fetch(
    new URL('../../../assets/fonts/Pretendard-Bold.otf', import.meta.url)
  ).then(res => res.arrayBuffer());
 
  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          padding: '60px 80px',
          background: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)',
          color: '#ffffff',
          fontFamily: 'Pretendard',
        }}
      >
        <div style={{ fontSize: 24, color: '#888', marginBottom: 16 }}>
          {post.category.toUpperCase()}
        </div>
        <div style={{ fontSize: 52, fontWeight: 700, lineHeight: 1.3 }}>
          {post.title}
        </div>
        <div
          style={{
            fontSize: 20,
            color: '#aaa',
            marginTop: 24,
            lineHeight: 1.6,
          }}
        >
          {post.description}
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Pretendard',
          data: fontData,
          style: 'normal',
          weight: 700,
        },
      ],
    }
  );
}
Info

ImageResponse는 Satori 라이브러리를 기반으로 합니다. CSS Flexbox만 지원하며, CSS Grid는 사용할 수 없습니다. 복잡한 레이아웃이 필요하면 중첩 Flex 컨테이너로 구성합니다.

Sitemap과 Robots 생성

sitemap.ts

app/sitemap.ts
typescript
import type { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/content';
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  const baseUrl = 'https://archive.kreathlab.com';
 
  const postEntries = posts.map(post => ({
    url: `${baseUrl}/tech/${post.slug}`,
    lastModified: new Date(post.updated ?? post.date),
    changeFrequency: 'monthly' as const,
    priority: 0.7,
  }));
 
  return [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1.0,
    },
    {
      url: `${baseUrl}/tech`,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.9,
    },
    ...postEntries,
  ];
}

robots.ts

app/robots.ts
typescript
import type { MetadataRoute } from 'next';
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/'],
      },
    ],
    sitemap: 'https://archive.kreathlab.com/sitemap.xml',
  };
}

Core Web Vitals 최적화 전략

지금까지 다룬 최적화 도구들이 Core Web Vitals 각 지표에 어떻게 기여하는지 정리합니다.

LCP (Largest Contentful Paint)

LCP는 가장 큰 콘텐츠 요소가 표시되기까지의 시간입니다.

전략효과
priority 속성LCP 이미지를 preload하여 로딩 우선순위 상승
next/font self-hosting폰트 네트워크 요청 제거로 텍스트 빠르게 표시
AVIF/WebP 자동 변환이미지 크기 감소로 다운로드 시간 단축
sizes 속성적절한 크기의 이미지만 다운로드

CLS (Cumulative Layout Shift)

CLS는 페이지 로딩 중 레이아웃이 얼마나 이동하는지를 측정합니다.

전략효과
width/height 명시이미지 공간을 미리 확보하여 레이아웃 시프트 방지
placeholder="blur"이미지 로딩 중 blur 이미지로 공간 유지
font-display: swap폰트 교체 시 레이아웃 시프트 최소화
Variable Font하나의 파일로 모든 굵기 지원, 폰트 교체 횟수 감소

INP (Interaction to Next Paint)

INP는 사용자 상호작용에 대한 응답 시간입니다. 이미지와 폰트 최적화가 직접적인 영향은 적지만, 전체적인 리소스 로딩 최적화가 메인 스레드 부하를 줄여 간접적으로 기여합니다.

실전: 메타데이터 레이어 설계

대규모 사이트에서 메타데이터를 체계적으로 관리하는 패턴입니다.

lib/metadata.ts - 메타데이터 유틸리티
typescript
import type { Metadata } from 'next';
 
const SITE_NAME = 'My Archive';
const SITE_URL = 'https://archive.kreathlab.com';
const SITE_DESCRIPTION = '기술 블로그, 포트폴리오, 독서 리뷰';
 
interface CreateMetadataParams {
  title: string;
  description?: string;
  path?: string;
  image?: string;
  type?: 'website' | 'article';
  publishedTime?: string;
}
 
export function createMetadata({
  title,
  description = SITE_DESCRIPTION,
  path = '',
  image,
  type = 'website',
  publishedTime,
}: CreateMetadataParams): Metadata {
  const url = `${SITE_URL}${path}`;
  const ogImage = image ?? `${SITE_URL}/og/default`;
 
  return {
    title,
    description,
    alternates: {
      canonical: url,
    },
    openGraph: {
      title,
      description,
      url,
      siteName: SITE_NAME,
      type,
      ...(publishedTime && { publishedTime }),
      images: [{ url: ogImage, width: 1200, height: 630 }],
    },
    twitter: {
      card: 'summary_large_image',
      title,
      description,
      images: [ogImage],
    },
  };
}
app/tech/[slug]/page.tsx - 유틸리티 활용
typescript
import { createMetadata } from '@/lib/metadata';
 
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
 
  return createMetadata({
    title: post.title,
    description: post.description,
    path: `/tech/${post.slug}`,
    image: `/og/${post.slug}`,
    type: 'article',
    publishedTime: post.date,
  });
}

이 패턴은 메타데이터 생성 로직을 중앙화하여, 모든 페이지에서 일관된 메타데이터를 생성합니다.

핵심 요약

  • next/image는 자동 WebP/AVIF 변환, Lazy Loading, blur placeholder를 제공합니다. LCP 이미지에는 priority 속성을, 반응형 이미지에는 sizes 속성을 반드시 적용합니다.
  • next/font는 빌드 시점에 폰트를 셀프 호스팅하여 런타임 네트워크 요청을 제거합니다. Variable Font와 font-display: swap으로 최적의 로딩 경험을 제공합니다.
  • generateMetadata와 generateViewport가 분리되어 각각 SEO 메타데이터와 뷰포트 설정을 담당합니다. JSON-LD 구조화 데이터로 검색 엔진 이해도를 높입니다.
  • ImageResponse로 동적 OG 이미지를 JSX 기반으로 생성하며, Next.js 16.2에서 2-20배 성능이 향상되었습니다.
  • sitemap.ts와 robots.ts로 검색 엔진 크롤링을 최적화합니다.

다음 장에서는 모니터링, 보안, 프로덕션 배포를 다룹니다. instrumentation.ts, CSP 헤더, Docker 배포, Build Adapters 등 프로덕션 준비에 필요한 모든 것을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#nextjs#react#typescript#frontend

관련 글

웹 개발

12장: 모니터링, 보안, 프로덕션 배포

Next.js의 프로덕션 배포를 다룹니다. instrumentation.ts, OpenTelemetry, 보안 헤더, Docker 배포, Build Adapters, 셀프 호스팅 전략을 살펴봅니다.

2026년 2월 11일·15분
웹 개발

10장: 미들웨어와 Proxy 고급 패턴

Next.js의 미들웨어 진화를 다룹니다. middleware.ts에서 proxy.ts로의 전환, 인증, i18n, A/B 테스팅, 속도 제한 등 고급 패턴을 살펴봅니다.

2026년 2월 7일·17분
웹 개발

13장: 실전 프로젝트 - Next.js 풀스택 앱 구축

Link Shortener 앱을 구축하며 Next.js App Router의 모든 기능을 실전에 적용합니다. 프로젝트 구조 설계부터 배포까지 전 과정을 다룹니다.

2026년 2월 13일·21분
이전 글10장: 미들웨어와 Proxy 고급 패턴
다음 글12장: 모니터링, 보안, 프로덕션 배포

댓글

목차

약 17분 남음
  • next/image: 이미지 최적화
    • 기본 사용법
    • 자동 포맷 변환과 반응형
    • Lazy Loading과 priority
    • Next.js 15-16의 이미지 변경사항
  • next/font: 폰트 최적화
    • 폰트 로딩의 문제
    • Google Fonts 사용
    • 로컬 폰트: Variable Font 사용
    • 레이아웃에 적용
  • generateMetadata: 동적 메타데이터
    • 기본 구조
    • generateViewport 분리
  • JSON-LD 구조화 데이터
  • OG 이미지 생성
    • ImageResponse API
  • Sitemap과 Robots 생성
    • sitemap.ts
    • robots.ts
  • Core Web Vitals 최적화 전략
    • LCP (Largest Contentful Paint)
    • CLS (Cumulative Layout Shift)
    • INP (Interaction to Next Paint)
  • 실전: 메타데이터 레이어 설계
  • 핵심 요약