Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .prettierrc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ export default {
parser: 'astro',
},
},
{
files: '*.mdx',
options: {
printWidth: 80,
},
},
],
}
Binary file not shown.
330 changes: 330 additions & 0 deletions src/content/post/blog/nextjs-image-optimization.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
---
title: NextJS๋Š” ์–ด๋–ป๊ฒŒ ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ• ๊นŒ?
date: 2025-02-08
updatedDate: 2025-02-08
tags: [nextjs, next Image, nextjs ์ด๋ฏธ์ง€ ์ตœ์ ํ™”]
image: ''
---

:::note
์ด ๊ธ€์€ NextJS **14.2.23** ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
:::

์›น์—์„œ ์ด๋ฏธ์ง€๋Š” **ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์†๋„**์™€ **์‚ฌ์šฉ์ž ๊ฒฝํ—˜**์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ค. ์ตœ์ ํ™”๋˜์ง€ ์•Š์€ ์ด๋ฏธ์ง€๋Š” ํŽ˜์ด์ง€ ๋กœ๋”ฉ์„ ๋А๋ฆฌ๊ฒŒ ํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ์„ ๋†’์ด๋ฉฐ, SEO์—๋„ ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด NextJS๋Š” `next/image` ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ๊ณตํ•ด ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.
๊ณต์‹ ๋ฌธ์„œ์™€ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ `next/image`์˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์›๋ฆฌ๋ฅผ ์•Œ์•„๋ณด์ž.

## ๋ฌด์—‡์„ ์ตœ์ ํ™”ํ• ๊นŒ?

