우리 서비스는 이미지 기반 기능이 많고, 유저가 생성한 콘텐츠(프로필 이미지, 캐릭터 이미지, 이미지 생성 결과물 등)를 빠르게 제공하기 위해 CDN이 필수적이었다.
특히 글로벌 서비스 확장을 고려하면 이미지 전송 속도와 비용은 실제 서비스 품질에 직결된다.
이번 글에서는 우리가 S3 + CloudFront 조합으로 CDN을 구축한 과정부터 백엔드/프론트에서 어떻게 이미지 최적화를 적용했는지까지 전체 흐름을 공유한다.
CDN은 AWS의 S3와 CloudFront만으로도 매우 쉽게 구축할 수 있다.
S3는 안정적이면서도 저렴한 객체 스토리지이며, 대규모 파일을 빠르게 저장하는 데 최적화되어 있다. CloudFront는 이러한 S3 파일을 전 세계 엣지 서버에 캐싱해 사용자에게 가장 가까운 위치에서 서빙해주는 글로벌 CDN 서비스다.
기본 구조는 간단하다. 파일은 S3에 저장하고, CloudFront가 이를 캐싱하여 고객에게 빠르게 전달한다.
특히 이미지 파일은 한 번 업로드하면 변경될 일이 거의 없기 때문에 캐시 효율이 매우 높다.
한 번 캐싱된 이미지는 재요청 시 S3가 아닌 CloudFront 엣지 서버에서 바로 제공되므로, 서비스 속도와 비용 측면 모두에서 큰 이점을 얻을 수 있다.
우리는 이미지 유형을 명확히 분리하기 위해 S3 버킷 내부를 다음과 같이 폴더 구조로 나눴다:
character-profile-image/
├── gif/
└── default/
user-profile-image/
├── gif/
└── default/
image-generation/
├── gif/
└── default/이미지 확장자는 용량 절감을 위해 모두 WebP 로 변환해서 업로드하지만, 내부적으로는 gif 원본인지 아닌지에 따라 분류했다.
왜 gif와 default로 나누었나?
Next.js의 next/image 컴포넌트는 대부분의 포맷을 자동 최적화해주지만, GIF는 최적화가 불가능하다.
- default → 자동 최적화 가능
- gif → unoptimized를 강제로 설정해야 함

출처 : Next.js 공식 문서
이를 위해 백엔드에서 이미지 분석 후 gif인지 판별하여 적절한 폴더로 업로드하도록 설계했다.
S3 버킷을 생성할 때 모든 외부 액세스를 차단해야 한다. (Block Public Access)
S3 버킷은 Cloudfront에서만 접근 가능해야 한다.
이를 위해 버킷 정책으로 Cloudfront Distribution에서는 접근 가능하도록 설정해줘야 한다.
S3 Bucket와 Cloudfront Distribution 생성 및 설정이 끝났으면, Cloudfront Domain을 설정해주면 된다.
이는 Route53의 Public Domain에서 쉽게 설정이 가능하다.
A Record로 직접 Cloudfront Distribution과 연결할 수 있다.
백엔드에서 이미지 업로드를 처리할 때는 아래 과정을 거친다.
- 클라이언트가 업로드한 원본 이미지 수신
- WebP 변환 및 리사이징(최적화)
- 이미지 분석 후 gif 여부를 판단
- 폴더 구조에 맞게 S3에 업로드
- 최종 이미지 URL을 DB에 저장
프론트엔드에서는 API에서 받은 데이터를 next/image 컴포넌트를 사용해 보여주기만 하면 된다.
기본 이미지는 아래와 같이 보여주면 된다.
<Image
src="https://cdn..../user-profile-image/default/xxx.webp"
width={200}
height={200}
alt="profile"
/>GIF 이미지는 최적화의 대상이 아니므로 unoptimized만 넣어주면 된다.
<Image
src="https://cdn..../user-profile-image/gif/xxx.webp"
width={200}
height={200}
unoptimized
/>하지만 매번 컴포넌트에서 GIF 여부를 판별해 unoptimized를 넣는 방식은 코드 중복이 발생하기 때문에 좋은 방법이 아니라고 생각했다.
이를 해결하기 위해 나는 ImageWrapper라는 공용 컴포넌트를 만들었다.
ImageWrapper는
src가 CDN의 파일인지 확인하고,- 경로에
"/gif/"가 포함돼 있으면 최적화를 하지 않는다. - 나머지는
next/image의 기본 최적화 로직 활용 - 서비스 전체에서 일관된 이미지 처리 규칙 유지
⠀
import Image from "next/image";
const CDN_DOMAIN = process.env.NEXT_PUBLIC_CDN_DOMAIN ?? ''
export const ImageWrapper = ({ src, ...props }) => {
const isCDN = src.includes(CDN_DOMAIN);
const isGIF = src.includes("/gif/");
return (
<Image
src={src}
unoptimized={isCDN && isGIF}
{...props}
/>
);
};이렇게 되면 분기할 필요 없이 받은 데이터를 그냥 전달해주면 알아서 처리가 된다.
<ImageWrapper
src={user.profileImage}
width={200}
height={200}
/>Cloudfront로 CDN을 구현함으로써 이미지 로딩 속도가 크게 향상되었다.
CloudFront의 글로벌 Edge Location에 저장돼 있는 캐시 덕분에 전 세계 어디서든 빠르게 이미지를 제공할 수 있었다.
다만 프로필 사진에 GIF를 허용하는 서비스는 매우 많을텐데, 다른 서비스는 Next.js에서 GIF를 보여줄 때 어떻게 처리할지 궁금하다.
CDN을 구축하는 과정은 단순히 정적 파일을 빠르게 제공하기 위한 인프라 구성 이상의 의미가 있다.
S3가 안정적인 스토리지를 제공한다면, CloudFront는 그 위에 글로벌 캐싱 레이어를 더해 서비스 품질을 한 단계 끌어올린다.
전 세계 엣지 로케이션에서 파일을 서빙함으로써 지리적 거리로 인한 지연을 최소화하고, 반복 요청을 캐시로 처리하여 백엔드와 S3의 부하도 크게 줄여준다.
또한 AWS에서는 HTTPS 인증서 관리, 도메인 연결, 여러 보안 기능 등 서비스 운영에 필요한 요소를 모두 통합적으로 제공하기 때문에 유지보수 비용 역시 현저히 감소한다.

댓글 남기기