1. 정적 빌드를 해보자#
지난 글에서 제 페이지의 성격과 잘맞는 렌더링 방식을 탐구하였고, 정적 페이지 빌드와 풀라우트 캐싱(HTML + RSC Payload) 방식을 채택하였습니다.
이번 글에서는 정적 빌드를 하면서 겪은 점들을 SEO와 엮어 공유해보려 합니다 :)
빌드 명령을 내리면 Next는 명시적(라우트 세그먼트)으로 페이지 렌더링 방식을 지정하지 않는 한 몇몇 기제가 존재하는지에 따라 정적/동적 페이지인지 판단하여 빌드를 시작합니다.
저는 정적으로 빌드를 원하기에, 동적 빌드 기제만 주의하면 되었는데요.
피드 페이지를 빌드하는 과정에서 아래와 같이 의도치 않게 동적 빌드를 하게 되는 문제를 겪었습니다.
2. Supabase SSR client의 cookies 사용#
이 블로그는 모든 페이지가 SEO가 중요한 페이지로 서버측 렌더링을 활용하기로 했고, 따라서 데이터를 가져오기 위한 supabase client를 SSR용으로 미리 셋업을 해놓은 상태였습니다.

다만 이 client는 next/headers의 동적함수인 cookies의 사용을 요구하였는데요.
cookies는 유저로부터 요청이 들어왔을 때 서버에서 쿠키로 받은 유저의 토큰 정보를 기반으로 페이지를 그리기 위한 용도이므로 런타임 서버에서만 사용할 수 있는 정보입니다.
그러기에 Next는 이 cookies라는 동적 함수를 호출한 페이지에 대해서는 항상 동적 빌드를 진행하게 됩니다.
제가 동적빌드를 하지 않는 이유는 아래와 같은데요.
- 피드 페이지는 cookies로 유저정보를 토대로 그려야 하는 페이지도 아니고 동적 렌더링 시 TTFB가 현저히 늦어지기 때문
- 정보 전달이 목적인 페이지이므로 여타 페이지에서도/가까운 미래에서도 유저정보를 기반으로 페이지를 구성할 일이 없어보인다는 점
- 나중에 좋아요나 싫어요같은 유저 정보가 필요한 기능이 추가되더라도 브라우저에서 유저측에서 직접 요청을 보내게하여 CSR을 할 것이기 때문
유저정보 기반 기능을 CSR로만 고려하는 이유?
- 추후 페이지의 CDN 캐싱을 고려하고 있기 때문입니다.
그게 무슨 상관이 있어요?
- SSR을 한다면 오리진이 될 내 Next 서버가 응답한 페이지가 CDN에 캐싱될 것인데요
- 이 때 유저정보가 입혀진 페이지가 캐싱이 되면 안되기 때문입니다.
- ex) 좋아요 버튼이 눌러진 채로 페이지가 캐싱되는 것을 방지
- 따라서 유저정보가 입혀지지 않은 상태의 페이지를 유저가 응답받은 후 CSR을 통해 좋아요를 눌렀다는 것을 표시할 것 입니다.
따라서 SSR 클라이언트를 제거하고 일반 JS 클라이언트를 채택하였습니다.
3. searchParams 사용#
우선 앞서, 제가 왜 searchParams를 사용했는지 설명드리자면
1) 필터된 리스트 페이지의 인덱싱#

피드에서는 글의 카테고리에 따라 리스트를 필터하는 기능이 있습니다.
저는 이 필터된 페이지들 각각이 모두 검색엔진에 인덱싱되길 바랬는데요.

