[Next.js, Vercel] 이미지 최적화를 하며 겪은 일들

hapwoo·2023년 7월 24일
7

부끄럽게도, 아무것도 몰랐다는 변명하에...(변명안됨)
이미지를 어떻게 관리해야할지 몰랐다.
단순히 이미지를 서버로 보내고 S3에 업로드하고 기능이 굴러가게만 했었더니....

이미지 성능에 관련한 다양하고 복잡한 문제들이 생겨났다.

이미지 로딩 속도가 심각할 정도로 느렸는데....


(과거의 내가 불러온 개노답 삼형제)

내가 싼 똥들을 보며 이건 아닌데 싶은 생각이 들어, 조금이나마 숨통이 트인 지금 이를 해결하게 되었다.

다음과 같은 순서를 거쳐 이미지 최적화 과정을 거쳤다.

  1. 기존에 s3에 업로드 된 이미지 용량 압축하기
  2. 이미지 에디터 내 이미지 압축 함수 넣기
  3. Next/Image 최적화
  4. Vercel로 배포한 경우, function region 변경

1. 기존에 s3에 업로드 된 이미지 용량 압축하기

기존에는 관리자 쪽에서 시공사례를 작성 할 수 있도록 열어두었는데, 이미지 용량 제한을 걸어두지 않는,,,, 치명적인,, 실수...가 아닌... 잘못을 저질렀다.
분명 서버쪽으로 던지면서 clinet max body size 이슈가 있었음에도, 문제를 인지하지 못한 것이 부끄럽다.

쨌든 변명은 뒤로 한채, 이미 s3에 업로드된 이미지를 압축해야 했기에 s3에서 이미지를 받아와 재업로드 하는 과정을 거쳤다.

파이썬을 이용하는 방법과 노드를 이용하는 방법 두가지를 모두 테스트 해보았는데, 노드를 이용하는것이 이미지 용량 대비 가시적인 손실률이 덜한 것 같아서 그냥 js 파일 하나 파서 돌렸다.

일단 썸네일을 db에서 json으로 따와서 thumbnail.js에 저장하고, 해당 이미지를 압축 후 _compressed 를 붙여 s3 저장했다.

import thumbnails from "./thumbnail.js";
import sharp from "sharp";
import AWS from "aws-sdk";

const thumbnailList = thumbnails.map((item) => item.thumbnail);

const width = 750;
const awsConfig = {
  accessKeyId: "$accessKeyId",
  secretAccessKey: "$secretAccessKey",
  region: "ap-northeast-2",
};
const s3 = new AWS.S3(awsConfig);

const compressImageUploadByKey = async (key) => {
  try {
    const compressedKey = key.replace(".", "_compressed.");
    const config = {
      Bucket: "$Bucket",
      Key: key,
    };

    let resizedConfig = {
      Bucket: "$Bucket",
      Key: compressedKey,
    };

    // fetch
    const imageData = await s3.getObject(config).promise();

    // resizing
    const imageBuffer = await sharp(imageData.Body)
      .resize({ width: width || 640 })
      .toBuffer();
    resizedConfig.Body = imageBuffer;

    // upload
    await s3.putObject(resizedConfig).promise();

    return compressedKey;
  } catch (error) {
    console.log("Get image by key from aws: ", error);
  }
};

thumbnailList.map((key, index) => {
  console.log(`${index + 1} / ${thumbnailList.length}`);
  if (key !== "") {
    compressImageUploadByKey(key);
  } else {
    console.error(`error: ${key}`);
  }
});

그리고 db에 _compressed를 붙인 이미지 경로로 바꿔주었다.

db는 postgresql을 사용하고 있다.

update ${tableName}
set thumbnail = concat(split_part(thumbnail, '.', 1), '_compressed.', split_part(thumbnail, '.', 2))
where thumbnail is not null;

2. 이미지 에디터 내 이미지 압축 함수 넣기

일단 싸둔 똥은 1차적으로 치웠으니, 새로이 쌀 똥들을 못싸도록 틀어 막아야했다.
이미지 압축에는 browser-image-compression 라이브러리를 사용했다
https://www.npmjs.com/package/browser-image-compression
그리고 하는김에, 확장자도 webp로 다 바꿨다.

yarn add browser-image-compression
// utils.ts
import imageCompression from 'browser-image-compression';