[NextJS ๊ณต์‹ ๋ฌธ์„œ](https://nextjs.org/docs/app/building-your-application/optimizing/images)์— ๋”ฐ๋ฅด๋ฉด `next/image` ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ•œ๋‹ค.

- **์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ตœ์ ํ™”**: ๋ธŒ๋ผ์šฐ์ €์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ํฌ๊ธฐ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ œ๊ณต
- **์ตœ์‹  ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜**: WebP, AVIF ๋“ฑ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ง€์›ํ•˜๋Š” ์ตœ์ ์˜ ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜
- **Lazy Loading** ๊ธฐ๋ณธ ์ง€์›: ํ•„์š”ํ•  ๋•Œ๋งŒ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” (๋ทฐํฌํŠธ์— ์ง„์ž…ํ•  ๋•Œ ์ด๋ฏธ์ง€ ๋กœ๋“œ)
- **Placeholder Blur** ์ง€์›: ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „์— ๋ธ”๋Ÿฌ ํšจ๊ณผ ์ œ๊ณต
- **CDN ์ตœ์ ํ™”**: Vercel ๋ฐฐํฌ ์‹œ ์ž๋™์œผ๋กœ CDN์„ ํ™œ์šฉํ•œ ์ตœ์ ํ™” ์ˆ˜ํ–‰

## ์–ด๋–ค ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜ํ• ๊นŒ?

```ts title="next/src/server/image-optimizer.ts"
function getSupportedMimeType(options: string[], accept = ''): string {
const mimeType = mediaType(accept, options)
return accept.includes(mimeType) ? mimeType : ''
}

const mimeType = getSupportedMimeType(formats || [], req.headers['accept'])
```

`next/image`์˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๋ฅผ ๋‹ด๋‹นํ•˜๋Š” `image-optimizer.ts` ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ๋ธŒ๋ผ์šฐ์ €์˜ **Accept** ํ—ค๋” ๊ฐ’๊ณผ config์˜ **formats**๋กœ ๋ณ€ํ™˜ํ•  ํฌ๋งท์„ ๊ฒฐ์ •ํ•œ๋‹ค.

๋ธŒ๋ผ์šฐ์ €์˜ **Accept** ํ—ค๋”๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” **MIME ํƒ€์ž…**์„ ์„œ๋ฒ„์— ์•Œ๋ฆฌ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜์ž๋ฉด Accept ํ—ค๋”์— **๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ง€์›ํ•˜๋Š” ํฌ๋งท ์ •๋ณด**๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค.

`getSupportedMimeType` ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด **formats** ๋ฐฐ์—ด ๊ฐ’ ์ค‘ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ง€์›ํ•˜๋Š” ์ตœ์ ์˜ ํฌ๋งท์œผ๋กœ `mimeType`์„ ๊ฒฐ์ •ํ•˜๋Š”๋ฐ, ๋งŒ์•ฝ formats ๋ฐฐ์—ด์˜ ๊ฐ’์„ **๋ชจ๋‘ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์›๋ณธ ํฌ๋งท์„ ์œ ์ง€**ํ•œ๋‹ค.

```js title="next.config.mjs"
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // [!code highlight]
},
}

export default nextConfig
```

options๋Š” `nextConfig`์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ **๊ธฐ๋ณธ๊ฐ’**์€ `['image/webp']`์ด๋‹ค.

## ์ตœ์ ํ™”๋˜์ง€ ์•Š๋Š” ์ด๋ฏธ์ง€

```ts title="next/src/server/image-optimizer.ts"
// ANIMATABLE_TYPES = [WEBP, PNG, GIF]
if (ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)) {
Log.warnOnce(
`The requested resource "${href}" is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`
)
return {
buffer: upstreamBuffer,
contentType: upstreamType,
maxAge,
}
}
```

GIF ๋“ฑ **Animatable ์ด๋ฏธ์ง€**๋Š” ์ตœ์ ํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๋Š”๋‹ค. ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๊ณผ์ •์„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์œ„ํ•ด `unoptimized={true}`๋ฅผ ๊ถŒ์žฅํ•œ๋‹ค.

```ts title="next/src/server/image-optimizer.ts"
if (
upstreamType.startsWith('image/svg') &&
!nextConfig.images.dangerouslyAllowSVG
) {
Log.error(
`The requested resource "${href}" has type "${upstreamType}" but dangerouslyAllowSVG is disabled`
)
throw new ImageError(
400,
'"url" parameter is valid but image type is not allowed'
)
}

// VECTOR_TYPES = [SVG]
if (VECTOR_TYPES.includes(upstreamType)) {
return {
buffer: upstreamBuffer,
contentType: upstreamType,
maxAge,
}
}
```

**๋ฒกํ„ฐ ์ด๋ฏธ์ง€**์ธ SVG๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ธ๋ฐ, `next/image`์—์„œ SVG๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด `unoptimized={true}`๋ฅผ **๋ฐ˜๋“œ์‹œ ์ ์šฉ**ํ•ด์•ผ ํ•œ๋‹ค.

{/* prettier-ignore-start */}
```js title="next.config.mjs"
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
dangerouslyAllowSVG: true, // [!code highlight]
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", // [!code highlight]
},
}
```
{/* prettier-ignore-end */}

ํ”„๋กœ์ ํŠธ ์ „์ฒด์— ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์œ„์ฒ˜๋Ÿผ `dangerouslyAllowSVG` ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.
SVG๋Š” JavaScript๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์–ด **XSS ๊ณต๊ฒฉ ๋“ฑ ๋ณด์•ˆ์ƒ์˜ ์œ„ํ—˜** ๋•Œ๋ฌธ์— 'dangerous'๋ผ๋Š” ์ด๋ฆ„์ด ๋ถ™์—ˆ์œผ๋ฉฐ, `contentSecurityPolicy` ์„ค์ •์„ ํ†ตํ•ด ๋ณด์•ˆ ์ •์ฑ…์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

## ์ตœ์ ํ™”๋Š” ์–ธ์ œ ์ด๋ฃจ์–ด์งˆ๊นŒ?

`next/image`์˜ ์ตœ์ ํ™”๋Š” ๋นŒ๋“œ ์‹œ์ ์ด ์•„๋‹Œ, ๋ธŒ๋ผ์šฐ์ €์—์„œ **์ด๋ฏธ์ง€๋ฅผ ์š”์ฒญ**ํ•  ๋•Œ ์ด๋ฃจ์–ด์ง„๋‹ค.
์ตœ์ ํ™” ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

1. HTML์— ์ด๋ฏธ์ง€ ํƒœ๊ทธ ๋ Œ๋”๋ง
1. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„œ๋ฒ„์— ์ด๋ฏธ์ง€ ์š”์ฒญ (์ด๋•Œ Accept ํ—ค๋”์— ์ง€์›ํ•˜๋Š” ์ด๋ฏธ์ง€ ํฌ๋งท ์ •๋ณด ์ „์†ก)

- ์˜ˆ) `Accept: image/avif,image/webp,image/*,*/*;q=0.8`

1. ์„œ๋ฒ„๊ฐ€ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์ˆ˜ํ–‰
1. ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€ ์‘๋‹ต

