SPA 국가별 라우팅 선택
SPA 서비스를 운영하다 보면 SEO를 위해 국가별로 URL 구조를 분리해야 하는 상황이 생긴다. 우리는 일본 시장 진출을 앞두고 /jp 경로를 통한 국가별 분기를 구현해야 했다.
문제 상황
- 일본에서 접속한 사용자는
/jp로 시작하는 URL로 보내야 함 - React 기반 SPA이므로 모든
/jp/**요청은/jp/index.html로 가야 함 - 하지만 정적 파일(js, css, 이미지)은 원본 경로 유지 필요
- 검색엔진이 크롤링할 수 있도록 서버 측에서 처리해야 함
선택지
1. Lambda@Edge
- 풀 프로그래밍 기능 제공
- Node.js 런타임 사용 가능
- 하지만 비용이 높고 콜드 스타트 존재
2. CloudFront Functions
- 매우 빠르고 저렴 (Lambda@Edge 대비 1/6 수준)
- 단순한 JavaScript만 가능
- 1ms 미만 실행 시간 제약
3. 클라이언트 JavaScript 리다이렉트
- 가장 간단하지만 SEO에 불리
- 검색엔진 크롤러가 국가 헤더를 제공하지 않을 수 있음
실제 선택: CloudFront Functions 두 개로 분리
우리는 CloudFront Functions를 선택했고, 관심사를 분리하기 위해 두 개의 함수로 나눴다.
CloudFront Function 1: 국가 감지 및 리다이렉트
function handler(event) {
var request = event.request;
var headers = request.headers;
var uri = request.uri;
if (headers['cloudfront-viewer-country'] &&
headers['cloudfront-viewer-country'].value.toLowerCase() === 'jp' &&
(uri === '/' || uri === '')) {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': { value: '/jp' }
}
};
}
return request;
}
CloudFront Function 2: SPA 라우팅 Rewrite
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.indexOf('/jp') === 0) {
var extensionPattern = /\.[a-z0-9]+$/i;
if (!extensionPattern.test(uri)) {
request.uri = '/jp/index.html';
}
}
return request;
}
왜 이렇게 판단했는가
1. 하나의 함수가 아닌 두 개로 분리한 이유
처음엔 하나의 함수에서 국가 체크와 URI rewrite를 모두 하려 했다. 하지만:
- 국가 감지는 루트 경로(
/)에서만 필요 - URI rewrite는 모든
/jp/**경로에 필요 - 하나로 합치면 매 요청마다 불필요한 국가 체크 로직이 실행됨
두 함수로 나누면 각자의 책임이 명확하고, 나중에 다른 국가를 추가할 때도 CloudFront Function 1만 수정하면 된다.
2. Lambda@Edge가 아닌 CloudFront Functions를 선택한 이유
Lambda@Edge가 더 강력하지만, 우리가 하는 일은:
- 헤더 하나 읽기
- 문자열 비교
- URL 변환
이 정도는 CloudFront Functions로 충분했고, 비용과 레이턴시 측면에서 이득이 컸다.
트레이드오프
1. 302 리다이렉트 선택
일본 사용자가 /에 접속하면 /jp로 한 번 더 리다이렉트된다. 301이 아닌 302를 선택한 이유:
/가/jp로 영구 이동한 것이 아님- 국가에 따라 다르게 동작하는 조건부 리다이렉트
- 검색엔진도
/와/jp를 각각 별도로 인덱싱해야 함
2. URL 경로와 표시 언어의 분리
서버(CloudFront)는 SEO를 위한 URL 구조만 담당한다. 실제 언어 표시는 클라이언트에서 navigator.language와 사용자 선택으로 처리한다.
이 덕분에:
- 일본에 있지만 영어를 쓰는 사용자도
/jpURL에서 영어로 볼 수 있음 - 일본 외 지역에서도 수동으로 일본어 선택 가능
트레이드오프: URL 경로와 실제 표시 언어가 불일치할 수 있다. 예를 들어 /jp인데 영어로 표시될 수 있다. 하지만 SEO는 URL 기반으로만 동작하므로 문제없다.
필요한 CloudFront 설정
두 함수를 Viewer Request 단계에 순서대로 연결:
- CloudFront Function 1 (국가 감지)
- CloudFront Function 2 (URI rewrite)
그리고 Cache Policy에 CloudFront-Viewer-Country 헤더를 포함시켜야 국가별 캐싱이 제대로 작동한다.
결론
이 설계에서 우리가 지키고 싶었던 목표는 명확했다.
검색엔진이 국가별 페이지를 URL 단위로 명확히 인식하도록 만드는 것이다.
그래서 서버(CloudFront)는 일본 트래픽을 /jp로 진입시키고, /jp/** 요청을 SPA 라우팅 규칙에 따라 /jp/index.html로 rewrite하는 역할까지만 맡겼다.
헤더 판별과 문자열 비교 수준의 로직만 필요했기 때문에, Lambda@Edge 대신 CloudFront Functions로도 충분하다고 판단했다.
다만 이 선택은 URL 구조와 실제 표시 언어를 분리한다는 트레이드오프를 전제로 한다.
/jp로 진입했더라도 사용자가 영어를 선택하면 영어로 표시될 수 있다. 우리는 이를 문제라기보다는, SEO는 URL로 해결하고 UX는 사용자 선택을 우선한다는 명확한 기준의 결과로 받아들였다.
완벽한 구조는 아니다.
초기 진입 시 302 리다이렉트가 한 번 발생하고, 국가 분기 로직과 캐시 정책을 함께 관리해야 한다는 부담도 있다.
그럼에도 현재 요구사항(국가별 URL 분기, SPA 라우팅, 정적 리소스 경로 유지)을 가장 단순한 형태로 만족시키고, 이후 다른 국가를 추가할 때 변경 범위를 최소화할 수 있다는 점에서 이 구조는 지금 시점에 충분히 설명 가능한 선택이라고 판단했다.
Leave a comment