SSR 환경에서 와일드카드 서브도메인 구축하기

오형근·2023년 10월 20일
4

Frontend

목록 보기
2/10
post-thumbnail

기존에 진행하던 프로젝트에서 티스토리와 같은 형태의 개인화된 서브도메인(내맘대로.tistory.com)을 적용해야하는 소요가 발생했다. 그래서 이것을 어떻게 해결하면 좋을까...생각하다가 두 가지 방법을 생각해내었다.

✌️두 방법?

1️⃣ 하위 레코드 생성 (+ Lambda를 이용한 자동화)

기존에 루트 도메인 네임서버가 Route53을 통해 관리되고 있었고, 단순하게 생각해보면 여기에 레코드를 생성해 모두 우리 프론트 도메인으로 보내주면 되는 간단한 일이었다.

이 경우에 프론트 도메인은 높은 확률로 Vercel과 같은 PaaS 서비스를 활용할 것이기 때문에, Vercel에서 배포한 도메인을 연결해주면 되는 간단한 일이었다.

여기서 유저가 새로운 서브도메인을 원할 때마다 일일히 생성해줄 수는 없는 노릇이었기 때문에, Lambda를 활용해서 레코드 생성을 자동화하고자 했다. 아래는 이에 대한 예시 코드이다.

import {
  Route53Client,
  ChangeResourceRecordSetsCommand,
} from "@aws-sdk/client-route-53";

export const handler = async (event) => {
	const client = new Route53Client({ region: "ap-northeast-2", 	credentials: {
  	accessKeyId: process.env.ACCESS_KEY,
  	secretAccessKey: process.env.SECRET_KEY
	} });

	const input = {
  	"ChangeBatch": {
    	"Changes": [
      	{
        	"Action": "CREATE",
        	"ResourceRecordSet": {
          	"Name": `${event.name}.root.domain`,
          	"ResourceRecords": [
            	{
              	"Value": ${my_ip}
            	}
          	],
          	"TTL": 60,
          	"Type": "A"
        	}
      	}
    	],
    	"Comment": "Web server for example.com"
  	},
  	"HostedZoneId": ${my_hosted_zone_id}
	};


	const command = new ChangeResourceRecordSetsCommand(input);
	const response = await client.send(command);
  return response;
};

당시에는 서버에 먼저 연결을 테스트해보고자 했기 때문에 A 레코드를 생성하였는데, 정말 간단하게 원하는 name을 가지는 서브도메인을 생성하는 코드였다.

물론 이러한 방식을 적용하면 가장 간단하고 수월한 작업이 되었겠지만, 금액적인 문제로 다음 방법을 구현하게 되었다.

Route53 레코드 요금

위 사진처럼 Route 53은 10000개까지의 레코드를 무료로 생성할 수 있고, 초과하는 레코드에 대해서는 개당 요금이 청구된다.

물론 10000명의 유저를 모을 수 있는 서비스라면,,,pricing을 적용해도 무방하겠지만, 어쨌거나 가격이 들어간다는 점은 결국 free pricing을 적용할 수 없다는 점이 크게 다가온 것 같다.

우선 두 번째 방법을 적용해보고 두 번째 방법이 실패한다면 첫 번째 방법을 적용하자! 라는 것이 결론이었는데,

그래서 두 번째 방법이 뭐냐고?

2️⃣ 와일드카드 서브도메인

두 번째 방법은 와일드카드(*)를 사용하는 방법이다.

와일드카드를 적용해 *.root.domain 과 같은 형태의 모든 접근에 대한 처리를 해주는 방법인데,

이걸 해결하기까지 넘어야 할 산이 이렇게 많은 줄 몰랐다,,,🥲

예. 이게 메인디쉬입니다.

지금부터 와일드카드 서브도메인을 적용하기 위한 이야기 보따리를 풀어볼까 한다!

✨ 발단

대충 시작은 다음 글에서 출발한다.

Next.js에서 wildcard subdomain 다루기(middleware, SSG)

이 글은 처음 와일드카드 서브도메인을 적용해야겠다 생각한 계기가 되어준 글이었고, 결국 마지막에 문제를 해결하는 가장 큰 키가 되었다.

이 자리를 빌려(대단한 자리는 아니지만,,,) 글을 추천해준 팀원과 글쓴이에게 감사의 말씀을 전합니다 :)

위 글에서 살펴보면 Next.js 내장 기능으로 제공된 middleware를 사용하면 간단하게 프록시를 적용할 수 있다. 이를 통해 애플리케이션에 들어오는 트래픽을 자유자재로 라우팅할 수 있어 다방면으로 유용하게 사용가능함을 알 수 있다.