๋‹ค์Œ ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด ์‚ดํŽด๋ณด์ž.

```tsx
import Image from 'next/image'
import testImage from '../public/test.png'

export default function Page() {
return (
<>
<img src={testImage.src} alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€" />
<Image src={testImage} alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€" />
</>
)
}
```

์œ„ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ Œ๋”๋ง ๋œ๋‹ค.

```html highlight={2,16}
<!-- img ํƒœ๊ทธ: ์›๋ณธ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ -->
<img src="/_next/static/media/test.5a33257e.png" alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€" />
<!-- Image ์ปดํฌ๋„ŒํŠธ: ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ URL๋กœ ๋ณ€ํ™˜ -->
<img
alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€"
loading="lazy"
width="1048"
height="743"
decoding="async"
data-nimg="1"
style="color:transparent"
srcset="
/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.5a33257e.png&amp;w=1080&amp;q=75 1x,
/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.5a33257e.png&amp;w=3840&amp;q=75 2x
"
src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.5a33257e.png&amp;w=3840&amp;q=75"
/>
```

`img` ํƒœ๊ทธ์™€ ๋‹ฌ๋ฆฌ `next/image` ์ปดํฌ๋„ŒํŠธ๋Š” ์ด๋ฏธ์ง€ URL์„ `/_next/image?url=...` ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. ์ด URL๋กœ **์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด** Next.js์˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ๋™์ž‘ํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

![slow 4g ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•œ ๊ฒฐ๊ณผ](/images/nextjs-image-optimization/img-test.avif)

๋„คํŠธ์›Œํฌ ํƒญ์„ ์‚ดํŽด๋ณด๋ฉด ๊ธฐ๋ณธ `img` ํƒœ๊ทธ์˜ ์š”์ฒญ์€ ์›๋ณธ PNG ์ด๋ฏธ์ง€(794 KB)๋ฅผ ๊ทธ๋Œ€๋กœ ์ „์†กํ•˜๊ณ  `next/image` ์ปดํฌ๋„ŒํŠธ์˜ ์š”์ฒญ์€ WebP ํ˜•์‹(45.7 KB)์œผ๋กœ **์ตœ์ ํ™”๋˜์–ด ์ „์†ก**๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
`next/image`์˜ ๋‘ ๋ฒˆ์งธ ์š”์ฒญ๋ถ€ํ„ฐ๋Š” **์บ์‹œ์—์„œ ์ฆ‰์‹œ ์‘๋‹ต**ํ•œ๋‹ค.

์ตœ์ ํ™” ๊ณผ์ •์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์š”์•ฝํ•ด ๋ณด์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ `/_next/image?url=...` ๊ฒฝ๋กœ๋กœ ์š”์ฒญ์„ ๋ณด๋ƒ„
1. ์„œ๋ฒ„๊ฐ€ Sharp(๋˜๋Š” Squoosh)๋ฅผ ์‚ฌ์šฉํ•ด ๋ฆฌ์‚ฌ์ด์ง• ๋ฐ ํฌ๋งท ๋ณ€ํ™˜ ์ˆ˜ํ–‰
1. ๋ณ€ํ™˜๋œ ์ด๋ฏธ์ง€๋ฅผ `/.next/cache/images/`์— ์ €์žฅ (๊ฐ™์€ ์š”์ฒญ์ด ์˜ค๋ฉด ์บ์‹ฑ๋œ ํŒŒ์ผ ๋ฐ˜ํ™˜)
1. ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „๋‹ฌ๋จ

์ด์ฒ˜๋Ÿผ **์š”์ฒญ ์‹œ์ (๋Ÿฐํƒ€์ž„)์— ์ตœ์ ํ™”**ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋””๋ฐ”์ด์Šค๋‚˜ ๋ธŒ๋ผ์šฐ์ € ๋“ฑ **ํด๋ผ์ด์–ธํŠธ์˜ ์ƒํ™ฉ์— ๋งž์ถฐ ์ตœ์ ํ™”**๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€๋Š” **์บ์‹œ์— ์ €์žฅ**๋˜์–ด ๊ฐ™์€ ์š”์ฒญ์ด ์™”์„ ๋•Œ ์ตœ์ ํ™” ๊ณผ์ • ์—†์ด ๋ฐ”๋กœ ์‘๋‹ตํ•œ๋‹ค.

