개요
내가 다녔던 회사의 커머스 플랫폼은 상품 이미지와 정적 리소스를 하나로 서빙하고 있었다. cdn은 국내 외부 업체를 거쳐 서빙됐고, 리소스 원본은 AWS MSP 파트너사 계약으로 권한이 제한된 A 계정의 s3에 있었다. 특히 운영하면서 아래의 문제가 발생했는데,
- 캐시 무효화를 직접 통제할 수 없음
- 프로덕션 배포마다 외부 cdn 퍼지를 사람이 직접 실행했어야 했다.
- 퍼지 누락으로 cdn이 구버전을 86400초 동안 들고 있다가. 웹 서비스 장애로 이어졌다.
- 엣지 로직을 붙일 수 없음
- 외부 cdn에는 Lambda@Edge나 CloudFront Functions 같은 엣지 로직을 붙일 수 없었다.
- 그래서 이미지 리사이징은 api 업로드 시점에 CarrierWave(Ruby 기반 파일 업로더)로 처리했다.
- 이미지 1장당 약 1초가 걸려, 리뷰나 문의 등록 API가 느렸다.
- 분산되어 있는 인프라 계정
- 운영 리소스가 있던 A(구) 계정은 MSP 파트너사 조직의 멤버 계정이라 일부 권한이 제한되어 있다. (sso 연결 불가 등)
- 25년 말부터 팀이 직접 관리하는 B(신규) 계정으로 인프라를 이관하고 있었고, cdn 또한 그 대상이다.

요구사항
커머스 플랫폼을 운영하지만, 커머스다운 cdn 시스템을 운영하지 않았다. 커머스에서 이미지는 장식이 아니라, 고객의 구매 결정과도 직결되는 리소스 그 자체라고 생각한다. 고객은 상품의 글보다 이미지를 먼저 보기 때문이다. 그래서 cdn을 처음부터 다시 만들기로 결정했다.
- 기존 cdn 도메인을 유지한 채 외부 cdn에서 신규 계정 CloudFront로 무중단 이관
- 클라이언트가
?w=300&h=200같은 쿼리 파라미터로 요청하면 Lambda@Edge에서 리사이징 - webp같은 필요한 포맷이 요구될 때 변환
1. 커머스 생태계에서 이미지 리사이징과 WebP가 중요한 이유?
위에서도 언급했듯, 이미지는 고객의 구매 결정을 이끄는 리소스라고 했다. 고객은 상품을 직접 경험할 수 없기 때문에 이미지를 통해 디자인이나, 색상, 질감 등을 파악하기 때문이다. 이것을 바탕으로 기술적으로 해결해야할 과제는 다음과 같다고 생각한다.
페이지 속도는 이미지가 결정한다
- 페이지를 열었을 때 사용자가 “다 떴다”고 느끼는 순간은, 화면에서 가장 큰 요소가 보일 때이다. 이 시점을 재는 지표가 LCP다.
- 데스크탑에서는 이 수치가 85%, 모바일에서는 76%이다. (Web Almanac 2025).
- 커머스는 상품 목록도 상세 페이지도 이미지가 화면을 채우고 있고, 이 리소스들을 가볍게 만들어 전달하는 것이 중요하다.
같은 화질이면 WebP가 더 작다
- WebP는 무손실 기준 PNG보다 26% 작고, 투명 배경 이미지도 손실 압축하면 PNG보다 보통 3배 작다. (구글)
- 우리 CDN에서 가장 많이 서빙되던 객체는 대부분 PNG다. AWS CloudFront Popular Objects 기준으로 PNG 6개가 105GB로 객체 전송량의 57%를 차지하고 있었다.
전송량이 곧 비용이다
- CDN은 전송량과 요청 수로 과금한다. 이미지를 크게 보내면 사용자는 느리게 받고, 비용은 그만큼 더 낸다.
- 원본을 그대로 보낼 때는 하루 전송량이 2,000GB를 넘었다.
- 리사이징, WebP 적용후 비용이 약 60% 줄었다.
2. 새로 구축된 CDN 아키텍처

- 캐시 정책에는 쿼리 파라미터의 w, h, f를 캐시 키에 포함했다.
CloudFront Functions (viewer-request): Accept 헤더로 WebP 지원 여부를 판단해?f=webp쿼리 스트링 추가- 캐시 키를 계산하기 전에 f=webp를 붙이기 때문에, WebP를 지원하는 브라우저와 지원하지 않는 브라우저가 서로 다른 캐시를 받음
Lambda@Edge (origin-request): 쿼리 파라미터를 해석해 리사이징 후 S3에 저장하고 URI rewrite- S3에는 원본 이미지와 리사이징된 이미지가 저장되어 있다.
3. 어떻게 만들 것인가
Origin-Request에서 람다를 실행한다

1) Origin-Request
AWS 공식 문서를 보면 origin-request는 CloudFront가 요청을 origin server(우리의 경우 s3)로 전달할 때 실행된다고 했다. 다만 요청한 객체가 Cache Hit라면 함수는 실행되지 않는다. 즉, Cache Miss인 경우에만 람다가 실행되므로, 한 번 리사이징된 이미지는 이후 요청에서 Miss될 때까지 람다 호출이 발생하지 않는다.
2) Viewer-Request
반면 viewer-request는 클라이언트가 요청할 때마다 트리거되어, CloudFront에 캐시되어 있는지 확인한다. 캐싱된 객체 요청에도 함수가 돌아 비용이 든다.
구현 방법
1) 변환 라이브러리는?
- nodejs에서 유명한 라이브러리인
sharp를 사용했다. fit: 'inside'로 비율을 유지한다.withoutEnlargement: true로 원본보다 크게 확대하지 않는다.- 설치시, Lambda 실행 환경(linux-x64)에 맞는 바이너리로 설치해야 한다.
2) 쿼리 파라미터
w(너비),h(높이),f(포맷: jpg, png, webp)로 원하는 형태로 객체를 가져온다.- 모두 optional이며, w만 준다고 했을때, 비율을 유지하며 반환한다.
3) s3 경로 규칙
- 원본과 같은 경로에 쿼리 파라미터 정보반 붙여 저장한다.
image.jpg->image_w500_h700.png
4) URI rewrite
람다가 이미지를 직접 반환하지 않고, 저장한 결과 경로로 URI를 바꿔 CloudFront가 S3에서 가져오게 한다. Lambda@Edge에서 직접 응답을 반환하면 본문 크기 제한(1MB)을 받기 때문이다. 이를 넘으면 502가 발생한다. (공식 문서)
Reference
- https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-generating-http-responses.html
- https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html
- https://developers.google.com/speed/webp?hl=ko
- https://almanac.httparchive.org/en/2025/performance#lcp-content-types