문제 해결 & 구현 기록

모멘트 서비스 이미지 로딩 속도 개선기 (WebP, Sharp, AWS Lambda)

sangchu 2025. 12. 24. 14:57

도입

모멘트 서비스에서 글 작성 시 이미지를 업로드할 수 있도록 기능을 추가했다. 하지만 기능을 추가하고 배포한 이후, 사용자에게 이미지 로딩 속도가 느리다는 피드백을 여러 번 받았다. 사실 기존 정적 이미지(레벨, 아이콘 등) 역시 체감상 느린 상태였다.

 

예전이라면 '사용자의 폰 사양이 낮아서, 네트워크 문제여서'라고 생각했겠지만, 웹 성능 최적화에 대해 학습한 이후 관점이 달라졌다.

단순 사용자의 네트워크 속도가 느리다고 끝낼게 아니라, 개발자가 개선할 수 있는 영역이었다.

 

이 글에서는 정적 이미지와 사용자 업로드 이미지를 어떻게 최적화했는지, 그리고 그 과정에서 겪은 고민과 해결책을 공유하고자 한다.

 

문제 상황

앞서 말했듯 서비스 론칭 후 “이미지 로드 속도가 너무 느리다”라는 피드백을 많이 받았다.

정적 이미지(아이콘)
사용자가 업로드한 동적 이미지



1. Lighthouse 측정

먼저 Lighthouse 측정을 해봤다.

Lighthouse는 Chrome 개발자 도구에 내장된 성능 측정 도구로, 웹페이지의 로딩 속도, 접근성, SEO 등을 분석해준다.

 

랜딩 페이지를 분석해보니 Performance 점수가 76점으로 개선이 필요한 수준이었다.

(일반적으로 90점 이상이면 양호한 편이며, 디바이스 성능에 따라 점수가 달라질 수 있다)

 

총 5가지의 측정 항목이 있는데 그 중에서도 LCP(Largest Contentful Paint)의 점수가 상당히 안좋다.

LCP는 페이지의 주요 콘텐츠가 화면에 표시되는 시간을 측정하는 지표다. 이미지가 클수록 LCP가 길어져 체감 속도가 느려진다.

 

내려보면 어느 부분에서 점수가 안좋게 나오는건지 리포트를 볼 수 있다. 대부분 images에 대한 내용이었다.

 

오른쪽 토글을 누르면 더 세부적으로 볼 수 있는데, 해당 부분은 캡쳐하지 못해서, 다른 예시 사진으로 보면 다음과 같다.

이로써 이미지 크기가 너무 커서 웹 성능에 영향이 갔다는 것을 1차적으로 확인 할 수 있었다.

 

2. 네트워크 탭 확인

또한 네트워크 탭에서 img 요청을 확인해봤다.

waterfall이라는 부분이 보이는데, 서버 응답(Waiting for server)은 빠르게 오지만, Content download하는데 1.76s나 걸리는걸 확인할 수 있었다.

이미지 행을 클릭해 자세히 봤는데, 이미지 사이즈가 2048x2048 였고, 용량도 2.3MB로 컸다.

현재 해당 이미지는 실제 웹 페이지에서는 40x40로 그려지는 이미지였다. 그에 비해 이미지 사이즈가 불필요하게 큰 상황이었다.

 

정적 이미지 최적화

앞서 Lighthouse와 네트워크 탭을 통해, 이미지 리소스가 웹 성능에 가장 큰 영향을 주고 있다는 것을 확인했다.

이에 따라 먼저 정적 이미지 최적화를 진행했다. 정적 이미지는 개수가 많지 않았기 때문에, 빌드 파이프라인을 구성하기보다는 Squoosh를 사용해 직접 최적화 작업을 수행했다. 이 과정에서 아래의 세 가지를 적용했다.

 

1. 이미지 확장자 webp로 변환

이미지 확장자 수정을 결심한 이유

기존 정적 이미지들은 대부분 PNG 포맷으로 관리되고 있었다.

PNG는 무손실 포맷이기 때문에 품질은 뛰어나지만, 그만큼 파일 용량이 크다는 단점이 있다.

Lighthouse 리포트에서도 “Serve images in modern formats” 경고가 있었고, 이는 이미지 포맷이 성능 문제와 직접적으로 연결되어 있음을 의미했다.

 

이미지 확장자 종류

웹에서 주로 사용하는 이미지 포맷은 다음과 같다. 우리는 이 중 WebP 확장자를 선택했다.

확장자 용량 효율 특징 주요 사용처
JPEG (.jpg) 중간 손실 압축 사진, 배경 이미지
PNG (.png) 낮음 무손실 압축 로고, 아이콘, UI 이미지
WebP (.webp) 높음 손실 / 무손실 압축 선택 가능 사진, 아이콘, 썸네일, 애니메이션