### ์บ์‹œ ์ •์ฑ…

`next/image`์˜ ์ด๋ฏธ์ง€ ์บ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ •์ฑ…์œผ๋กœ ์šด์˜๋œ๋‹ค.

- **์บ์‹œ ์ €์žฅ**: ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€๋Š” `<distDir>/cache/images` ๋””๋ ‰ํ† ๋ฆฌ์— ์ €์žฅ๋จ
- **์บ์‹œ ์œ ํšจ ๊ธฐ๊ฐ„**
- `minimumCacheTTL` ์„ค์ •๊ฐ’๊ณผ ์›๋ณธ ์ด๋ฏธ์ง€์˜ `Cache-Control` ํ—ค๋” ์ค‘ **๋” ํฐ ๊ฐ’**์„ ์‚ฌ์šฉ
- ๊ธฐ๋ณธ์ ์œผ๋กœ **60์ดˆ** ๋™์•ˆ ์บ์‹œ ๋˜๋ฉฐ, `next.config.js`์—์„œ `minimumCacheTTL` ์„ค์ •์œผ๋กœ ์กฐ์ ˆ ๊ฐ€๋Šฅ
- ๋ณ„๋„์˜ ์บ์‹œ ๋ฌดํšจํ™” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— `minimumCacheTTL`์„ ๋„ˆ๋ฌด ๊ธธ๊ฒŒ ์„ค์ •ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ
- ๋งŒ์•ฝ ๊ฐ•์ œ๋กœ ๊ฐฑ์‹ ํ•˜๋ ค๋ฉด `src`๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์บ์‹œ ํŒŒ์ผ์„ ์ง์ ‘ ์‚ญ์ œํ•ด์•ผ ํ•จ

```js title="next.config.mjs"
const nextConfig = {
images: {
minimumCacheTTL: 60, // ์บ์‹œ ์œ ํšจ ๊ธฐ๊ฐ„์„ 60์ดˆ๋กœ ์„ค์ • // [!code highlight]
},
}
```

์บ์‹œ์˜ ์œ ํšจ ๊ธฐ๊ฐ„์ด ๋งŒ๋ฃŒ๋œ ์ด๋ฏธ์ง€๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ **๋งŒ๋ฃŒ๋œ(stale) ์ด๋ฏธ์ง€๋ฅผ ์ฆ‰์‹œ ์ œ๊ณต**ํ•˜๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์™€ **์บ์‹œ๋ฅผ ์—…๋ฐ์ดํŠธ**ํ•œ๋‹ค.
์บ์‹œ์˜ ์ƒํƒœ์— ๋”ฐ๋ผ `x-next-cache` ์‘๋‹ต ํ—ค๋”์˜ ๊ฐ’์ด `MISS`, `STALE`, `HIT`๋กœ ๊ฒฐ์ •๋œ๋‹ค.

- `MISS`: ์บ์‹œ์— ์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (์ตœ์ดˆ ์š”์ฒญ ์‹œ)
- `STALE`: ์บ์‹œ๋Š” ์žˆ์ง€๋งŒ ์œ ํšจ ๊ธฐ๊ฐ„์ด ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ
- `HIT`: ์œ ํšจํ•œ ์บ์‹œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ

## ๋นŒ๋“œ ํƒ€์ž„ ์ตœ์ ํ™”

๋Ÿฐํƒ€์ž„์— ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๊ฐ€ ์ง„ํ–‰๋˜์ง€๋งŒ, ๋นŒ๋“œ ์‹œ์ (๋˜๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์˜ ์ปดํŒŒ์ผ ์‹œ์ )์— ๋ฏธ๋ฆฌ ์ค€๋น„ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

```jsx
import testImage from '../public/test.png'

export default function Page() {
return (
<Image
src={testImage} // [!code highlight]
placeholder="blur"
alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€"
/>
)
}
```

์ด๋ ‡๊ฒŒ ์ •์ ์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด NextJS๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

```js
{
src: '/_next/static/media/test.5a33257e.png',
height: 743,
width: 1048,
blurDataURL: '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.5a33257e.png&w=8&q=70',
blurWidth: 8,
blurHeight: 6
}
```