그 이유는 현업에서 비슷하게 리스트 형태의 페이지가 있었고 카테고리에 따라 필터가 가능한 기능이 있었습니다.
기존에는 필터된 페이지들에 대해 캐노니컬로 하나로 묶어서 처리했었으나
💡 캐노니컬이란? 중복 콘텐츠 문제를 해결하기 위한 SEO 요소입니다.
예를 들어
- https://example.com/products/shoes
- https://example.com/products/shoes?color=red
- https://example.com/products/shoes?utm_source=facebook
- https://www.example.com/products/shoes
같이 비슷한 주소가 여러개 있다고 할 때 검색엔진의 크롤링 리소스가 분산됩니다.
필터된 리스트 페이지 각각이 다른 의미로서 고유한 가치를 제공한다고 생각하여 캐노니컬을 제거하는 것을 원하였고
그 페이지에 대한 대규모 리뉴얼이 이루어질 때 필터별 페이지의 캐노니컬을 제거하여 모두 인덱싱이 가능하게 처리되면서 노출량이 급증하게 되어 지표에 좋은 영향을 끼쳤기 때문입니다.
2) 필터수단으로서 searchParams 사용하려 했으나..#
따라서 이 매커니즘을 블로그에 구현하기 위해 카테고리로 피드 리스트 페이지를 필터하는 수단으로 searchParams를 사용했는데요.
- ex) https://www.choiseongjun.com/?categoryId=1

이는 런타임에서 요청이 들어왔을 때 알 수 있는 동적인 값이라 일반적으로는 동적 빌드로 처리됩니다.
저는 path 역할을 하는 params가 미리 빌드타임에 특정 몇개의 값을 뽑아내어 정적빌드를 할 수 있도록 돕는 generateStaticParams를 갖고 있듯,
페이지의 변수값 역할을 하는 searchParams도 이에 상응하는 함수가 있을 것이라 생각했는데요... (얼추 generateStaticSearchParams라는 이름으로..)
아니더군요..🥲
params로 구분되는 path는 엄연히 페이지의 성격을 가지나, searchParams는 변수값정도로 여겨지기 때문이 아닐까 싶습니다.
Next가 의도한 바는 SEO가 중요하면 개별 리소스로 표현해라. 즉, 핵심 콘텐츠는 경로로 표현해라라는 규칙을 강제한 것 같습니다. 그러니까 프레임워크인거죠.
4. id대신 slug path를 사용해보자#
정적 빌드를 위해서 generateStaticParams를 활용하기로 하였고, 처음에는 리스트 페이지를 필터할 식별자로 카테고리의 id를 고려했습니다.
- ex) https://www.choiseongjun.com/category/1
다만 공식문서 등 요새 SERP에 등장하는 정보 전달성 페이지를 보면 이렇게 유저 입장에서는 이해할 수 없는 식별자가 아닌 읽기 쉬운 유니크한 단어를 path로 쓰는 케이스가 많이 보이는데요.
- ex) https://f-lab.kr/blog/developer-blog-tips
이 것이 실제로 SEO 차원에서 의미가 있는지 문득 궁금해지더군요.
따라서 구글 검색엔진의 공식문서를 조사해보았습니다.

네. 설명 URL이라는 항목으로 명확하게 나와있네요. 네이버에서도 검색 친화적인 URL에 대해 언급한 바가 있구요.
즉, 구글과 네이버 모두 유저에게 도움이 될 만한 키워드를 URL에 포함시킬 것을 권장합니다.
다만 스크롤을 내리다보니 좀 모순되는 것 같은 의아한 워딩도 발견했는데요.

도움이 될 만한 키워드를 path로 구성하는 것은 유저 입장에서 더 많은 선택을 유발할 수 있으니 권장한다. 다만 검색엔진이 너의 페이지의 순위를 매기는 요소는 아니다.
라는 듯한 느낌으로 공식문서에서는 말합니다.
개인적으로도 /post/1보단 /post/how-to-make-pizza 라는 주소가 더 클릭하고 싶게 생겼긴 합니다.
결론은 기술적으로 장점은 없으나 유저 관점에서 클릭을 유발할 수 있으니 적용을 권장함이므로 적용하지 않을 이유가 없네요.
이렇게 id 대신 slug를 path 식별자로 적용을 하게 되었습니다.
그리고 비슷한 고민을 가진 포스트 상세 페이지에서도 마찬가지로 적용하게 되었습니다.
- ex) https://www.choiseongjun.com/post/caching-strategy-i-implemented
지금까지 정적 페이지로 빌드하기 위해 고민하며 적용한 것들을 정리해보았는데요.
내용이 좀 길어지는 것 같아 캐싱에 대한 내용은 다음 글에서 다루겠습니다 :)