webp로 선택한 이유

WebP를 선택한 이유는 다음과 같다.

  • 같은 품질 기준에서 JPEG, PNG보다 더 작은 용량을 제공한다.
  • 손실 / 무손실 압축을 모두 지원해 이미지 특성에 따라 압축 방식을 선택할 수 있다.

이러한 특성 덕분에 이미지 성격에 따라 JPEG, PNG, GIF 등으로 포맷을 나누어 관리할 필요 없이, 하나의 확장자를 기준으로 일관된 이미지 최적화 전략을 적용할 수 있다.

즉, 가장 높은 압축 효율을 기본값으로 유지하면서도 손실 여부를 옵션으로 조절할 수 있어, 관리 복잡도를 줄이면서 웹 성능 개선을 동시에 달성할 수 있다고 판단해 WebP를 선택했다.

 

또한 WebP는 Chrome, Edge, Firefox, Safari 등 대부분의 최신 브라우저에서 이미 지원되고 있어, 실서비스에서 사용하기에 충분한 호환성을 갖추고 있다.

 

구형 브라우저 고려

다만, 일부 구형 브라우저에서는 WebP를 지원하지 않는 경우가 있기 때문에, 이를 고려한 fallback 전략이 필요했다. 이를 위해 아래 코드와 같이 <picture> 태그를 사용해 WebP를 우선 제공하고, 미지원 브라우저에서는 PNG를 fallback으로 제공하도록 구현했다.

<picture>
  <source srcset="/images/example.webp" type="image/webp" />
  <img src="/images/example.png" alt="example image" />
</picture>

이 방식은 다음과 같이 동작한다.

  • 브라우저 호환성: WebP를 지원하지 않는 브라우저에서는 자동으로 PNG를 사용
  • 성능 최적화: WebP를 지원하는 브라우저에서는 높은 압축률의 WebP를 그대로 활용

이를 통해 구형 브라우저 대응과 최신 브라우저 성능 최적화를 동시에 만족할 수 있다.

 

2. 이미지 사이즈 resize

이미지 포맷을 WebP로 변경하더라도, 이미지 해상도가 실제 사용 크기에 비해 지나치게 크다면 성능 문제는 여전히 남는다.

실제로 네트워크 탭을 통해 확인한 이미지 중에는 다음과 같은 사례가 있었다.

  • 실제 화면 렌더링 크기: 40 × 40
  • 원본 이미지 크기: 2048 × 2048

즉, 브라우저는 실제 화면에 필요한 크기보다 훨씬 큰 원본 이미지를 먼저 모두 다운로드한 뒤, 이를 화면 크기에 맞게 축소해서 렌더링하고 있었다.

이 문제를 해결하기 위해, 이미지를 실제 사용 크기에 맞게 resize를 진행했다.

 

3. 이미지 압축

이미지 포맷을 WebP로 변경하고, 해상도를 실제 사용 크기에 맞게 resize하는 것만으로도 상당한 개선 효과를 얻을 수 있었다.

하지만 같은 해상도의 이미지라도 압축 방식과 압축 강도에 따라 파일 용량은 더 줄일 수 있다.

 

이미지 압축에는 크게 손실 압축과 무손실 압축이 있으며, 이 중 손실 압축은 이미지 정보를 일부 제거하는 방식이다. 이 방식은 사진처럼 색 변화가 자연스럽고 세부 디테일이 많은 이미지에서는 큰 용량 절감 효과를 얻을 수 있다.

 

반면, 아이콘이나 로고처럼 경계가 또렷하고 픽셀 하나하나가 중요한 이미지에 손실 압축을 적용하면, 경계가 흐려지거나 미세한 블러가 발생해 시각적인 품질 저하로 이어질 수 있다.

 

즉, 압축을 무조건 적용하는 것이 아니라, 이미지의 용도와 시각적 특성을 기준으로 손실 여부를 구분하는 것이 중요하다.

따라서 이미지의 성격에 따라 압축 기준을 다음과 같이 나누어 적용했다.

 

  1. 사용자 갤러리에서 업로드한 사진 이미지
    • 손실 압축 적용
    • 용량 감소 효과가 크고, 사용자가 품질 저하를 인지하기 어려움
  2. 아이콘, 로고, UI 이미지
    • 무손실 압축 적용
    • 이미지 정보를 유지해 선명도를 보존

 

정적 이미지 최적화 결과

정적 이미지 최적화 이후, 네트워크 요청 기준으로 가장 큰 변화를 확인할 수 있었다.

먼저 파일 사이즈를 비교해보면 단일 이미지 기준으로 약 800배 이상 용량이 감소했다.

  • 2059 KB → 2.5 KB

 

또한 네트워크 탭에서 확인한 Content Download 시간 역시 크게 개선되었다.

  • 1.76s (1760ms) → 2.62ms