๋นŒ๋“œ ์‹œ์ ์— ์ด๋ฏธ์ง€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ blur ์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— **๋Ÿฐํƒ€์ž„์— ์ถ”๊ฐ€ ๊ณ„์‚ฐ์ด ํ•„์š” ์—†๊ณ **, ๋ฏธ๋ฆฌ ์ƒ์„ฑ๋œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ `width`์™€ `height` ์†์„ฑ์„ ๋ณ„๋„๋กœ ์ „๋‹ฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

```jsx
<Image
src="/images/test.png" // [!code highlight]
width={500}
height={300}
placeholder="blur"
alt="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€"
/>
```

๋งŒ์•ฝ ์œ„์ฒ˜๋Ÿผ ๋ฌธ์ž์—ด๋กœ ์ง์ ‘ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜๋ฉด Layout Shift๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด `width`์™€ `height`๋ฅผ **๋ฐ˜๋“œ์‹œ ์ง€์ •**ํ•ด์•ผ ํ•œ๋‹ค.
๋˜ํ•œ ๋ฏธ๋ฆฌ `blurDataURL` ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ •์  import ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ `placeholder="blur"` ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด **์ฒซ ์š”์ฒญ ์‹œ์ **์— `blurDataURL`์ด ์ƒ์„ฑ๋œ๋‹ค.

## Sharp VS Squoosh

`next/image` ์ปดํฌ๋„ŒํŠธ๋Š” Sharp ๋˜๋Š” Squoosh ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ•œ๋‹ค.

```tsx title="next/src/server/image-optimizer.ts"
let showSharpMissingWarning = process.env.NODE_ENV === 'production'

if (sharp) {
// sharp ํŒจํ‚ค์ง€๊ฐ€ ์žˆ์œผ๋ฉด sharp๋กœ ์ตœ์ ํ™”
const transformer = sharp(buffer, {
sequentialRead: true,
})
// ...
} else {
// production ๋ชจ๋“œ์ด๊ณ  output: "standalone" ์ด๋ฉด Sharp ํŒจํ‚ค์ง€ ํ•„์ˆ˜
if (showSharpMissingWarning && nextConfigOutput === 'standalone') {
Log.error(
`Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly. Read more at: https://nextjs.org/docs/messages/sharp-missing-in-production`
)
throw new ImageError(500, 'Internal Server Error')
}

// production ๋ชจ๋“œ์—์„œ Sharp ํŒจํ‚ค์ง€ ๊ถŒ์žฅ
if (showSharpMissingWarning) {
Log.warnOnce(
`For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'npm i sharp', and Next.js will use it automatically for Image Optimization.\n` +
'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production'
)
showSharpMissingWarning = false
}

