Next.js SSG(static site generation), SEO를 위한 meta 태그 적용하기

gon Kim
elecle
Published in
12 min readDec 2, 2021

Next.js와 SSG

react를 활용하는 대부분의 서비스에서 보다 더 안정적이고 효율적인 개발이 가능한 프레임워크인 Next.js를 이용합니다. Next.js를 이용하면 기본 제공되는 함수와 설정을 통해 SSR(Server Side Rendering)을 훨씬 더 손쉽게 구현할 수 있는데요, SSR 뿐만 아니라 정적인 HTML을 생성해주는 SSG(Static Site Generation)를 위한 export도 지원합니다.

일레클에서는 회사 홈페이지와 서비스 웹뷰를 Next.js로 개발하여 SSG 방식으로 배포를 진행하고 있습니다. 서버사이드 렌더링이 필요해지는 시점이 언젠가 올 것이라고 생각하고 있지만, Next.js에서도 서버사이드 렌더를 구현하기 위한 코드와 이를 관리하기 위한 인적 리소스, 현재 dynamic SEO가 필요하지 않다는 점, 그리고 배포와 devops 측면에서의 인프라 관리 비용을 고려하여 SSG 방식을 채택했습니다.

Next.js에서 next export라는 명령어로 손쉽게 생성가능한 정적 HTML파일들은 aws s3-cloudfront 혹은 aws amplify, Firebase hosting이나 Azure를 통해 쉽게 배포할 수 있으며, 필요시 ec2나 직접 관리하는 서버 상에 올려 nginx를 이용하여 직접 관리할 수도 있습니다. (aws s3과 cloudfront를 통한 배포에 대해 더 알아보고 싶다면 지난 글(AWS CodePipeline과 Amazon s3를 통한 정적 웹사이트 CI/CD 구축(Next.js))을 확인하세요!)

그러나 실제로 SSG 방식으로 1년 이상 서비스를 운영해보니 Next.js에서 공식적으로 제공하는 SSG 임에도 Documentation이나 배포 인프라 서비스에 따라 추가적인 설정이 필요하거나 예상과 다르게 작동하는 경우가 매우 많았습니다.

1. 쿼리스트링/slash 처리 문제

첫번째로 특히, url상의 쿼리스트링이나 ending trailing slash의 처리는 aws에서 기본적으로 지원하지 않고 있었는데요.(관련 github issue) 원하는 대로 (일반적인 url접근 방식이 정상적으로 처리가 되도록) 작동시키기 위해서는 s3이나 amplify에 rewrite/routing rule을 추가하거나 s3-cloudfront 앞단에 lambda@edge를 붙일 수 밖에 없는데, 특히 lambda@edge설정을 적절하게 해준다면 마치 nginx config을 변경하듯 마음대로 컨트롤할 수 있지만, 해당 함수를 관리하는 리소스가 추가적으로 발생했습니다.(또한 product/dev환경에서의 lambda@edge설정을 제대로 테스팅하기 위해서는 aws terraform등을 통해 aws서비스를 programmatic하게 다루어야 하는 등 제대로 된 환경을 구축하는 데 리소스가 상당하다고 판단하였습니다.) aws자체의 문제 이외에도 Next.js에서 router에서 쿼리스트링을 읽어오는 데 SSG를 통해 배포했을 때는 정상적으로 작동하지 않는 문제도 있었습니다. (하여 router의 query 대신 URLSearchParams를 통해 직접 읽어올 수 밖에 없었습니다. 이는 hot reload를 지원하는 ssr방식으로 구동되는 next dev를 통한 로컬 개발 환경과 next export를 통한 ssg배포의 작동 방식이 달라 개발/프로덕트 실행 환경이 근본적으로 동일할 수 없는 상황 그 자체도 개발상 큰 문제라고 생각됩니다.)

2. next/head의 meta 태그가 페이지 접근 이후 렌더되어 발생하는 문제

