개요

최근 AWSKRUG 안건환님의 이미지 최적화는 어떻게 동작하는가? Next.js는 왜 Sharp를 추천하는가 발표 영상을 보고, 이미지 최적화의 중요성을 느낄 수 있었습니다. 하지만 NextJS가 제공하는 편리함에도 불구하고, 다른 관점에서 이 방식을 생각해볼 필요가 있다고 느꼈습니다. 이번 글에서는 NextJS의 이미지 최적화 방식의 문제점과 대안에 대해 논의하고자 합니다.

발표 영상에서 라이브러리별 이미지 속도와 사이즈 최적화에 대해 다루고 있기 때문에, 이 글에서는 따로 다루지 않겠습니다.

NextJS가 제공하는 이미지 최적화의 문제점

리소스 사용 문제

NextJS는 내부적으로 squoosh 또는 sharp 라이브러리를 이용해 이미지를 최적화합니다. 유저가 웹사이트를 방문하여 이미지를 호출할 때 다음과 같은 두 번의 호출이 발생합니다:

  1. 원본 이미지 호출
  2. 최적화된 이미지 호출

이 과정에서 2번, 최적화된 이미지를 호출하기 위한 추가 리소스가 소모됩니다. 원본 이미지를 스토리지나 CDN에서 호출한 후, 최적화된 이미지를 다시 호출해야 하므로 총 두 번의 작업이 필요합니다. 물론 캐싱을 통해 리소스를 어느 정도 절약할 수 있지만, 기본적으로 한 번 더 호출이 발생합니다.

멀티 인스턴스간 최적화된 이미지 공유 문제

최적화된 이미지는 최초 호출 시 .nextimage 폴더에 저장됩니다. 그러나 멀티 인스턴스 환경에서는 각 인스턴스마다 최초 호출 시 이미지를 최적화해야 합니다. 이는 인스턴스마다 동일한 이미지를 최적화하는 불필요한 작업을 초래합니다.

  • 3000 port 일 때, 새로고침 시 304 브라우저 캐시

  • 3001 port 일 때, 동일한 이미지를 호출 하지만 캐싱이 되지 않는다.

개선 방안

이미지 최적화 옵션 끄기

NextJS의 이미지 최적화 옵션을 끄는 방법을 제안합니다. 이를 위해 next.config.js 파일에서 unoptimized: true 옵션을 설정할 수 있습니다.

next.config.json
1module.exports = {
2 images: {
3 unoptimized: true,
4 },
5};
  • 원본 이미지 한 번 호출

이 옵션을 사용하면 이미지를 호출할 때 기본 최적화 과정을 생략하여 리소스 사용을 줄일 수 있습니다.

NextJS의 Image 컴포넌트는 이미지 최적화 외에도 여러 가지 이점이 있기 때문에, img 태그를 사용하는 대신 Image 컴포넌트의 unoptimized 옵션을 사용하기로 했습니다.

멀티 인스턴스 이미지 공유 문제 해결

보통 스토리지(혹은 CDN)를 이용해 이미지를 제공하는데, 이 단계에서 이미 이미지를 공유합니다. 그렇다면 이미지를 미리 최적화한 후 스토리지에 올리면 어떨까요? 내부 CMS에서 이미지를 업로드할 때, NextJS에서 사용하는 Sharp 알고리즘을 적용해 미리 최적화한 후 저장하는 방법을 제안합니다. 이렇게 하면 NextJS 서버에서 이미지 최적화 작업을 수행할 필요가 없어집니다.

CMS 최적화 순서는 다음과 같습니다.

  1. 내부 CMS에서 이미지를 업로드
  2. 업로드 시 Sharp 알고리즘을 적용해 이미지를 최적화
  3. 최적화된 이미지를 스토리지에 저장
  4. NextJS는 스토리지에서 이미 최적화된 이미지를 호출

기존 NextJS 최적화와 CMS 최적화 비교

먼저, 원본 이미지의 사이즈는 1MB이고, NextJS가 진행한 최적화 이미지의 사이즈는 33.9KB입니다.

upload/index.js
1const express = require('express');
2const path = require('path');
3const sharp = require('sharp');
4const fs = require('fs');
5const router = express.Router();
6
7const originFilePath = path.join(__dirname, '원본 이미지.png');
8const outputFilePath = path.join(__dirname, '최적화 이미지.webp');
9
10router.post('/uploads', async (req, res) => {
11 const originalImage = sharp(originFilePath);
12 const originImageState = fs.statSync(originFilePath);
13 const originImageSize = originImageState.size / 1024;
14 console.log('[최적화 전 사이즈]', `${originImageSize.toFixed(2)} KB`); // 1KB
15
16
17 // 이미지 최적화
18 await originalImage.webp({ quality: 75 }).toFile(outputFilePath);
19
20 const outputImageState = fs.statSync(outputFilePath);
21 const outputImageSize = outputImageState.size / 1024;
22 console.log('[최적화 후 사이즈]', `${outputImageSize.toFixed(2)} KB`); // 38KB
23
24 res.sendFile(outputFilePath);
25});

위 코드는 CMS에서 Sharp 알고리즘을 적용한 간단한 API 코드입니다. 이 코드에서는 NextJS에서 지정한 quality를 75로 동일하게 설정했습니다.

위 코드의 실행 결과, 최적화된 이미지의 사이즈는 38KB로 나타났습니다. NextJS의 최적화 알고리즘과 정확히 동일한 로직을 사용하지 않았기 때문에 33.9KB와 약간의 차이가 있지만, 원본 이미지의 사이즈를 고려하면 이 차이는 미미합니다.

마무리

각 서비스의 클라우드 트래픽을 보다가 문득, CMS는 최소 스펙으로 배포해도 리소스가 여유로운데 NextJS의 이미지 최적화 작업을 CMS에서 대신하면 어떨까 하는 생각이 들었습니다. 이러한 아이디어를 바탕으로 이 프로젝트를 진행하게 되었습니다. 개발 초기 단계에서는 NextJS가 자동으로 제공하는 이미지 최적화 기능을 활용하는 것이 편리할 수 있습니다. 그러나 서비스가 성장함에 따라 리소스 효율성을 고려한다면, CMS에서 이미지 최적화를 수행하는 방식을 시도해 볼 만한 것 같습니다.

번외

디자이너님들이 사용하는 피그마 플러그인을 이용해 이미지를 최적화하면, 이미지 사이즈가 41.3KB까지 줄어듭니다(quality 90 사용). 디자이너가 직접 이미지를 업로드할 수 있는 환경이라면 이 방법을 적용하는 것도 좋을 것 같습니다.

참고

  1. 이미지 최적화는 어떻게 동작하는가? Next.js는 왜 Sharp를 추천하는가
  2. NextJS Image Optimization