// Squoosh ๋ณ€ํ™˜ ๋กœ์ง
const orientation = await getOrientation(buffer)
const { processBuffer } =
require('./lib/squoosh/main') as typeof import('./lib/squoosh/main')
// ...
}
```

Sharp๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด Squoosh๋ฅผ ์‚ฌ์šฉํ•ด ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค. ๋‹จ, ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋Š” Sharp ์—†์ด๋„ ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ production ๋ชจ๋“œ์—์„œ๋Š” **Sharp ์„ค์น˜๋ฅผ ๊ถŒ์žฅ**ํ•œ๋‹ค.
ํŠนํžˆ output: "standalone" ์„ค์ •์ด ๋˜์–ด ์žˆ๋‹ค๋ฉด Sharp๊ฐ€ ํ•„์ˆ˜์ด๋ฉฐ, ์—†์„ ๊ฒฝ์šฐ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

:::warning{title="Next15๋ถ€ํ„ฐ ๋‹ฌ๋ผ์ง„ ์ "}
[NextJS 15๋ถ€ํ„ฐ๋Š” **๊ธฐ๋ณธ์ ์œผ๋กœ sharp๋ฅผ ์‚ฌ์šฉ**](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/image-optimizer.ts)ํ•˜๊ธฐ ๋•Œ๋ฌธ์— sharp๋ฅผ ์ˆ˜๋™ ์„ค์น˜ํ•  ํ•„์š” ์—†๋‹ค.
์†Œ์Šค ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์ด์ „๊ณผ ๋‹ฌ๋ฆฌ ์กฐ๊ฑด๋ฌธ ์—†์ด Sharp๋ฅผ ์‚ฌ์šฉํ•ด ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
:::

### Sharp๋ฅผ ๊ถŒ์žฅํ•˜๋Š” ์ด์œ 

[Sharp](https://github.com/lovell/sharp)๋Š” C++ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ `libvips` ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ•ด **์†๋„๊ฐ€ ๋น ๋ฅด๋‹ค**. ๋ฐ˜๋ฉด Squoosh๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰๋˜๋Š” ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋„๊ตฌ๋กœ ์‹œ์ž‘ํ–ˆ๊ณ  WebAssembly ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์ ธ ์ƒ๋Œ€์ ์œผ๋กœ ๋А๋ฆฌ๋‹ค.

`next/image`๋Š” ๋Ÿฐํƒ€์ž„์— ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋งŒํผ ์†๋„๊ฐ€ ๋น ๋ฅธ `Sharp`๋ฅผ ๊ถŒ์žฅํ•œ๋‹ค.

| | Sharp (๊ถŒ์žฅ) | Squoosh (๋Œ€์ฒด ์˜ต์…˜) |
| ---------- | ------------------------------- | ---------------------- |
| **๊ตฌํ˜„** | ๋„ค์ดํ‹ฐ๋ธŒ C++ ๊ธฐ๋ฐ˜ | WebAssembly(WASM) ๊ธฐ๋ฐ˜ |
| **์„ฑ๋Šฅ** | ๋น ๋ฆ„ | ์ƒ๋Œ€์ ์œผ๋กœ ๋А๋ฆผ |
| **๋ฆฌ์†Œ์Šค** | CPU ์‚ฌ์šฉ๋Ÿ‰๊ณผ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ด ์ข‹์Œ | CPU ๋ถ€ํ•˜๊ฐ€ ํผ |

## ์š”์•ฝ

- **๋Ÿฐํƒ€์ž„ ์ตœ์ ํ™”**: ์š”์ฒญ ์‹œ์ ์— ํด๋ผ์ด์–ธํŠธ์˜ ์ƒํ™ฉ(๋ธŒ๋ผ์šฐ์ €, ๋””๋ฐ”์ด์Šค)์— ๋งž์ถฐ ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”
- **ํฌ๋งท ๋ณ€ํ™˜**: ๋ธŒ๋ผ์šฐ์ €์˜ Accept ํ—ค๋”๋ฅผ ํ™•์ธํ•ด ์ง€์›ํ•˜๋Š” ์ตœ์‹  ํฌ๋งท(AVIF, WebP ๋“ฑ)์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜
- **์„ฑ๋Šฅ ์ตœ์ ํ™”**: Sharp๋ฅผ ์‚ฌ์šฉํ•œ ๋น ๋ฅธ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์™€ ํšจ์œจ์ ์ธ ์บ์‹ฑ ์ „๋žต์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
- **ํŠน์ˆ˜์ผ€์ด์Šค ์ฒ˜๋ฆฌ**: SVG๋‚˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฏธ์ง€(GIF ๋“ฑ)๋Š” ์ตœ์ ํ™” ๋Œ€์ƒ์—์„œ ์ œ์™ธ๋˜๋ฉฐ, unoptimized ์˜ต์…˜ ์‚ฌ์šฉ์„ ๊ถŒ์žฅ
- **๋นŒ๋“œ ํƒ€์ž„ ์ตœ์ ํ™”**: ์ •์  import๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ ํ™” ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ

## ๋งบ์œผ๋ฉด์„œ

์ง€๊ธˆ๊นŒ์ง€ NextJS์˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๊ธฐ๋Šฅ์„ ์‚ดํŽด๋ดค๋‹ค. `next/image`๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์–‘ํ•œ ์ตœ์ ํ™” ์ „๋žต์„ ์ž๋™์œผ๋กœ ์ ์šฉํ•ด ์›น ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŒ…์„ ์ž‘์„ฑํ•˜๋ฉฐ ๊ณต์‹ ๋ฌธ์„œ์™€ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ณ  ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์›๋ฆฌ์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ํ•™์Šตํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๋‹จ์ˆœํžˆ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด **๋‚ด๋ถ€ ๋™์ž‘ ์›๋ฆฌ**๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ์˜€๋‹ค.

`next/image` ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ๋ฒ•์€ [๊ณต์‹ ๋ฌธ์„œ](https://nextjs.org/docs/app/api-reference/components/image#configuration-options)์—์„œ ๋” ์ž์„ธํ•˜๊ฒŒ ์‚ดํŽด๋ณผ ์ˆ˜ ์žˆ๋‹ค.