기존에는 이미지 한 장을 다운로드하는 데에만 1초 이상이 소요되었지만, 최적화 이후에는 0.0026초로 줄어든 것을 확인할 수 있다.

최적화 이전
최적화 후

이러한 변화는 단순히 수치상의 개선에 그치지 않았다. 실제로 화면에서도, 이전과 달리 이미지가 즉시 로드되는 것처럼 느껴질 만큼 체감 성능이 개선되었다.

즉, 불필요하게 큰 이미지 리소스를 제거하고 화면에 필요한 크기와 용량만 제공함으로써, 이미지 로딩이 사용자 경험에 영향을 주지 않는 상태를 만들 수 있었다.

아이콘 이미지가 체감상 즉시 로드되는 상태

 

사용자가 업로드한 이미지 최적화

기존 이미지 업로드 방식

기존의 이미지 업로드 흐름은 다음과 같았다.

  1. 사용자가 이미지 업로드를 요청한다.
  2. 프론트엔드에서 서버에 이미지 업로드용 URL을 요청한다.
  3. 서버는 S3에 이미지를 업로드할 수 있는 Presigned URL을 생성해 반환한다.
  4. 클라이언트는 해당 URL로 이미지를 직접 S3에 업로드한다.
  5. 이미지 업로드가 완료된다.

이 방식은 일반적인 S3 기반 이미지 업로드 구조로, 서버 부하를 줄이면서도 안정적으로 이미지를 저장할 수 있다는 장점이 있다.

다만 이 구조에서는 사용자가 업로드한 이미지가 그대로 S3에 저장되기 때문에, 이미지 크기나 포맷에 대한 제어가 어렵다는 한계가 있었다.

 

해결 방법 – AWS Lambda 기반 이미지 최적화

정적 이미지와 달리, 사용자가 업로드하는 이미지는 사전에 최적화할 수 없다.

런타임 중에 업로드되는 이미지이기 때문에 빌드 타임에 처리할 수도 없고, 크기와 포맷도 제각각이다.

이미지 최적화를 어디서 수행할지 고민했다. 각 방식의 장단점은 다음과 같았다.

  • 프론트엔드 처리: 사용자 기기에서 처리하므로 기기 성능에 영향을 받음
  • 서버 직접 처리: 모든 이미지가 서버를 거치게 되어 서버 트래픽과 비용 증가
  • 업로드 후 비동기 처리: 기존 구조 유지 + 자동 최적화 가능 ✅

결국 S3 업로드 이벤트를 트리거로 Lambda를 실행하는 방식을 선택했다.

이 방식은 다음과 같이 동작한다.

  1. 사용자가 기존과 동일하게 S3에 이미지를 업로드한다
  2. 업로드 완료 시 S3 이벤트가 Lambda를 자동 실행한다
  3. Lambda에서 이미지 최적화(WebP 변환, resize, 압축)를 수행한다
  4. 최적화된 이미지를 별도의 S3 버킷에 저장한다

기존 업로드 구조를 변경하지 않으면서도, 정적 이미지 최적화에서 사용한 전략(WebP, resize, 압축)을 동일하게 적용할 수 있다는 점에서 가장 적합한 해결책이라고 판단했다.

 

세팅 과정

1. 이미지 전용 S3 버킷 분리

기존에는 이미지가 하나의 S3 버킷에 그대로 저장되고 있었다.

여기에 최적화된 이미지까지 함께 관리하면 구조가 복잡해질 수 있다고 판단해, 다음과 같이 버킷을 분리해 구성했다.

  • 원본 이미지 저장용 S3 버킷
  • 최적화된 이미지 저장용 S3 버킷

이를 통해 원본 이미지 보존하면서도, 최적화 결과를 명확하게 분리해 관리할 수 있게 되었다.

 

2. Lambda 함수 생성 및 트리거 연결

이미지 변환을 담당하는 Lambda 함수(moment-image-converter)를 생성하고, 원본 이미지 S3 버킷에 Object Created 이벤트 트리거를 연결했다.

즉, 사용자가 이미지를 업로드하면, 별도의 추가 요청 없이 자동으로 Lambda가 실행되도록 구성했다.

 

 

3. 이미지 변환 로직 구현

Lambda 내부에서는 sharp 라이브러리를 사용해 다음 작업을 수행했다. 이는 앞서 정적 이미지 최적화에서 적용했던 과정과 동일하다.

  • 이미지 resize
  • WebP 변환
  • 압축 처리

이 로직을 람다 코드 소스에 넣고 Deploy를 하면 된다.

 

구현 시에는 올리브영 기술 블로그의 Lambda 이미지 리사이징 예제를 참고했고, 우리 프로젝트 구조에 맞게 저장할 S3 디렉토리 구조만 일부 수정해 사용했다.

 