문제점은 여기에서 발생한다.

🏃 시도해본 것들

문제 1. Vercel 에 네임서버를 이전해야한다고?

위 글에서 살펴보면 Vercel에 *.root.domain 과 같은 와일드카드가 적용된 서브도메인을 domain 설정에 추가해주었는데, 이렇게 되면 아래와 같은 에러를 뱉어낸다.

vercel subdomain error

에...뭘까...

일반적인 경우에는 탭에 Nameservers 말고 다른 옵션들이 함께 있어 다른 서비스에서 호스팅을 하더라도 문제없이 작동하도록 되어 있지만, 와일드카드 서브도메인을 적용하는 경우는 네임서버를 Vercel로 이전해야한다...라는 옵션 밖에 없었다...


여기서 사실 안되나보다,,,라고 생각하고 돌아서려고 했는데, 돌아서서 생각해보니 다음과 같은 상황이 어이가 없다고 느껴졌다.

엥? EC2로 배포한 우리 서버는 Route53에서 직접 와일드카드 서브도메인 연결하면 잘만 될게 뻔한데, 그러면 프론트도 이렇게 배포하면 되는거 아닌가?

그래서 결국 AWS 내에 있는 서비스로 이전해서 프론트 배포를 직접 진행하고 이를 와일드카드 서브도메인에 연결하는 것으로 목표를 세웠다.

사실 netlify나 cloudflare page의 경우는 동일하거나 그 보다 더욱 엄격할 것 이라고 생각이 들어 깊게 시도해보지는 못했는데, 기본적으로 시도했을 때 두 곳 모두 네임서버 이전이라는 허들이 존재해 돌아올 수밖에 없었다.

문제 2. Amplify도 안된다고?

여기서 짚고 넘어가야하는 문제가 하나 있다.

우리 프로젝트의 경우 프론트에서 SSR을 부분적으로 적용중이기 때문에, 정적 파일로 빌드해서 그냥 S3에 업로드하고 Cloudfront에 연결하는 방식은 곱게 통하지 않는다.

SSR을 위해 애플리케이션을 올릴 서버가 분명히 필요하고, 이를 위해서 처음 시도한 것은 AWS Amplify였다.

간단히 설명을 하자면, AWS Amplify는 S3와 Cloudfront를 결합해 Vercel처럼 배포를 쉽게 할 수 있도록 해주는 서비스인데, 여기에서 프레임워크가 Next인 경우 SSR을 지원한다.

그래서 처음에는 Amplify를 이용해 배포한 뒤, 해당 리소스를 Route53에 연결해주면 되겠다! 라는 생각이었다.

그러나,,,,

뭐,,,정규식이 안 맞는다고 한다. 여기에서 한 번 더 좌절아닌 좌절을 했다.

AWS 내부 서비스로도 구현이 안된다고...?


사실 생각을 거듭해보면 어렵지 않게 이유를 찾을 수 있다.

위 도면을 살펴보자. AWS Route53에서 호스팅 되고 있는 도메인이 있고 해당 Route53을 통해 생성된 레코드 a.example.com, b.example.com, *.example.com이 존재할 때, Route53은 경로가 지정되어 있는 a, b 레코드에 대해서 우선적으로 매핑을 진행하고, 이후에 매칭된 레코드가 존재하지 않을 때 *.example.com으로 트래픽을 보내주게 될 것이다.

그런데 여기서 만일 *.example.com에 연결되어있는 API Gateway가 존재한다면(본래 불가능하지만) 해당 API Gateway는 a.example.com과 b.example.com에 대한 정보는 모르는 채 트래픽을 전달받게 될 것이다.

그러면 API Gateway 입장에서는 a.example.com, b.example.com의 트래픽들도 당연히 자신에게 올 것으로 예상할텐데, 실제는 그렇지 못하기 때문이다.

이렇듯 각 레코드로 연결된 리소스들 간의 혼선이 오지 못하도록 와일드카드를 활용한 레코드 연결 및 등록은 호스팅 영역에서만 가능하게 되어있음을 알 수 있다.

여기서 포기하면 재미가 없긴 해

🌟 해결..?

무언가 실마리가 풀리기 시작한 지점은 이곳에서부터였다.

프론트엔드 와일드카드 서브도메인을 적용하기 이전에 서버 SameSite 문제를 커버하기 위해 SSL 인증을 미리 해두는 과정이 있었는데, 그 과정에서 와일드카드 서브도메인을 직접 적용해보고자 서버가 배포된 EC2를 직접 Route53에 있는 와일드카드 레코드에 연결하게 되었다.