Next.js 공식 홈페이지의 next/head api 문서(https://nextjs.org/docs/api-reference/next/head)에서 확인할 수 있는 대로 meta태그와 og태그는 페이지 접근 이후 렌더 됩니다. SSR에서는 렌더된 페이지를 응답으로 받기 때문에 SEO나 og태그를 이용하여 컨텐츠를 공유할 때 작성한 데이터 정상적으로 노출이 됩니다. 하지만 SSG를 통해 생성된 정적 파일에서는 meta태그와 og태그는 페이지에 접근한 이후 렌더되기 때문에 미리보기 링크나 페이지별 SEO는 next/head를 페이지에 구현하는 것으로는 기본적으로 불가능합니다.

이번 포스팅을 통해서는 두번째, meta태그가 페이지 배포시점이 아니라 접근 시에 렌더되어 발생하는 문제를 분석해보고, 해결 과정을 공유하고자 합니다.

문제 상황

구글 자바스크립트 검색엔진은 페이지 접근 이후 렌더를 통해 meta태그로 설정된 title과 contents까지도 잘 보여주지만, 일반적으로 소셜 미디어 공유 preview를 위해 설정하는 og태그를 next/head를 이용하여 각 페이지 파일 내에서 설정을 해도 SSG방식의 배포시 meta태그가 생성되지 않습니다.

next build, next export

next build 커맨드를 실행하면 페이지 파일에 getServerProps, getStaticProps 함수가 포함되어 있는지, 아닌지에 따라 static한 html파일을 생성할지, server side로 렌더할 페이지인지 판단하여 빌드 파일을 .next 디렉토리에 생성합니다.

next build 실행시 친절하게 알려줍니다.

이후 next export 커맨드를 실행하면 정적 html파일이 기본 out directory로 export됩니다.

원인

서버사이드 렌더의 경우에는 페이지 소스를 요청하는 시점에 메타태그를 렌더하여 태그가포함된 HTML파일을 생성하여 응답하기에 검색엔진이 메타태그를 읽는데 문제가 없습니다. 그러나 먼저 말씀드린 것과 같이 next export를 통해 생성된 정적 HTML파일에서는 meta태그와 og태그가 페이지에 접근한 이후 렌더되기 때문에, 미리보기 링크나 페이지별 SEO는 next/head를 페이지에 구현하는 것으로는 기본적으로 불가능합니다. 하지만, 의문이 듭니다. 빌드 시점에 HTML 파일을 생성한다면, 빌드 시에 생성되는 HTML파일에 필요한 head태그를 붙이면 되지 않나?

SSG 방식에서 meta태그를 설정하려면?

ssr 방식에서 페이지별로 다른 meta태그가 아니라 공통의 기본 헤더를 설정하기 위해서는 생성된 html과 body태그를 감싸 외부의 공통적인 스크립트/head태그를 커스텀할 수 있도록 _documents 파일을 생성하여 적용할 수 있습니다.(https://nextjs.org/docs/advanced-features/custom-document) Static Site Generation방식에서 _documents는 어떻게 작동할까요?

  • Document is only rendered in the server, event handlers like onClick won't work.

Next.js Custom Document 의 주의사항에서는 위와 같이 Document가 서버에서만 렌더된다고 설명합니다. SSG방식으로 정적 파일을 생성하는 경우에는 빌드/export 시점에 Custom Document가 각 페이지 HTML이 생성될 때 공통적으로 적용되어 페이지 HTML파일마다 들어가게 됩니다. Custom Document를 작성하고 next build, next export를 실행하여 실제 생성된 HTML파일을 보면 작성된 태그가 잘 들어가있음을 확인할 수 있습니다. 이를 통해 공통의 Custom Document에 메타 태그를 설정하여 검색 엔진 노출, 소셜 미디어 공유시 원하는 문구와 이미지를 보여줄 수 있는 것이죠.

해결되지 않은 문제 — 페이지별로 설정할 수는 없다.

Custom Document를 통해 모든 페이지에서 동일하게 보이는 head 태그를 생성할 수 있지만, 그렇게 해도 페이지별로 다른 meta태그를 설정하여 검색엔진에 디테일한 정보를 주고자 하는 목적이 달성되지 않습니다. 위에서 설명드린대로 페이지에서 next/head를 통해서 설정해도 클라이언트 사이드에서 렌더를 통해 실행되기에 정상적으로 검색 엔진에 노출시킬 수 없고요.(브라우저 상단 타이틀을 페이지에서 설정하면 접속 이후 렌더되어 잘 보이지만 생성된 HTML파일을 보면 페이지에 하드코딩된 것이 아니라 js에 의해 클라이언트 사이드에서 페이지 접속 이후 렌더가 됩니다.)

해결: getStaticProps에서 정의하여 Document에서 접근

그런데, 배포를 위한 빌드 시점에 Document를 포함한 정적 파일이 생성된다면... 그렇다면 페이지별 HTML이 Document와 함께 생성되는 시점에 넣으면 되는 것 아닐까? 라는 생각이 들었습니다. 또한 SSG방식에서 빌드 시점에 getStaticProps 함수를 통해 static generation에서 빌드시에 실행되어야 한다고 명확히 정의하고, Document에서 각 페이지의 getStaticProps에서 리턴하는 값들에 어떻게든 접근할 수 있을 것이라고 생각하였습니다. Custom Document에서 props로 받는 인자를 (노가다로) 분석해보니, 렌더 함수 내부에서 this.props.__NEXT_DATA__.props.pageProps 값이 바로 getStaticProps에서 리턴한 값들을 데이터로 가지고 있으며, 빌드 시점에 접근 가능하다는 점을 확인할 수 있었습니다. Next.js 소스를 찾아보니 __NEXT_DATA__ 에 페이지의 여러 데이터를 JSON으로 넣어 이용하고 있지만, 이는 공식적으로 도큐멘테이션 된 내용이 아니기에 언제든 바뀌거나 예상치 못한 동작이 발생할 수 있음을 유의하여 이용해야 합니다.

정리

페이지별로 Document에서 정의된 Script를 포함하여 HTML 생성됩니다. 이 때, 페이지별로 static render를 위해 getStaticProps가 선언되어 있다면 페이지 데이터(__NEXT_DATA__.props.pageProps)로 페이지에서 선언된 함수 리턴값을 Document에 가져와서 이용할 수 있고, 페이지별 head태그를 설정할 수 있다. 간단하게 코드를 통해 정리하면 다음과 같습니다.

1. blog.tsx 페이지에서 임의의 변수 이름으로 Document에서 접근할 수 있도록 적절한 값을 리턴하도록 한다.

2. _documents.tsx 커스텀 도큐먼트를 생성하여 페이지의 pageProps에 접근하여 태그에 적절한 값이 들어갈 수 있도록 한다.

페이지에서 ogTitle, ogDescription, ogImage로 정의되어 getStaticProps에서 리턴하는 값이 있다면 해당 값을, 없다면 디폴트 값을 보여줍니다.

3. 빌드 시점에 _document.tsx를 통해 정상적으로 파싱된 값이 meta태그에 잘 들어가 생성된 HTML파일에서도 이를 확인할 수 있다.

next export를 통해 생성된 blog/index.html의 최상단 코드

4. 검색엔진이나 소셜미디어 공유시 이를 확인할 수 있다.

opengraph.xyz에서 확인할 수 있는 정상적으로 적용된 og:title과 og:description

걱정거리

1. __NEXT_DATA__의 pageProps에 접근이 앞으로 지속적으로 지원될 지는 모릅니다. 오피셜 도큐멘테이션이 있는 것도 아니고, SSG를 위한 구체적인 설명을 도큐멘테이션하는 편도 아니기에 당장 다음 업그레이드에서 이를 구현하는 방식이 어떻게 변경될지 모릅니다.

2. 생각해보면 어차피 이렇게 배포 시점에 static한 태그를 넣는 것인데 next/head에서는 왜 지원하지 않는가? 이 듭니다. 아마도 next/head에서는 렌더 시점에서 특히나 서버사이드 렌더의 경우 DB를 조회하는 등의 추가적인 데이터 페칭을 통해 더욱 더 적절한 meta태그를 페이지 접근 시점에 즉시 제공할 수 있을 것이므로 ssg에서 빌드 시점에서의 처리를 커버하지 않았을 것으로 추측됩니다. 다만 Custom Document를 이용하여 배포하는 것이 가능하므로, 동일하게 작동하도록 next/head나 Custom Document를 통한 meta 태그 적용이 SSG에서도 작동할 수 있도록 충분히 기능을 지원할 수 있을 것이라고 생각됩니다.(수요와 이슈 제기의 문제..아닐지..)

3. Custom Document 기능은 SSG방식으로 배포 시에도 공통된 Head나 Script가 처리되는 데 매우 잘 작동하지만, 문서가 상당히 불친절합니다.. 사실 SSG와 관련된 기능에 대해 문서화가 불친절한데, 비교되는 vue의 Nuxt.js의 경우 서버사이드 렌더와 SSG상의 차이가 Next.js처럼 크게 느껴지지 않기에 SSG에서 어떻게 동작하는지에 대한 문서를 찾는 경우가 애초에 없을 정도로 동일하게 작동하고 있었기 때문입니다.

Next.js를 ssg방식으로 배포하여 실제 서비스를 운영하는 개발자 분들을 찾습니다.

SSG가 분명 편하고.. 리소스도 줄고.. 성능상의 이점도 가져가면서 동적인 url 맵핑이 필수가 아닌 이상 편한데.. 어떻게든 되는데.. (Next.js 자체에서의 한계일수도 있지만) 편한게 아닌 것 같다는 생각이 자꾸 듭니다. 혹시 실제 서비스에서 Next.js를 SSR이 아니라 SSG로 사용하시는 다른 개발자분들이 있으실까요? 맞닥뜨리는 문제를 어떻게 해결하고 계신가요? 혹은 굳이 SSG방식으로 개발하는 것보다 SSR이 나은 점은 무엇이 있을까요? Next.js로 서비스를 운영하시는 개발자 분들의 많은 조언 부탁드립니다!

--

--