직접 Next.js로 블로그를 만들면서, SEO 설정이 생각보다 손이 많이 간다는 걸 알았습니다. 프레임워크가 제공하는 metadata API는 편리하지만, 제대로 쓰지 않으면 페이지마다 검색 엔진에 보여줘야 할 정보가 조용히 사라집니다. 빌드가 성공하고 페이지가 렌더링되더라도, canonical이 엉뚱한 도메인을 가리키거나 RSS 링크가 없어지는 일이 생겼습니다.
metadata.alternates가 생각대로 동작하지 않는다#
가장 처음 넘어졌던 지점은 alternates였습니다. 레이아웃에서 RSS 피드를 link alternate로 선언했는데, 개별 포스트 페이지를 열면 RSS 링크가 사라져 있었습니다.
이유는 문서에 명시된 동작이었습니다. Next.js의 metadata에서 alternates는 중첩 세그먼트에서 얕게 통째로 대체됩니다. 자식 페이지에서 canonical만 바꾸려고 alternates: { canonical: "/posts/slug" }를 썼더니, 부모 레이아웃이 설정한 <link rel="alternate" type="application/rss+xml">가 모든 자식 페이지에서 없어진 것입니다.
해결은 alternates를 건드려야 하는 모든 페이지에서 RSS 링크까지 함께 반환하는 헬퍼를 만드는 것이었습니다.
export function siteAlternates(canonical: string) {
return {
canonical,
types: {
"application/rss+xml": `${SITE_URL}/feed.xml`,
},
};
}
이 헬퍼를 layout, home, posts 목록, 개별 포스트, 카테고리 페이지까지 alternates를 쓰는 모든 곳에서 호출하면 RSS 링크가 일관되게 유지됩니다. 헬퍼 하나가 정책을 강제하는 구조입니다. 왜 shallow merge가 일어나는지와 openGraph까지 같은 함정이 퍼지는 패턴은 Next.js 16 metadata 자식 segment override 함정 글에 더 자세히 풀어 두었습니다.
canonical URL과 도메인 일관성#
SEO 설정에서 자주 놓치는 부분이 canonical URL의 도메인 일관성입니다. 로컬 개발용 URL이나 스테이징 도메인이 canonical 또는 og:url에 섞이면, 검색 엔진이 동일한 콘텐츠를 다른 두 URL로 인식합니다.
저는 NEXT_PUBLIC_SITE_URL 환경변수를 단일 진실 소스로 두고, 값이 없을 때를 대비한 폴백을 포함해 관리합니다.
export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ??
"https://lifeisonecoin.com";
export const siteMetaBase = new URL(SITE_URL);
Cloudflare Pages 같은 정적 호스팅에서는 빌드 시 환경변수가 주입되고, OS 환경변수가 .env.local보다 우선합니다. 운영 환경에서 NEXT_PUBLIC_SITE_URL을 정확히 설정해두면 canonical, sitemap, RSS 피드의 도메인이 모두 같은 값을 가리킵니다.
로컬에서 운영 도메인 기준으로 빌드 결과를 검증하고 싶을 때는 NEXT_PUBLIC_SITE_URL=https://lifeisonecoin.com npm run build처럼 OS env를 통해 오버라이드하면 됩니다.
sitemap과 RSS 피드를 직접 구현하는 이유#
Next.js의 app/sitemap.ts는 정적 export 모드에서도 잘 동작하지만, URL을 세밀하게 제어하고 싶다면 직접 구현하는 편이 낫습니다. noindex 처리된 포스트를 sitemap에서 제외하거나, 카테고리 페이지를 별도로 포함하는 경우가 대표적입니다.
RSS 피드도 마찬가지입니다. <description>을 CDATA로 감싸지 않으면 HTML이 포함된 콘텐츠에서 W3C Feed Validator 경고가 생깁니다. 직접 생성하면 이런 부분을 정확하게 제어할 수 있고, 형식 검증도 CI 단계에서 명시적으로 추가할 수 있습니다.
function wrapInCdata(str: string): string {
return `<![CDATA[${str.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
}
검색 엔진 등록은 코드와 별개다#
코드 쪽 SEO 설정을 마쳐도 Google Search Console, Naver Search Advisor 등록과 sitemap 제출은 별도로 해야 합니다. canonical이 올바르게 설정되어 있어도 크롤링이 이루어지기 전까지는 반영되지 않습니다.
한국어 콘텐츠라면 Naver Search Advisor 등록은 Google 못지않게 중요합니다. HTML 태그 방식으로 인증 토큰을 발급받아 <meta name="naver-site-verification"> 형태로 추가하고, 인증 완료 후 sitemap URL을 제출하면 됩니다. 토큰은 환경변수로 관리하면 코드에 직접 노출되지 않습니다.
SEO는 코드 한 번 설정하면 끝나는 작업이 아닙니다. 발행한 콘텐츠가 실제로 색인되고 있는지, canonical이 올바르게 인식되고 있는지 주기적으로 확인하는 습관이 따라와야 합니다. 저는 이 블로그를 처음 만들면서 유입 0에서 시작한 경험이 있는데, 코드보다 색인 확인과 콘텐츠 발행 주기가 더 중요하다는 걸 그때 배웠습니다.