그리고,,,되는데..?

됐다. 연결이 너무 잘 되었다.

사실 이걸 확인하고 나서 "이러면 프론트엔드도 최후의 수단은 있으니까 무조건 되겠는데?" 라는 생각으로 시도를 한 것이었다.

근데 그 과정에서 시도한 다양한 플랫폼들을 통한 배포가 모두 아쉽게 마무리되어서,,,결국 EC2에 직접 배포하는 방향을 택하게 되었다.

결국 EC2에 소스코드를 받고, Next 빌드에 필요한 자원이 부족해 스왑 메모리도 할당하고, pm2로 무중단으로 배포를 올려두고, ALB 설정해서 SSL도 먹여주고,,,등등의 과정을 거친 뒤, 이제는 당연히 되겠지 라는 생각이었다.

사실 이 방법은 무조건 될 것이라고 알고 있었고, 기존에 Vercel과 같은 플랫폼의 강력한 기능들을 포기하지 못해 최대한 플랫폼을 활용하려고 했던 것이었는데,,,안타깝게도 실패했다.

현재는 EC2 프리티어 안쪽의 리소스로 빌드하기에는 애플리케이션의 규모가 커져, Docker 이미지를 ECR에 배포해두고 compose파일을 통해 Elastic Beanstalk으로 배포를 해둔 상태이다.

특히 Elastci Beanstalk의 추가 배치를 이용한 롤링 업데이트 기능을 활용해 무중단 배포를 진행하고 있다.

뭐 사실 안 되는 것은 아니었다. 원하는 형태가 아니었을 뿐,,,

🥲 다시 문제가 발생했다.

처음에 생각한 구조는 대충 이렇다.

프론트에 접근하는 URL의 경우 https://*.example.com의 형태를 띄고, 우리는 여기에서 와일드카드에 들어가는 내용을 프론트에서 처리해줄 것인지, 백에서 처리해줄 것인지에 대한 고민에 빠졌다.

방법 1. 프론트엔드에서 파싱해서 보내기

https://*.example.com의 형태를 파싱해 https://example.com/*과 같은 형태로 서버에 요청을 보낸다. 그러면 서버에서는 와일드카드가 없는 깔끔한 요청을 받을 수 있고, 추가적인 로직이 들어가지 않는다.

방법 2. 백엔드에서 직접 파싱하기

https://*.example.com의 형태 그대로 프론트엔드를 띄워주고, 비동기 호출을 할 때에도 해당 Origin을 유지한다. 그러면 백엔드에서는 Origin을 파싱해 와일드카드 부분의 정보를 가져온다. 이 경우 프론트엔드는 단순히 와일드카드 정보를 전달하기만 할 뿐 추가적인 로직이 들어가지 않는다.


뭐가 좋을까...고민을 하는 도중에 백엔드에서는 이미 권한 분리를 위한 대대적인 리팩토링이 들어가고 있었고, 그 과정에서 파싱을 통해 와일드카드 정보를 가져오는 로직을 추가하면 좋을 것 같다고 생각해 백엔드에서의 파싱을 우선적으로 시도하기로 했다.

결국 문제는 SSR...

어어,,,문제가 발생했다.

위에서 언급한 방법 2로 구현을 한 경우, Next의 Nested Routing을 통해 Slug 정보를 가져오는 것이 아니라 직접 window.location 정보를 가져와 와일드카드 부분을 파싱하는 것을 시도했었다. 그러나 와일드카드 서브도메인이 적용되는 곳은 SEO가 중요한 부분이었기에 SSR을 포기할 수 없었고, 이 과정에서 빌드 실패가 일어났다.

그도 당연한 것이, 아직 브라우저에 띄우지도 않았는데 window 객체가 존재할리 없었다,,,결국 SSR 환경에서 window객체를 불러오지 못한다는 큰 걸림돌로 인해 2번 방법을 포기하게 되었다.

그러면 남은 방법은 1번! 사실 1번의 경우는 생각해 둔 확실한 방법이 있지만, 이를 이용하면 URL이 조금 더러워진다는 단점이 발생했다.

단순하게 생각해서 와일드카드 서브도메인이 적용된 URL에 path에도 동일한 정보를 집어넣어, 서브도메인의 경우는 fake로 작용하도록 하는 것이다.

