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

우창완·2023년 3월 3일
10
post-thumbnail

Next.js는 path parameter에 관해서만 SSG를 지원하고 subdomain에 대해서는 SSG를 지원하지 않습니다. 이를 해결하기 위해 페이지로의 request들을 middleware에서 하위 경로로 rewrite하여 SSG를 지원하였습니다. 이를 통해 서비스의 성능을 200%(Lighthouse)이상 향상시킬 수 있었습니다.

상황

회사 프로젝트에서 유저가 3D 웹페이지로 만들고자하는 모델링 파일을 제출하면 이를 Three.js 등 웹 3D 기술을 이용하여 3D 웹페이지를 생성하고 이 페이지를 유저에게 subdomain 형식으로 제공하고 있습니다. 유저는 서브 도메인을 설정할 수 있어서, 와일드 카드 subomain을 다룰 일이 있었는데요, 예를 들면 motion-desk.example.com, awesome-painting.example.com 와 같이 사용자가 원하는 주소를 생성할 수 있습니다.

와일드카드 서브 도메인이 무엇일까요?

먼저, 서브 도메인을 먼저 살펴보면, 네이버 블로그는 blog.naver.com 으로 blog로 시작하고 네이버 맵은 map.naver.com map으로 시작합니다. blog와 map을 subdomain이라고 합니다.

와일드카드 서브 도메인은 서브 도메인 자리에 정해져 있는 특정 주소가 아닌 모든 서브 도메인 이름을 매핑할 수 있는 서브 도메인입니다.

아래와 같은 방식으로도 표현할 수 있습니다.

  • [*].example.com
티스토리로 예를 들면, [subomain].tistory.com 와 같은 형식으로 
원하는 서브 도메인을 생성할 수 있고 subdomain에 따라서 별도의 블로그 공간을 갖게 됩니다.

이러한 와일드카드 서브 도메인을 어떻게 처리할 수 있을까요?

여러가지 방법으로 와일드카드 서브도메인을 다룰 수 있을 것 같은데요, 하나씩 살펴보겠습니다

방법 1. CSR을 이용하여 해결하기

첫번째 방법은 client side renedering을 subdomain을 다룰 수 있습니다.

clinet side에서 각 subdomain에 맞는 data fetching을 하여 결과를 렌더링하면 되겠죠.
* 포스팅에 쓰인 코드들은 회사 코드가 아닌 개념을 설명하기 위해서 간단한 코드로 재구성하였습니다.


  useEffect(() => {
    const hostname = window.location.hostname
    const subdomain = hostname.split(".")[0]

    const fetchData = async () => {
      const response = await httpClient().get(`http://wildcard-example.com/${subdomain}`);
      setProjectInfo(response.data)
    }
    fetchData();
  }, [])

제가 처음 회사 프로젝트를 인수인계 받았을 때 CSR을 이용하여 wildcard 서브 도메인을 처리하고 있었습니다. 하지만 서비스를 하며 여러가지 한계점이 있었는데요, 먼저 프로젝트를 다른 사람에게 카톡 메신저나 sns에 공유시에 Open Graph image가 생성되지 않았습니다.

저희 서비스는 3D 웹페이지를 제작해주고 이 페이지를 다른 사람에게 공유할 수 있는데요, CSR에서는 유저가 각각의 3d 웹페이지에 대한 썸네일을 OG Image를 생성할 수 없는 것은 서비스에 치명적인 약점이었습니다. 또한, CSR 방식에서는 각 웹페이지에 맞는 메타데이터를 생성하기에 구글 검색 시에 페이지에 꼭 필요한 정보가 노출이 되고 있지 않았습니다. 그래서 우리 팀은 CSR 대신 다른 방법으로 문제를 해결하기로 접근합니다.

방법 2. SSR을 이용해보기

Next.js의 SSR 을 이용하면 다음과 같이 처리할 수 있습니다

export const getServerSideProps = async (context) => {
  const hostname = context.req.headers.host;
  const subdomain = hostname?.split('.')[0];

  const result = await httpClient().get(`http://localhost:3000/api/subdomain/${subdomain}`);


  return {
    props: {
      project: result
    }
  }
};

getServersideProps의 context에서 hostname과 subdomain을 받을 수 있고, 이를 이용하여 서버에서 프로젝트 정보를 받아올 수 있습니다.

이를 통해, SSR을 이용하여 와일드 카드 서브도메인을 다룰 수 있었고, CSR 방식에 비하여 각 프로젝트에 적절한 메타 데이터를 생성할 수 있었고, 3D 모델링 썸네일을 OG Image로 생성할 수 있었습니다.

대부분의 경우에는 지금 방식으로도 문제를 해결할 수 있을 것 입니다. 하지만 저희 서비스는 Three.js를 비롯한 여러가지의 무거운 웹 3D, 애니메이션 라이브러리들을 사용하고 있어 다른 서비스들에 비해 무거운 편이었고, response에 3D 모델링 데이터도 포함하고 있어 SSR를 이용하여도 response time이 긴 편이었습니다.

