web-performance

Astro vs Next.js — 이미지 최적화는 어떻게 다를까

Astro의 빌드 타임 이미지 최적화와 Next.js의 런타임 이미지 최적화가 각각 어떻게 동작하는지 비교하고, 프로젝트 특성에 따른 선택 기준을 정리합니다.

astro nextjs image-optimization performance

들어가며

웹에서 이미지는 전체 페이지 용량의 절반 이상을 차지하는 경우가 많다. 최적화되지 않은 이미지 하나가 LCP(Largest Contentful Paint)를 수백 밀리초 늦추고, width/height 없는 이미지가 CLS(Cumulative Layout Shift)를 유발한다.

이런 문제를 프레임워크 레벨에서 해결하기 위해 Astro와 Next.js는 각각 이미지 컴포넌트를 제공한다. 둘 다 Sharp를 사용해 포맷 변환과 리사이즈를 처리하지만, 언제 그 작업을 수행하는지가 근본적으로 다르다.

Astro — 빌드 타임에 완성하는 방식

기본 동작

Astro의 <Image /> 컴포넌트는 astro build 시점에 이미지를 처리한다.

---
import { Image } from 'astro:assets';
import photo from '../assets/images/photo.png';
---
<Image src={photo} alt="프로필 사진" />

이 코드가 빌드를 거치면 다음과 같은 일이 일어난다.

빌드 시작

├─ Vite가 import된 이미지 감지
│   └─ photo.png의 메타데이터 추출 (width, height, format)

├─ Sharp로 이미지 처리
│   ├─ WebP로 포맷 변환
│   ├─ 지정된 크기로 리사이즈
│   └─ 품질 최적화

├─ 해시된 파일명으로 출력
│   └─ /_astro/photo_Z1a2b3c.webp

└─ HTML에 완성된 <img> 태그 삽입
    └─ <img src="/_astro/photo_Z1a2b3c.webp" width="800" height="600" alt="프로필 사진" loading="lazy" />

핵심은 빌드가 끝나면 이미 최적화된 정적 파일이 완성된다는 점이다. 배포 후에는 서버가 할 일이 없다. CDN에 올려두면 그대로 서빙된다.

이미지 경로에 따른 차이

Astro에서는 이미지를 어디에 두느냐에 따라 최적화 여부가 결정된다.

---
import { Image } from 'astro:assets';
import optimized from '../assets/images/photo.png';
---

<!-- src/assets/ 이미지: 최적화됨 -->
<Image src={optimized} alt="최적화 대상" />

<!-- public/ 이미지: 최적화 안 됨, 원본 그대로 서빙 -->
<Image src="/images/photo.png" alt="정적 파일" width={800} height={600} />

src/assets/에서 import한 이미지는 Vite 파이프라인을 거쳐 메타데이터가 자동 추출된다. width, height를 직접 지정할 필요가 없다. 반면 public/ 폴더의 이미지는 빌드 파이프라인을 거치지 않으므로 최적화되지 않고, widthheight를 수동으로 지정해야 한다.

<Picture /> — 다중 포맷 폴백

브라우저마다 지원하는 이미지 포맷이 다르다. AVIF는 압축률이 가장 좋지만 Safari 구버전에서는 지원하지 않는다. <Picture /> 컴포넌트는 이 문제를 <source> 태그 체인으로 해결한다.

---
import { Picture } from 'astro:assets';
import photo from '../assets/images/photo.png';
---
<Picture src={photo} formats={['avif', 'webp']} alt="다중 포맷" />

빌드 결과:

<picture>
  <source srcset="/_astro/photo_abc.avif" type="image/avif" />
  <source srcset="/_astro/photo_def.webp" type="image/webp" />
  <img src="/_astro/photo_ghi.png" alt="다중 포맷" width="800" height="600" loading="lazy" />
</picture>

브라우저는 위에서부터 지원하는 첫 번째 포맷을 선택한다. AVIF를 지원하면 AVIF, 아니면 WebP, 둘 다 안 되면 PNG로 폴백한다.

getImage() — 프로그래밍 방식

컴포넌트가 아닌 코드에서 최적화된 이미지 URL이 필요할 때 사용한다. CSS background-image나 OG 이미지 등에 활용할 수 있다.

---
import { getImage } from 'astro:assets';
import bg from '../assets/images/bg.png';

const optimizedBg = await getImage({ src: bg, format: 'webp', width: 1920 });
---
<div class="hero-bg" style={`background-image: url(${optimizedBg.src})`}></div>

Next.js — 요청할 때 처리하는 방식

기본 동작

Next.js의 <Image /> 컴포넌트는 빌드 시점에 이미지를 처리하지 않는다. 대신 클라이언트가 이미지를 요청할 때 서버에서 실시간으로 최적화한다.

import Image from 'next/image';

const Profile = () => (
  <Image src="/photo.png" alt="프로필 사진" width={800} height={600} />
);

이 코드가 렌더링되면 다음과 같은 흐름이 진행된다.

브라우저가 이미지 요청

├─ <img> 태그의 src가 /_next/image?url=/photo.png&w=828&q=75 로 변환됨