https://*.example.com/* 과 같은 형태를 띄고, 앞의 에서는 아무 정보도 가져오지 않는다. 모든 로직은 path에 있는 를 통해서 이루어진다.

우선 마땅한 방법이 떠오르지 않았기에, 우선은 이 방식을 통해서 못생긴(?) 와일드카드 서브도메인을 구현할 수 있었다.

다만 이렇게 되었을 때 하나의 URL에 동일한 정보가 중복되어 들어간다는,,,조금 껄끄러운 문제가 있었다.

해결을 위한 머리싸매기

이걸 해결하고 싶어서 정말 다양한 방법을 생각했다.

동 서버에 프록시를 둬서 트래픽 분산처리를 해야할지,
아예 미들웨어 서버를 올려서 추가적인 요청을 통해 받아와야할지 등등

복잡한 생각이 많았는데, 결국 답은 Next.js 에 내장된 middleware였다.

방법이 생각보다 가까이 있어 조금 놀랐는데, Next.js에 내장된 middleware를 적용하면 서비스로 오는 모든 요청들을 먼저 확인해 파싱이나 트래픽 관리 등을 쉽게할 수 있다. 쉽게 작성하고 구축하는 프록시라고 생각하면 편할 것 같다.

그래서 이 middelware를 어떻게 사용했나?

적용된 방법 자체는 위에서 언급한 방법 1: 프론트에서 파싱해서 보내기 이다.

다만 이때 SSR이 적용되려면 무조건 Nested Routing을 이용해 path slug를 통한 와일드카드 정보 추출이 가능했어야 하는데, middleware를 통해 이를 깔끔하게 해결할 수 있었다.

Next Middleware를 적용하면?

Next middleware를 이용해 들어온 요청을 확인하고 만일 루트 도메인이 아니라면 서브 도메인 부분을 추출해 path 특정 위치에 넣어준다. 라는 로직을 짰다.

middleware의 경우 SSR 빌드와 무관하게 애플리케이션 맨 앞단에 존재하기 때문에 빌드 환경 등에 영향을 받지 않고 간단한 전처리 작업을 할 수 있다는 큰 장점을 가진다.

위 사진과 같이 Next.js 애플리케이션 앞단에 리버스 프록시 역할을 하는 middleware 파일을 작성해주었다.

코드를 보면 subdomain 부분의 데이터를 추출해주고, 해당 데이터를 활용해 url을 재작성하는 과정을 거치고 있음을 알 수 있다.

이렇게 Next SSR 환경에서도 온전히 동작하는 와일드카드 서브도메인을 구축할 수 있었는데, 내가 여기서 마지막으로 시사하고 싶은 점은 wildcard-available space에 대한 것이다.

아래 다이어그램을 살펴보자.

다이어그램을 살펴보면 와일드카드를 적용하는 부분이 유저 트래픽 ~ Next.js middleware로 제한되면서, 프론트엔드 애플리케이션을 비롯한 백엔드까지 해당 와일드카드 url이 전파되지 않음을 알 수 있다.

사실 와일드카드는 해킹 등의 시도에 매우 취약하고, 이러한 점에서 다른 외부 모듈이나 라이브러리를 적용함에 있어서 불리함을 안고 가거나 연동이 어려운 부분이 분명히 존재한다. 따라서 와일드카드가 영향을 주는 부분이 많아지면 안되는데, 위의 그림처럼 사전에 와일드카드를 차단하고 필요한 만큼만 사용할 수 있었다는 점에서 좋은 아키텍쳐라고 생각했다.


지금까지 와일드카드 서브도메인을 구축하면서 생겼던 이슈들과, 삽질한 내용들에 대한 이야기를 정리해보았다.

결과적으로,,,와일드카드는 최대한 사용을 자제하면 좋을 것 같다...꼭 필요한 곳에서만 사용하기!

2개의 댓글

comment-user-thumbnail
2023년 12월 19일

CloudFront function 을 통해 엣지단에서 URL을 rewrite 해주는 방식은 어떤가요?!!
물론 과금의 문제도 있겠지만 프리티어로 사용도 가능하니 초반에는 큰 문제가 없을 듯하구
또한 특정 지을 수 없고 제한 없이 다양한 값이 생길 수 있는 와일드카드 하위 도메인으로서 요청이 들어온다면 그에 대해 Origin에 대해 요청할 수 있도록 rewrite/redirect 시켜줄 수도 있겠다는 생각이 조그맣게 드네요..!!
짧은 지식을 가지고 생각해낸 생각인터라 형근님의 의견도 궁금해요!!

1개의 답글