실제로 Lighthouse 평균 40~45 점을 기록하여 성능 개선이 꼭 필요했습니다. 이를 어떻게 해결하였을까요?

3. SSG를 이용할 수 있을까? (feat. ISR, middleware)

저희 서비스의 3D 웹 페이지는 한 번 만들어지면 변경이 없거나 빈도가 매우 낮은 수준이었고, 따라서, SSR보다는 SSG 방식이 더욱 적합하였습니다.

하지만 아쉽게도 Next.js는 path parameter에 관해서만 SSG를 지원합니다. 즉, 유저가 접속하는 awesome-painting.example.com 페이지는 SSG를 통하여 pre-generate 할 수 없습니다.
그러면 wildcard subdomain에서는 SSG를 활용할 수 없을까요?

middleware를 활용하기

Next.js는 12.0.0 부터 middleware을 지원하기 시작했습니다. middleware을 활용하면 페이지에 도달하기 전에 특정 로직을 실행할 수 있습니다.
middleware를 통해서 host rewrite를 할 수 있다는 예시를 Vercel 팀에서도 소개하고 있습니다.

middleware에서 url을 rewrite 시키면 각 서브 도메인마다 ssg를 지원할 수 있지 않을까? 라는 생각을 할 수 있었고, middleware 통해 url을 설계를 하였습니다.

실제 서비스는 더욱 복잡하지만, 매우 간략히 도식화하면 다음과 같습니다.

자 이것을 middleware를 통해 처리해볼까요?

import { NextRequest, NextResponse } from 'next/server';

export const middleware = (req: NextRequest) => {
  const subdomain = req.headers.get('host')?.split('.')[0]!;
  const newURL = new URL(`/project/${subdomain}`, req.url);
  return NextResponse.rewrite(newURL);
};

export const config = {
  matcher: '/',
};

이렇게 하면 유저가 subdomain.example.com으로 접속 시, example.com/project/[subdomain]으로 request를 rewrite 할 수 있고, 이 경로에 대한 처리는 pages/project/[subdomain] 에서 할 수 있습니다! 이제 우리는 getStaticPathsgetStaticProps를 사용하여 SSG의 이점을 가질 수 있습니다.

코드는 다음과 같아요!


export const getStaticProps = async (context: GetStaticPropsContext) => {
  const subdomain = context.params?.subdomain;
  const response = await httpClient().get(`/project/domain/${subdomain}`);
  return {
    props: response.data,
  };
};

ISR? SSG?

SSG는 모든 페이지를 빌드 시점에 pre-generate 시켜 SSR에 비해 응답시간이 빠른 반면에, 빌드 시간이 길어질 수 있다는 단점이 있습니다. 저희 서비스는 서버로부터 받아오는 데이터에 3d 파일의 여러 정보도 포함하고 있고, 많은 서브 도메인 페이지들이 생겨날 수 있어 모든 페이지들을 빌드 시점에 생성하기 보다는 ISR을 선택하여 반응 시간과 빌드 시간간에 적절한 균형을 맞췄습니다.

export const getStaticPaths: GetStaticPaths = async () => {
  
   'project list를 서버로부터 받아와서 paths 생성`
	const pahts= ...

  return {
    paths,
    fallback: 'blocking',
  };
};

배포는 어디에 해야 될까?

Vercel

vercel이 wildcard 도메인을 지원하고 있습니다.

Vercel Project -> Settings -> Domains 에서 와일드 카드 도메인을 설정할 수 있습니다.

Netlify

Netlify도 wildcard를 지원하지만 유료 사용자들에 한해서만 이용할 수 있다고 합니다.

성능 개선 결과🔥

실제로 저희 서비스에 올라와져있는 예시 프로젝트를 테스트 했을 때, lighthouse 점수가 40~45 점에서 90점 이상으로 성능 개선된 것을 확인할 수 있었습니다.
(웹 3D의 무거운 라이브러리들을 사용하면서도 90점 이상 기록한 것은 예상보다도 더 높은 수치여서 결과를 보고 소리 질렀습니다..)

성능 개선을 마치고

국내에는 물론, 영어로도 관련 주제에 관한 이야기가 거의 없는 주제여서 여러 삽질도 하였지만, 서비스의 성격과 기술을 함께 고려해서 저희 서비스에 어울리는 적정 기술로 서비스 성능 개선에 성공하여 매우 보람찼던 경험이었습니다. 특히, 저희 도메인에 특화된 핵심 기능을 개선할 수 있어 회사에게도, 저에게도 굉장히 뜻 깊은 일이었습니다. 🎉
wildcard subdomain과 middleware를 활용 방법에 관한 정보를 찾는 분들에게 많은 도움이 되었으면 합니다

profile
CDD(Coffee driven development)

1개의 댓글

comment-user-thumbnail
2023년 3월 5일

유의미한 성능 개선을 이루어 내셔서 매우 뿌듯하시겠군요! 유익한 글 잘 보고 갑니다!

답글 달기