├─ Next.js 이미지 API 라우트가 요청 수신
│   ├─ 원본 이미지 로드 (/public/photo.png)
│   ├─ Accept 헤더 확인 → 브라우저가 WebP/AVIF 지원하는지 판단
│   ├─ Sharp로 리사이즈 + 포맷 변환
│   └─ 결과를 .next/cache/images/에 캐시

└─ 최적화된 이미지 응답
    └─ 이후 동일 요청은 캐시에서 서빙 (minimumCacheTTL까지)

최초 요청 시에는 Sharp 처리 시간만큼 지연이 발생한다. 하지만 한 번 캐시되면 이후 요청은 빠르게 응답된다.

반응형 srcset 자동 생성

Next.js의 가장 큰 강점은 sizes prop 하나로 반응형 이미지를 자동 처리한다는 점이다.

<Image
  src="/photo.png"
  alt="반응형"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

이 코드는 next.config.js에 정의된 deviceSizesimageSizes를 조합해 srcset을 자동 생성한다.

<img
  srcset="
    /_next/image?url=/photo.png&w=640&q=75 640w,
    /_next/image?url=/photo.png&w=750&q=75 750w,
    /_next/image?url=/photo.png&w=828&q=75 828w,
    /_next/image?url=/photo.png&w=1080&q=75 1080w,
    /_next/image?url=/photo.png&w=1200&q=75 1200w
  "
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

브라우저는 현재 뷰포트 너비와 DPR(Device Pixel Ratio)을 고려해 가장 적합한 크기를 선택한다. 모바일에서는 640w 이미지를, 데스크톱에서는 1200w 이미지를 받게 된다.

반면 Astro의 <Image />는 이런 자동 srcset 생성이 없다. 반응형 이미지가 필요하면 <Picture />widths prop을 수동으로 지정하거나, getImage()로 직접 구성해야 한다.

Placeholder blur

Next.js는 이미지 로딩 중 표시할 blur placeholder를 내장 지원한다.

import photo from '../public/photo.png';

// 정적 import — 빌드 시 저해상도 base64를 자동 생성
<Image src={photo} alt="blur" placeholder="blur" />

// 외부 이미지 — blurDataURL을 직접 제공
<Image
  src="https://example.com/photo.png"
  alt="blur"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
  width={800}
  height={600}
/>

정적 import의 경우 빌드 시 자동으로 저해상도 이미지를 생성해 인라인 base64로 삽입한다. 사용자는 이미지가 로딩되는 동안 흐릿한 미리보기를 보게 되어 체감 로딩 속도가 개선된다.

Astro에는 이 기능이 내장되어 있지 않다. 필요하다면 빌드 스크립트에서 저해상도 이미지를 직접 생성하거나, CSS backdrop-filter 등으로 유사하게 구현해야 한다.

커스텀 로더

기본적으로 Next.js는 자체 /_next/image API로 이미지를 처리하지만, 외부 이미지 CDN을 사용할 수도 있다.

const cloudinaryLoader = ({ src, width, quality }: {
  src: string;
  width: number;
  quality?: number;
}) => {
  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 'auto'}/${src}`;
};

<Image
  loader={cloudinaryLoader}
  src="photo.png"
  alt="Cloudinary"
  width={800}
  height={600}
/>

이 경우 /_next/image API를 거치지 않고 Cloudinary가 직접 이미지를 최적화한다. next.config.js에서 전역 로더를 설정하면 Node.js 서버 없이 정적 export에서도 이미지 최적화를 사용할 수 있다.

비교

항목AstroNext.js
최적화 시점빌드 타임런타임 (요청 시)
이미지 처리 엔진SharpSharp
서버 필요 여부SSG면 불필요필요 (또는 커스텀 로더로 우회)
출력 포맷빌드 설정에 따라 고정Accept 헤더 기반 자동 선택
반응형 srcset수동 구성 필요sizes prop으로 자동 생성
Placeholder blur미지원 (직접 구현)내장 지원
CLS 방지import 시 자동 추론width/height 필수 또는 fill
다중 포맷 폴백<Picture /> 컴포넌트Accept 헤더 기반 자동 선택
외부 이미지빌드 시 fetch 후 최적화런타임 프록시
캐시빌드 결과 = 캐시파일시스템 + CDN 캐시
CDN 로더getImage() 커스텀내장 로더 시스템

어떤 프로젝트에 어떤 방식이 맞을까

Astro 방식이 적합한 경우

Next.js 방식이 적합한 경우

정리

Astro와 Next.js의 이미지 최적화는 같은 문제를 다른 시점에서 해결한다. Astro는 빌드할 때 모든 이미지를 미리 처리해두고, Next.js는 사용자가 요청할 때 그때그때 처리한다.

어느 쪽이 더 낫다고 단정할 수는 없다. 이미지가 빌드 시점에 확정되는 정적 사이트라면 Astro의 빌드 타임 방식이 서버 비용 없이 예측 가능한 성능을 제공한다. 이미지가 동적으로 추가되거나 다양한 디바이스에 대응해야 하는 서비스라면 Next.js의 런타임 방식이 더 유연하다.

결국 이미지가 언제 확정되는가서버를 운영할 수 있는가라는 두 가지 질문에 대한 답이 선택 기준이 된다.