CloudFront와 S3를 사용한 웹 앱 배포 시, CSR은 CloudFront의 Default root object와 Error pages 설정을 통해 라우팅을 처리하고, SSG는 CloudFront Functions와 같은 커스텀 라우팅 솔루션을 활용하여 라우팅을 처리하는 것이
적합합니다.
이번에 사내 홈페이지를 배포하면서, 생전 처음으로 Next.js 기반 SSG 웹 앱을 S3(+CloudFront)에 올려보게 되었는데요. 라우팅 설정 과정에서 꽤나 삽질을 했었기에 정리가 한 번 필요하다고 생각을 했고, 이참에 S3(+CloudFront) 환경에서 CSR과 SSG 라우팅 설정의 차이에 대한 글을 써보게 되었습니다.
💡 FYI.
이 글에서 이어지는 라우팅 관련 내용은 모두 브라우저에서 직접 URL을 요청했을 때 서버가 처리하는 방식, 즉 서버 사이드 라우팅을 기준으로 다룹니다.
CSR 웹 앱의 라우팅 설정
CSR 웹 앱을 S3(+CloudFront)에 배포할 때, 별도의 설정이 없으면 원하는 대로 라우팅이 되지 않기 때문에 아래와 같이 설정을 해 주어야 합니다.
1. Default root object
먼저 루트 경로(/)로 접속 시, index.html을 반환하게 하기 위해서는 아래 그림과 같이 CloudFront에서 Default root object를 index.html로 설정해 주어야 합니다.

2. Error pages
위와 같이 Default root object를 잘 설정해두었다 하더라도, 서브 경로(/articles)로 접속하는 경우 S3에는 실제로 articles라는 파일이나 폴더가 없을 것이기 때문에 원하는 리소스를 반환할 수 없게 됩니다.
이를 커버하기 위해서는 아래 그림과 같이 CloudFront의 Error pages 탭에서 403, 404 오류 시 200 + index.html을 반환하도록 설정해 주어야 합니다.

💡 FYI.
사실은 Default root object 설정 없이 Error pages 설정만으로도 모든 라우팅 케이스에 대해 문제없이 동작하겠으나, 캐시가 없는 상태에서 루트 경로(/)로 접속하는 경우 아주 미세하게나마 성능 차이가 있으므로 Default root object도 함께 설정해 주는 것이 좋습니다.
SSG 웹 앱의 라우팅 설정
SSG 웹 앱을 S3(+CloudFront)에 배포할 때도, 별도의 설정이 없으면 원하는 대로 라우팅이 되지 않을 것입니다.
일단 인프라 레벨에서의 라우팅 설정에 대한 이야기를 하기 전에, 먼저 Next.js 기반 웹 앱을 SSG 형태로 빌드하는 방법부터 간단히 짚고 넘어가겠습니다.
Build as SSG
아래와 같이 next.config.js 파일에서 output을 export로 설정해 줍니다. 다른 속성들은 선택 사항이지만, 이 글에서 trailingSlash만은 true로 설정한 경우를 기준으로 설명하겠습니다.
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
// ⚠️ Required: Export pages as static HTML for SSG
output: "export",
// ✨ Optional: Emit `/me.html` -> `/me/index.html`
trailingSlash: true,
// ✨ Optional: Change the output directory `out` -> `dist`
distDir: "dist",
// ✨ Optional: Disable image optimization for SSG when using images, since there is no server to run the API
images: {
unoptimized: true,
},
};
module.exports = nextConfig;
CSR과 동일한 방법으로 라우팅 설정 해보기
이제 빌드 산출물을 S3에 올린 뒤, CSR과 동일한 방법으로 라우팅 설정을 해보겠습니다.
- CloudFront에서
Default root object를 index.html로 설정 - CloudFront의
Error pages탭에서 403, 404 오류 시 200 + index.html을 반환하도록 설정
결과는 아래와 같을 것입니다.
루트 경로(/)로 접속 시 Default root object 설정에 의해 index.html을 반환하게 되는데,
이때 당연하게도 index.html이 있다면 문제없이 index.html을 반환하겠지만, index.html이 없다면(예컨대, 다국어 지원을 위해 각 언어별로 kr/index.html 또는 en/index.html과 같이 구성된 경우) Error pages 설정이 있더라도 결국엔 원하는 리소스를 반환할 수 없게 됩니다.
서브 경로(/articles)로 접속 시 Error pages 설정에 의해 200 + index.html을 반환하게 되는데,
SSG 웹 앱의 경우 페이지별로 각기 다른 HTML을 가지고 있을 것이기 때문에, index.html이 있든 없든 원하는 리소스를 반환할 수 없게 됩니다.
CloudFront Functions를 활용한 커스텀 라우팅 설정
앞서 CSR과 동일한 방법으로 라우팅 설정을 해보았는데요, SSG 웹 앱의 경우 Default root object와 Error pages 설정만으로는 어떠한 방법으로도 모든 라우팅 케이스를 커버할 수 없다는 것을 눈치채셨을 거예요.
결국 SSG 웹 앱의 경우 별도의 커스텀 라우팅 솔루션이 필요하게 됩니다. AWS에서는 CloudFront Functions 또는 Lambda@Edge와 같은 솔루션을 생각해 볼 수 있는데, 우리가 구현하려는 기능은 단순한 라우팅 수준에 그칠 것이므로 비용과 성능 측면에서 CloudFront Functions가 좀 더 적합한 선택일 것입니다.
이제 우리가 원하는 라우팅 기능을 CloudFront Functions로 구현해보겠습니다.
function handler(event) {
const request = event.request;
// If the request is for the root path, map it to /index.html
if (request.uri === "/") {
request.uri = "/index.html";
}
// If the request is for a directory, append index.html
else if (request.uri.endsWith("/")) {
request.uri += "index.html";
}
// If the request has no file extension, treat it as a directory and append /index.html
else if (!request.uri.includes(".")) {
request.uri += "/index.html";
}
return request;
}
위 로직에 따라 아래와 같은 라우팅 케이스를 처리할 수 있게 되며, 이로써 SSG 웹 앱에서의 모든 라우팅 케이스를 커버할 수 있게 됩니다.
- 루트 경로 처리:
/→/index.html - 디렉터리 경로 처리:
/articles/→/articles/index.html - 확장자가 없는 경로 처리:
/resume→/resume/index.html
CloudFront와 CloudFront Functions 연결하기
이제 아래와 같이 CloudFront의 Behaviors 탭에서 Path pattern이 Default (*)인 Behavior에 CloudFront Functions를 연결해 주기만 하면 모든 라우팅 설정이 완료됩니다.
