이미지 최적화는 웹 성능의 기본이다. 특히 이미지가 핵심인 서비스라면 더욱 그렇다.
pixelDiff를 만들면서 이미지를 다루는 일이 많았다. Figma에서 4K 해상도로 export한 PNG를 저장하고, 목록에서 썸네일로 보여주고, 로딩 중에는 blur placeholder를 띄워야 했다. 매번 원본 이미지를 그대로 쓸 수는 없었다.
Node.js에서 이미지 처리를 한다면 Sharp가 사실상 표준이다. libvips 기반이라 빠르고, API도 직관적이다.
Sharp가 뭘까
Sharp는 Node.js용 고성능 이미지 처리 라이브러리다. ImageMagick이나 GraphicsMagick보다 4-5배 빠르다고 알려져 있다.
왜 이렇게 빠를까? Sharp가 사용하는 libvips의 아키텍처 덕분이다.
ImageMagick은 이미지 전체를 메모리에 올린 뒤 처리한다. 4K 이미지라면 수십 MB가 통째로 메모리에 올라간다. 반면 libvips는 "demand-driven" 방식이다. 이미지를 작은 타일 단위로 쪼개서, 실제로 필요한 부분만 메모리에 올려 처리한다. 리사이징할 때 전체 픽셀을 다 읽을 필요가 없으니 당연히 빠르다.
거기다 libvips는 멀티스레드를 자동으로 활용하고, CPU의 SIMD 명령어로 벡터 연산까지 한다. JavaScript가 아닌 C/C++ 네이티브 코드로 실행되니 오버헤드도 적다.
이런 복잡한 최적화를 신경 쓸 필요 없이, 그냥 설치하고 쓰면 된다:
npm install sharp
# 또는
pnpm add sharp
기본 사용법은 이렇다:
import sharp from 'sharp';
// 리사이징
await sharp(buffer)
.resize(600, null) // 너비 600px, 높이는 비율 유지
.toBuffer();
// 포맷 변환
await sharp(buffer)
.webp({ quality: 75 })
.toBuffer();
WebP를 써야 하는 이유
WebP는 구글이 만든 이미지 포맷이다. 같은 화질에서 JPEG보다 25-34%, PNG보다 26% 정도 작다.
2024년 기준 모든 주요 브라우저가 지원한다. IE 11만 아니면 문제없다.
| 포맷 | 용도 | 압축률 |
|---|---|---|
| JPEG | 사진, 복잡한 이미지 | 손실 압축 |
| PNG | 투명도 필요, 단순한 이미지 | 무손실 |
| WebP | 둘 다 대체 가능 | 손실/무손실 선택 |
썸네일 생성
목록 페이지에서 4K 원본을 그대로 보여줄 필요는 없다. 600px 너비면 충분하다.
interface ThumbnailOptions {
width?: number; // 기본 600px
maxHeight?: number; // 기본 1200px
quality?: number; // 기본 75
format?: 'webp' | 'png' | 'jpeg';
}
async function generateThumbnail(
buffer: Buffer,
options: ThumbnailOptions = {}
): Promise<Buffer> {
const {
width = 600,
maxHeight = 1200,
quality = 75,
format = 'webp',
} = options;
// 1단계: 너비 기준으로 리사이징
let resizedBuffer = await sharp(buffer)
.resize(width, null, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate() // EXIF orientation 자동 적용
.toBuffer();
// 2단계: 높이가 너무 길면 중앙 크롭
const metadata = await sharp(resizedBuffer).metadata();
if (metadata.height && metadata.height > maxHeight) {
const top = Math.floor((metadata.height - maxHeight) / 2);
resizedBuffer = await sharp(resizedBuffer)
.extract({
left: 0,
top,
width: metadata.width!,
height: maxHeight,
})
.toBuffer();
}
// 3단계: WebP로 변환
return sharp(resizedBuffer)
.webp({ quality, effort: 4 })
.toBuffer();
}

- 몇 가지 포인트가 있다.
`withoutEnlargement: true`
원본보다 크게 확대하지 않는다. 300px 이미지를 600px로 늘리면 화질만 나빠진다.
`rotate()`
스마트폰으로 찍은 사진은 EXIF에 방향 정보가 들어있다. rotate()를 호출하면 Sharp가 알아서 처리한다. 인자 없이 호출하면 EXIF 기준으로 자동 회전한다.
`effort: 4`
WebP 압축 노력도다. 0-6 사이 값인데, 높을수록 파일이 작아지지만 인코딩이 느려진다. 4가 적당한 균형점이다.
Blur Placeholder 생성
Next.js Image 컴포넌트는 blurDataURL prop을 받는다. 이미지 로딩 중에 흐릿한 미리보기를 보여주는 기능이다.
async function generateBlurPlaceholder(
buffer: Buffer,
options: { width?: number; quality?: number } = {}
): Promise<string> {
const { width = 10, quality = 50 } = options;
const blurBuffer = await sharp(buffer)
.resize(width, null, {
fit: 'inside',
withoutEnlargement: true,
})
.jpeg({
quality,
progressive: false,
})
.toBuffer();
const base64 = blurBuffer.toString('base64');
return `data:image/jpeg;base64,${base64}`;
}
10x10 픽셀이면 충분하다. 어차피 CSS로 blur 처리되어 보이기 때문이다.
WebP 대신 JPEG를 쓰는 이유는 호환성이다. data URL로 인라인되기 때문에 브라우저 지원 범위가 넓을수록 좋다.
무손실 압축
pixelDiff는 픽셀 단위 비교 서비스다. 썸네일과 달리 원본 이미지는 픽셀이 정확히 보존되어야 한다.
WebP는 무손실 모드도 지원한다:
async function convertToWebPLossless(buffer: Buffer): Promise<Buffer> {
return sharp(buffer)
.webp({
lossless: true,
effort: 4,
})
.toBuffer();
}
PNG 대비 약 26% 파일 크기가 줄어든다. 픽셀은 그대로 보존되면서.
실제 적용 결과
Figma에서 export한 4K PNG 기준으로 측정했다.
| 처리 | 용량 변화 | 용도 |
|---|---|---|
| 원본 PNG → WebP Lossless | -26% | 픽셀 비교용 |
| 원본 → 600px WebP | -85% | 목록 썸네일 |
| 원본 → 10px JPEG | -99.9% | Blur placeholder |
썸네일의 경우 리사이징과 압축이 함께 적용되어 용량 절감이 크다.
주의할 점
Sharp는 서버에서만 동작한다
Sharp는 네이티브 모듈이다. 브라우저에서는 쓸 수 없다. 클라이언트에서 이미지 처리가 필요하면 Canvas API나 브라우저 내장 ImageBitmap을 써야 한다.
Docker 빌드 시 플랫폼 주의
M1 Mac에서 빌드한 이미지를 Linux 서버에서 실행하면 Sharp가 동작하지 않을 수 있다. Docker 빌드 시 --platform linux/amd64 옵션을 명시하거나, multi-stage 빌드에서 타겟 플랫폼에 맞게 설치해야 한다.
메모리 사용량
큰 이미지를 처리할 때 메모리를 많이 쓴다. 4K 이미지 한 장이 메모리에서 수십 MB를 차지할 수 있다. 동시에 여러 이미지를 처리한다면 병렬 처리 수를 제한하는 게 좋다.
이미지 최적화는 복잡할 필요가 없다. Sharp와 WebP 조합이면 대부분의 요구사항을 커버할 수 있다.
- 썸네일: 리사이징 + WebP 손실 압축
- 원본: WebP 무손실 압축
- Placeholder: 극소 해상도 + JPEG
용량은 줄이고, 사용자 경험은 높이고. 서버 비용도 절약된다.
프런트엔드 엔지니어, QA 엔지니어 그리고 디자이너를 위한
" ALL IN ONE " QA 서비스
https://pixeldiff.turtle-tail.com
'FrontEnd' 카테고리의 다른 글
| next-intl 없이 i18n 직접 만들기 (0) | 2026.02.07 |
|---|---|
| iframe 키 이벤트 재합성으로 Cross-Origin 우회하기 (0) | 2026.02.05 |
| S3 Presigned URL로 클라이언트 직접 업로드 (0) | 2026.01.22 |
| 풀페이지 스크린샷 타일 스티칭 구현 (0) | 2026.01.20 |
| Pixi.js 줌 레벨별 UI 표시/숨김 구현하기 (0) | 2026.01.17 |
