0. 배경
위치 추적 게임 서비스, 어데고?! 의 핵심 부분인 콘텐츠 부분을 전담하고 있다. 사용자가 이미지를 업로드한 후, 미리 보기로 업로드한 이미지들을 보여주는 것부터, 올린 전체 장소 조회, 게임에서 콘텐츠로 활용 등 서비스 전체적으로 이미지가 필수적으로 존재해야 한다.
1차 MVP에선 사용자가 업로드한 이미지를 그대로 S3에 저장하고, url로 해당 이미지를 불러와 사용했다. 크기가 작은 이미지를 저장하고 사용하는 것에 문제가 되지 않았으나, 크기가 큰 이미지를 저장하고 불러올 때 서비스에 지연 시간이 생기는 것을 확인했다. 이러한 지연은 사용자에게 예상보다 큰 불편함을 느끼게 하고, 서비스가 안정적이지 못하다는 인상을 남기게 되는 악영향을 초래하기에 심각성을 인지하고 곧바로 수리에 돌입했다.
문제의 원인은 이미지의 크기가 크다는 것이었고, 이미지를 업로드할 때 이미지를 최적화한다면 모든 조회가 매끄러워질 것이라는 판단을 내렸다.
이미지 최적화를 위해 Next/image와 sharp 라이브러리를 모두 사용하여, 할 수 있는 최대한 최적화를 진행하고자 했다.
1. 이미지 업로드 최적화 방법
- Next/image 사용
- sharp 라이브러리 사용
- 이미지 압축: webp로 변환
- 이미지 크기 제한: 800px 이하로 제한
2. Next/image 사용
NextJS의 경우, img 태그가 아닌 Next/image를 활용한 Image 태그를 사용한다면 일차적으로 이미지를 최적화할 수 있다. 해당 태그를 사용했을 때 서버가 동작하게 되면, /_next/image라는 라우트를 만들어 이미지 최적화 모듈을 사용해 자동으로 이미지를 최적화하게 된다.
하지만 NextJS가 모든 경우의 이미지를 최적화해주는 것은 불가능하고 동적으로 최적화를 진행해야 하는 경우, sharp를 통해 이차적으로 최적화를 진행할 수 있다.
import Image from 'next/image';
<Image src={file} alt="Preview Image" fill style={{ objectFit: 'cover' }} />;
3. sharp 라이브러리 사용
(0) 환경
sharp 라이브러리의 경우, node 기반의 라이브러리이기에 클라이언트에서 사용할 수 없다. 그렇기에 Next의 Server에서 이미지 최적화를 진행하게 된다.
(1) 기본 구조
import { NextResponse } from 'next/server';
import sharp from 'sharp';
export async function POST(req: Request) {
try {
const formData = await req.formData();
const file = formData.get('file') as Blob;
/* **************** */
/* 로직이 처리될 부분 */
/* **************** */
return new Response(compressedBuffer, {
headers: {
'Content-Type': 'image/webp',
},
});
} catch (error) {
console.error('이미지 처리 중 에러:', error);
return NextResponse.json({ error: '이미지 처리 실패' }, { status: 500 });
}
}
(2) 이미지 압축하기
const compressedBuffer = await sharp(buffer)
.rotate()
.withMetadata()
.webp({ quality: 80 })
.toBuffer();
(3) 이미지 크기 제한하기
- 가로/세로에 따른 크기 제한 옵션 설정
- 가로가 세로보다 긴 경우, 가로를 800px로 제한
- 세로가 가로보다 긴 경우, 세로를 800px로 제한
const metadata = await sharp(buffer).rotate().metadata();
let resizeOptions = {};
if (metadata.width && metadata.height) {
if (metadata.width > metadata.height) {
// 가로가 세로보다 길 경우
resizeOptions = { width: 800, withoutEnlargement: true };
} else {
// 세로가 가로보다 길 경우
resizeOptions = { height: 800, withoutEnlargement: true };
}
}
- sharp에 크기 제한 옵션 적용
const compressedBuffer = await sharp(buffer)
.rotate()
.resize(resizeOptions)
.withMetadata()
.toBuffer();
4. 활용
(0) 이미지 최적화 적용 화면 및 PR
Feat/#17: 이미지 최적화 구현(이미지 업로드하는 경우) by minjeongss · Pull Request #31 · urdego/Urdego_Front
#️⃣ 연관된 이슈 #17 📝 작업 내용 컨텐츠 크기 제한 가로가 세로보다 긴 경우, 가로를 800px로 제한 세로가 가로보다 긴 경우, 세로를 800px로 제한 컨텐츠 확장자 webp로 변환 📸 성능 향상 이미
github.com
- 이미지 최적화 활용 화면