export const compressImage = async (image: File, maxBytes: number, onProgress: (p: number) => void) => {
    const resizingBlob = await imageCompression(image, { maxSizeMB: maxBytes, fileType: 'image/webp', onProgress: onProgress });
    const resizingFile = new File([resizingBlob], image.name, { type: 'image/webp' });
    return resizingFile;
};

일단 이미지를 압축하는 함수를 만들고, 이미지 input에 업로드되면 이미지를 압축했다.
원래는 그냥 했더니 압축하는데 시간이 걸려서 로딩시간이 붕 뜨길래, imageCompression의 onProgress api를 이용해서 이미지 압축 진행률을 보여줬다.

import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import { ClipLoader } from 'react-spinners';
import styled from 'styled-components';
import theme from 'styles/theme';
import VStack from 'components/VStack';
import Text from 'components/Text';
import { compressImage, convertToBase64 } from 'utils/common';

type Props = {
    thumbnail: File;
    setThumbnail: Dispatch<SetStateAction<File | string>>;
};

const Thumbnail = ({ thumbnail, setThumbnail }: Props) => {
    const imageInput = useRef<any>(null);
    const [src, setSrc] = useState<string>('');
    const [progress, setProgress] = useState<number>(0);

    const onCickImageUpload = () => {
        imageInput.current.click();
    };
    const [isCompressing, setIsCompressing] = useState<boolean>(false);

    const onProgress = (p) => setProgress(p);

    const onChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.target.files) {
            const file = e.target.files[0];
            setIsCompressing(true);
            await compressImage(file, 0.1, onProgress).then(async (res) => {
                const src = await convertToBase64(res);
                setSrc(src as string);
                setThumbnail(res);
                setIsCompressing(false);
            });
        }
    };


    useEffect(() => {
        if (typeof thumbnail === 'string') {
            setSrc(thumbnail);
        }
    }, [thumbnail]);

    return (
        <ImageInputWrapper onClick={onCickImageUpload}>
            <ImageInput type="file" ref={imageInput} accept="image/*" onChange={onChange} />
            {isCompressing ? (
                <ImageNull>
                    <VStack space={20} items="center">
                        <ClipLoader />
                        <Text size="md" weight="rg" color={theme.color.gray2}>
                            {progress}%
                        </Text>
                    </VStack>
                </ImageNull>
            ) : thumbnail ? (
                <img src={src} width="100%" />
            ) : (
                <ImageNull>
                    <Text size="md" weight="rg" color={theme.color.gray2}>
                        클릭해서 사진을 추가해주세요.
                    </Text>
                </ImageNull>
            )}
        </ImageInputWrapper>
    );
};
export default Thumbnail;

3. Next/Image 최적화

next image 태그를 사용하고 있긴 했는데, props를 적극적으로 사용하지 않았던 것 같아, 좀 더 보완하려했다.
관련하여 참고할 수 있는 사이트가 많았다.
https://nextjs.org/docs/pages/building-your-application/optimizing/images
https://fe-developers.kakaoent.com/2022/220714-next-image/

대충 정리하자면,
1. 가능한 responsive 보다는 fixed 이미지 이용하기
2. sizes 이용하기
3. lazy loading 이용하기

4. Vercel로 배포한 경우, function region 변경

그렇게 하고나니, 로컬에서는 다음과 같이 이미지 로딩 속도가 눈에 띄게 빨라진 것을 볼 수 있었다.

10밀리초 이내로 똥을 치웠다는 안도감도 잠시..



나한테 왜이러시는거죠??

그래서 네트워크를 뜯어보았더니, 서버 응답이 이상하게 긴 것을 확인할 수 있었다.


커맨드 라인에 다음과 같은 주소를 입력하면

 nslookup {버슬에 배포한 프로젝트명}.vercel.app
Server:         168.126.63.1
Address:        168.126.63.1#53

Non-authoritative answer:
Name:   {버슬에 배포한 프로젝트명}.vercel.app
Address: 76.76.21.142

와 같이 뜨는데..
https://www.ip2location.com/demo/
주소를 검색해보니, 미국으로 뜬다.

그래서 vercel 쪽 region 관련해서 서치를 해본 결과....

https://vercel.com/docs/concepts/functions/serverless-functions/regions#override-your-default-serverless-region

function region이 있었고,
워싱턴으로 설정되어있는 것을 발견했다.
졸지에 미국인됨..

그래서 리전을 한국으로 바꾸었다.

살만해졌다..

profile
프론트 개발자

1개의 댓글

comment-user-thumbnail
2023년 7월 24일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기