실제로 많은 이미지 최적화 관련 레퍼런스가 이 코드를 기반으로 작성되어 있어, 안정성과 신뢰성 측면에서도 참고하기에 적합했다.

Lambda 환경 설정과 sharp 모듈 빌드 과정은 내용이 길어질 수 있어 이 글에서는 생략했다. 해당 부분은 위에서 참고한 올리브영 기술 블로그에 잘 정리되어 있으므로, 자세한 설정 과정은 해당 글을 참고하면 좋을 것 같다.

 

Lambda 최종 코드

const { S3Client, GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3");
const sharp = require("sharp");

const s3 = new S3Client({ region: "ap-northeast-2" });

exports.handler = async (event) => {
  // 1. S3 이벤트에서 버킷과 파일 정보 추출
  const bucket = event.Records[0].s3.bucket.name;
  const key = decodeURIComponent(
    event.Records[0].s3.object.key.replace(/\\+/g, " ")
  );

  // 2. 무한 루프 방지: 이미 최적화된 이미지는 건너뜀
  if (key.startsWith("upload-resize/")) {
    return;
  }

  // 3. 파일명에서 확장자 제거하고 .webp로 변경
  const fileName = key.split("/").pop();
  const baseFileName = fileName.split(".").slice(0, -1).join(".");
  const dstKey = `upload-resize/${baseFileName}.webp`;

  try {
    // 4. S3에서 원본 이미지 가져오기
    const response = await s3.send(
      new GetObjectCommand({ Bucket: bucket, Key: key })
    );
    const stream = response.Body;
    const content_buffer = Buffer.concat(await stream.toArray());

    // 5. sharp를 사용해 이미지 최적화
    const output = await sharp(content_buffer)
      .resize(800, 800, { fit: "inside" }) // 최대 800x800으로 리사이즈
      .webp({ quality: 80 }) // 손실 압축 (quality 80)
      .toBuffer();

    // 6. 최적화된 이미지를 새 경로에 저장
    await s3.send(
      new PutObjectCommand({
        Bucket: bucket,
        Key: dstKey,
        Body: output,
        ContentType: "image/webp",
      })
    );

    console.log("Successfully resized and uploaded");
  } catch (error) {
    console.error("Error processing file", error);
  }
};

 

사용자 업로드 이미지 최적화 결과

기존 이미지 버킷(images/)을 확인해보면, 업로드된 이미지의 확장자가 jpeg, jpg, png 등으로 제각각이었고, 이미지 크기 또한 대부분 2MB 내외로 상당히 큰 것을 확인할 수 있었다. 간혹 수십 MB를 넘어 수백 MB, 심지어 1GB에 가까운 이미지가 업로드되는 경우도 있었다.

이러한 이미지들은 네트워크 비용뿐만 아니라, 로딩 성능 측면에서도 큰 부담이 될 수밖에 없는 상태였다.

 

반면, Lambda를 통해 최적화된 이미지를 저장하는 버킷(images-resize/)을 살펴보면

모든 이미지가 WebP 확장자로 통일되어 있었고, 이미지 크기 또한 평균 약 7KB 수준으로 크게 줄어든 것을 확인할 수 있었다.

이는 정적 이미지 최적화에서 적용했던 전략(WebP 변환, resize, 압축)을 사용자 업로드 이미지에도 동일하게 적용한 결과다.

 

 

결과적으로 얻은 효과를 정리하자면,

  • 이미지 포맷 통일(WebP)
  • 업로드 이미지 용량 대폭 감소
  • 네트워크 다운로드 비용 감소
  • 이미지 로딩이 UX 병목이 되지 않는 상태 확보

즉, 사용자가 어떤 크기의 이미지를 업로드하더라도 서비스에서는 항상 최적화된 이미지가 제공되도록 보장하는 구조를 만들 수 있었다.

 

마무리

이번 작업을 통해 가장 크게 느낀 점은, 이미지 성능 문제는 측정 가능하고 해결 가능하며, 설계와 자동화를 통해 일관된 최적화를 제공할 수 있는 문제라는 것이었다.


Lighthouse와 네트워크 탭을 통해 병목을 확인하고, 정적 이미지와 사용자 업로드 이미지를 구분해 최적화가 수행되는 위치와 시점을 설계했다. 그 결과, S3 이벤트와 Lambda를 활용해 이미지 업로드 이후 자동으로 최적화가 수행되는 파이프라인을 구축할 수 있었다.

이제 사용자가 어떤 크기의 이미지를 업로드하더라도, 서비스에서는 일관된 기준으로 최적화된 이미지가 자동으로 제공된다.
이미지 최적화를 개별 작업이 아닌, 서비스 품질을 구조적으로 보장하는 영역으로 가져갈 수 있었던 경험이었다.