(1) 사용자가 이미지 업로드할 때, Next Server로 이미지 최적화 요청 및 활용
- 코드 위치: hooks/useRegisterFiles.ts
const compressFile = async (fileList: File[]) => {
const compressedFileList: File[] = [];
for (const file of fileList) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/content/compress', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('파일 압축 실패');
}
const compressedBlob = await response.blob();
const fileNameToWebp = file.name.split('.')[0] + '.webp';
const compressedFile = new File([compressedBlob], fileNameToWebp, {
type: 'image/webp',
});
compressedFileList.push(compressedFile);
}
return compressedFileList;
};
(2) Next Server에서 이미지 최적화 진행
- 코드 위치: app/api/content/compress/route.ts
import { NextResponse } from 'next/server';
import sharp from 'sharp';
export async function POST(req: Request) {
try {
const formData = await req.formData();
const file = formData.get('file') as Blob;
// file 객체를 Buffer로 변환
const buffer = Buffer.from(await file.arrayBuffer());
// 가로/세로에 따른 옵션 설정
const metadata = await sharp(buffer).rotate().metadata();
let resizeOptions = {};
if (metadata.width && metadata.height) {
if (metadata.width > metadata.height) {
// 가로가 세로보다 길 경우
resizeOptions = { width: 800, withoutEnlargement: true };
} else {
// 세로가 가로보다 길 경우
resizeOptions = { height: 800, withoutEnlargement: true };
}
}
// Sharp로 이미지 압축
const compressedBuffer = await sharp(buffer)
.rotate()
.resize(resizeOptions)
.withMetadata()
.webp({ quality: 80 })
.toBuffer();
return new Response(compressedBuffer, {
headers: {
'Content-Type': 'image/webp',
},
});
} catch (error) {
console.error('이미지 처리 중 에러:', error);
return NextResponse.json({ error: '이미지 처리 실패' }, { status: 500 });
}
}
5. 성능 개선 결과
(1) 이미지의 크기: 20배 감소
- 적용 전: 2,500,000Byte
- 적용 후: 120,000Byte

(2) 이미지 불러오는 시간: 40배 감소
시크릿 창에서 캐시 사용 중지를 설정하고 테스트한 환경이다.
- 적용 전: 44ms
- 적용 후: 2ms

6. 결론
Next/image와 sharp 라이브러리를 총동원하여 클라이언트에서 사용자가 이미지를 업로드했을 때의 최적화를 진행했다. 예상보다 더 뛰어난 성능 개선을 이룰 수 있어 뿌듯했다. 배포된 서비스를 사용할 때 이미지를 조회하는 로직이 빨라짐을 체감하며, 서비스의 질을 높이는데 기여했다는 생각이 들었다.
이미지를 최적화하는 방법은 각 서비스에서 집중하고 있는 부분이 무엇인지와 불편함을 느끼는 것의 원인이 무엇인지를 분석하는 것에 있다고 생각한다. 원인을 깊이 파고들어, 진정한 문제를 찾아 최적화를 진행하는 것이 올바른 접근 방법이 아닐까 싶다. 😀👍
7. 참고한 블로그
'프로그래밍 - 활용 > Front-end' 카테고리의 다른 글
| NEXT.js에서 dynamic import 사용하기 (0) | 2025.02.23 |
|---|---|
| 리액트의 Virtual DOM 동작 원리 분석기: Fiber Reconciler편 (0) | 2025.01.24 |
| 객체의 불변성 유지법: Javascript, React편 (2) | 2025.01.18 |
| NPM에 라이브러리 배포하기: usePortal편 (0) | 2025.01.15 |