React 랜딩을 Next.js SSG로 전환해 검색봇이 읽을 HTML을 만든 과정
기존 React SPA 랜딩의 JavaScript 렌더링 의존성을 줄이기 위해 Next.js App Router와 SSG를 도입하고 Googlebot·네이버 Yeti가 읽을 HTML, metadata, sitemap을 구성한 과정을 정리합니다.
기존 랜딩 페이지는 React 기반의 SPA로 구성돼 있었습니다. 브라우저에서 JavaScript가 실행된 뒤 화면을 만드는 구조는 사용자 인터랙션을 구현하기에는 편했지만, 검색엔진 입장에서는 첫 응답에 서비스 소개 문구와 페이지 구조가 충분히 들어 있지 않을 수 있었습니다.
이번 개편에서는 서비스 전체를 Next.js로 옮기지 않고 검색 유입의 입구가 되는 공개 랜딩 영역만 Next.js App Router와 SSG로 전환했습니다. 로그인 이후의 기능처럼 요청마다 데이터가 달라지는 화면이 아니라, 모든 방문자에게 같은 내용을 보여주고 변경 주기가 길다는 랜딩 페이지의 특성을 기준으로 내린 선택이었습니다.
기존 React SPA에서 무엇이 문제였을까
React 자체가 SEO에 불리한 것은 아닙니다. 문제는 렌더링 방식이었습니다. 일반적인 SPA는 서버가 비어 있거나 내용이 적은 HTML과 JavaScript 파일을 보내고, 브라우저가 스크립트를 실행한 뒤 실제 제목과 본문을 만듭니다.
아래 코드 예시는 설명을 위해 식별자와 운영 설정값을 일반화했습니다.
<div id="root"></div>
<script src="/assets/app.js"></script>
사용자 브라우저에서는 잠시 뒤 완성된 화면이 보이지만 검색로봇은 다음 작업을 추가로 수행해야 합니다.
flowchart LR A[URL 수집] --> B[내용이 적은 초기 HTML] B --> C[JavaScript 파일 요청] C --> D[스크립트 실행] D --> E[본문과 링크 생성] E --> F[페이지 해석과 색인]
이 과정에서 JavaScript 파일이 크거나, 실행 중 오류가 발생하거나, 필요한 리소스에 접근하지 못하면 사용자가 보는 화면과 검색로봇이 해석한 문서가 달라질 수 있습니다. 핵심 콘텐츠를 확인하기 위해 JavaScript 실행이 반드시 필요하다는 점이 공개 랜딩 페이지에는 불필요한 위험이었습니다.
Googlebot은 JavaScript를 실행하지만 즉시 색인하는 것은 아닙니다
Googlebot이 JavaScript를 전혀 실행하지 못해서 Next.js를 도입한 것은 아닙니다. Google은 Chromium 기반 렌더러로 JavaScript를 실행할 수 있습니다.
다만 Google의 JavaScript SEO 가이드에 따르면 JavaScript 페이지는 크게 크롤링 → 렌더링 → 색인 단계를 거칩니다. 초기 HTML이 앱 셸만 제공하면 실제 콘텐츠를 보기 위해 렌더링 대기열을 거쳐야 하고, 렌더링 시점은 즉시라고 보장되지 않습니다.
Google 관점에서 React SPA가 가질 수 있는 위험은 다음과 같았습니다.
- 초기 HTML만으로는 서비스 설명과 내부 링크가 충분히 보이지 않을 수 있음
- 렌더링 대기열을 거친 뒤에야 JavaScript로 만든 본문을 확인할 수 있음
- JavaScript 오류가 발생하면 해당 실행 이후의 콘텐츠가 누락될 수 있음
- robots.txt나 네트워크 문제로 JS·CSS 리소스가 차단되면 완성된 화면을 해석하기 어려움
- 클라이언트에서만 바뀌는 title과 description은 초기 응답에서 확인하기 어려움
Google도 서버 렌더링이나 사전 렌더링이 사용자와 크롤러 모두에게 유리하며, 모든 검색봇이 JavaScript를 실행하는 것은 아니라고 설명합니다. 따라서 Googlebot의 렌더링 능력에 기대기보다 첫 HTML에 제목과 본문을 함께 제공하는 방향을 선택했습니다.
네이버 검색로봇은 HTML 구조와 리소스 접근성을 함께 봅니다
국내 검색 유입을 고려하면 Google만 대응해서는 부족했습니다. 네이버 검색로봇인 Yeti가 페이지를 수집할 때도 HTML의 제목, 설명, 링크 구조와 필수 리소스에 접근할 수 있어야 합니다.
네이버 서치어드바이저의 리소스와 링크 관리 가이드는 페이지를 표현하는 데 필요한 JavaScript 등의 리소스가 robots.txt나 IP 차단으로 접근 불가능하면 페이지를 제대로 렌더링하지 못해 검색 결과가 다르게 나오거나 노출되지 않을 수 있다고 안내합니다.
또한 네이버 SEO 기본 가이드는 다음 항목을 문서 분석의 중요한 기준으로 안내합니다.
- 각 페이지를 설명하는 고유한
title - 페이지 내용을 요약하는
meta description - 문서의 핵심 주제를 나타내는 제목 구조
- 이미지 내용을 설명하는
alt - 검색로봇이 발견할 수 있는 일반적인 링크 구조
즉, 네이버 검색로봇이 JavaScript를 어느 수준까지 처리하는지를 가정하기보다 완성된 HTML만으로도 페이지의 주제와 이동 경로를 이해할 수 있게 만드는 것이 더 안전했습니다.
React를 버린 것이 아니라 Next.js로 렌더링 책임을 확장했습니다
Next.js를 도입한 이유는 React를 다른 UI 기술로 교체하기 위해서가 아닙니다. React 컴포넌트는 그대로 사용하면서, React만으로 직접 구성해야 했던 서버 렌더링과 검색엔진용 문서 생성을 프레임워크에 맡기기 위해서였습니다.
React SPA에서 별도로 조합해야 했던 기능을 Next.js App Router에서는 하나의 구조 안에서 처리할 수 있었습니다.
| 필요했던 기능 | React SPA에서의 부담 | Next.js에서 선택한 방식 |
|---|---|---|
| 초기 HTML 생성 | 별도 SSR·프리렌더링 구성 필요 | 빌드 시 정적 렌더링 |
| 페이지별 제목·설명 | DOM 또는 별도 head 라이브러리 관리 | Metadata API |
| 공개 URL 목록 | 별도 XML 생성 스크립트 필요 | sitemap.ts |
| 크롤링 정책 | 배포 파일을 직접 관리 | robots.txt 제공 |
| 이미지 전달 | 포맷·크기 정책을 별도 구성 | next/image |
| 경로 구성 | 라우터와 번들 설정 필요 | App Router 파일 라우팅 |
핵심은 React의 컴포넌트 개발 경험은 유지하면서 검색에 필요한 HTML 생성 책임을 Next.js에 추가한 것입니다. 페이지 안의 스크롤 이동과 반응형 상호작용은 Client Component로 남기고, 검색엔진이 읽어야 할 문서 정보는 빌드 결과에 포함했습니다.
왜 모든 화면을 SSR로 만들지 않고 랜딩만 SSG로 만들었을까
Next.js를 사용한다고 모든 요청을 서버에서 렌더링할 필요는 없습니다. 렌더링 방식은 화면의 데이터 성격에 따라 달라져야 합니다.
| 화면 성격 | 적합한 방식 | 이유 |
|---|---|---|
| 공개 랜딩·제품 소개 | SSG | 모든 방문자에게 같은 내용, 변경 주기가 낮음 |
| 로그인 이후 개인화 화면 | SSR 또는 CSR | 사용자·권한·요청 시점에 따라 데이터가 달라짐 |
| 실시간 상태 화면 | CSR 또는 동적 렌더링 | 최신 API 응답과 사용자 상호작용이 중요함 |
랜딩 페이지는 가격과 기능 소개가 배포 시점에 결정되고 요청별 사용자 데이터가 없었습니다. 코드에도 cookies, headers, 요청 시점의 fetch처럼 동적 렌더링이 필요한 요소가 없었습니다.
이 조건에서 SSR을 사용하면 방문할 때마다 같은 HTML을 서버가 반복해서 만들어야 합니다. 반면 SSG는 빌드할 때 HTML을 한 번 만들고 이후 요청에는 생성된 결과를 재사용할 수 있습니다.
flowchart LR A[콘텐츠 배포] --> B[빌드 시 HTML 생성] B --> C[정적 결과 캐시] C --> D[Googlebot 요청] C --> E[네이버 Yeti 요청] C --> F[사용자 요청]
랜딩에 SSG를 선택한 이유는 다음과 같습니다.
- 검색로봇이 JavaScript 실행 전에도 제목과 본문을 읽을 수 있음
- 요청마다 서버 렌더링을 반복하지 않아도 됨
- 동일한 HTML을 안정적으로 캐시하고 전달할 수 있음
- 서버 상태와 무관하게 공개 페이지를 제공하기 쉬움
- 콘텐츠가 바뀌는 배포 시점에만 다시 생성하면 됨
변하지 않는 공개 콘텐츠에 SSR 비용을 지불할 이유가 없었고, SPA의 렌더링 의존성도 줄일 수 있었기 때문에 SSG가 맞았습니다.
Client Component가 있어도 정적 HTML을 만들 수 있습니다
메인 랜딩에는 스크롤 위치에 따른 버튼 노출처럼 브라우저에서만 가능한 기능이 있어 "use client"가 필요했습니다.
"use client";
export default function Home() {
const { scrollY } = useScroll();
return (
<Frame>
<HeroSection />
<FeatureSection />
<ContactSection />
</Frame>
);
}
Client Component라고 해서 반드시 빈 HTML만 전달되는 것은 아닙니다. Next.js는 정적 렌더링 가능한 경로의 Server Component와 Client Component 결과를 빌드 시 HTML로 만들고, 브라우저에서는 JavaScript가 연결되며 상호작용이 활성화됩니다.
이를 통해 본문은 정적 HTML로 제공하고 스크롤·버튼 동작만 hydration 이후 실행할 수 있었습니다. SEO를 위해 인터랙션을 포기하거나, 인터랙션 때문에 전체 페이지를 SPA로 되돌릴 필요가 없었습니다.
Metadata를 초기 HTML에 포함했습니다
루트 레이아웃에는 사이트 전체의 기본 제목과 설명을 선언했습니다.
export const metadata: Metadata = {
title: "서비스 소개",
description:
"서비스의 주요 기능을 소개합니다.",
};
키오스크, 관리자, 배리어프리처럼 검색 의도가 다른 경로에는 별도의 metadata를 두었습니다.
export const metadata: Metadata = {
title: "서비스 소개 - 키오스크",
description: "키오스크 서비스 소개",
};
Next.js의 정적 metadata는 빌드 시 각 페이지의 head에 포함됩니다. React가 실행된 뒤 document.title을 바꾸는 방식과 달리, Google과 네이버 검색로봇이 초기 HTML에서 페이지별 제목과 설명을 바로 확인할 수 있습니다.
네이버는 여러 페이지가 같은 title이나 description을 사용하면 어떤 문서를 검색 결과에 노출해야 하는지 판단하기 어려울 수 있다고 안내합니다. 그래서 하나의 서비스 이름만 반복하기보다 각 경로의 목적을 구분했습니다.
sitemap과 robots.txt로 발견 경로를 명시했습니다
SSG로 HTML을 만들어도 검색로봇이 URL을 발견하지 못하면 수집되지 않습니다. 공개할 랜딩 경로를 sitemap.ts에 명시했습니다.
export default function sitemap() {
const baseUrl =
process.env.PUBLIC_SITE_URL || "https://example.com";
const now = new Date().toISOString();
return [
{ url: `${baseUrl}/`, lastModified: now },
{ url: `${baseUrl}/kiosk`, lastModified: now },
{ url: `${baseUrl}/barrier-free`, lastModified: now },
{ url: `${baseUrl}/manager`, lastModified: now },
{ url: `${baseUrl}/user-app`, lastModified: now },
];
}
robots.txt에서는 공개 페이지의 수집을 허용하고 sitemap 위치를 연결했습니다.
User-agent: *
Disallow: /api/
Disallow: /_components/
Allow: /
Sitemap: https://example.com/sitemap.xml
네이버 robots.txt 가이드도 sitemap 위치를 robots.txt에 기록하면 검색로봇의 콘텐츠 수집을 도울 수 있다고 설명합니다. sitemap은 공개 URL을 알려주고 robots.txt는 접근 범위를 알려주는 역할로 분리했습니다.
이미지도 브라우저 표시와 검색 해석을 함께 고려했습니다
랜딩 페이지에는 큰 이미지가 많아 next/image를 사용했습니다. Next.js의 이미지 처리는 크기와 포맷 전달을 최적화하는 데 도움이 되지만, 검색엔진이 이미지의 의미까지 자동으로 알게 되는 것은 아닙니다.
<Image
src={sectionImage}
alt={section.title}
className={styles.sectionImage}
/>
콘텐츠 이미지에는 주변 문맥에 맞는 alt를 제공해야 Google 이미지 검색과 네이버 검색로봇, 스크린 리더가 의미를 파악할 수 있습니다. 장식 이미지는 빈 alt를 사용하고, 정보를 전달하는 이미지는 파일명이 아니라 이미지가 설명하는 내용을 작성하는 기준을 세웠습니다.
적용 범위와 남은 보완점
이번 작업으로 React SPA가 JavaScript를 실행한 뒤에야 제공하던 공개 콘텐츠를 Next.js의 정적 HTML로 옮기고, metadata·sitemap·robots.txt를 함께 구성했습니다. 랜딩 페이지와 검색로봇 설정도 운영 서비스에 반영됐습니다.
다만 코드 기준으로 다음 보완점은 남아 있었습니다.
- 한국어 페이지의 루트
html lang이en으로 지정됨 - 사용자 앱 경로의 전용 metadata가 없어 루트 값을 상속함
- 일부 이미지의
alt가banner_img처럼 의미를 설명하지 못함 - canonical, Open Graph, 구조화 데이터가 구성되지 않음
- sitemap의
lastModified가 실제 수정일이 아니라 생성 시각을 사용함 - 첫 화면 배너에
priority와 반응형sizes가 지정되지 않음
따라서 이 작업을 검색 순위나 LCP가 개선됐다는 결과로 표현하지는 않았습니다. 그 효과를 판단하려면 Search Console과 네이버 서치어드바이저의 색인·노출 데이터, 실제 성능 지표를 전후로 비교해야 합니다.
정리
Next.js 도입의 목적은 단순한 프레임워크 교체가 아니었습니다. 기존 React 컴포넌트를 유지하면서 검색 유입이 필요한 랜딩 페이지만 정적 HTML로 제공하기 위한 선택이었습니다.
- React SPA의 핵심 콘텐츠가 JavaScript 렌더링에 의존하는 범위를 줄였습니다.
- Googlebot의 렌더링 대기와 JS 실행 실패 가능성에 덜 의존하게 했습니다.
- 네이버 Yeti가 HTML 구조와 필수 리소스를 안정적으로 해석할 수 있게 했습니다.
- 요청마다 내용이 같다는 랜딩의 특성에 맞춰 SSR 대신 SSG를 사용했습니다.
- Metadata, sitemap, robots.txt를 App Router 안에서 함께 관리했습니다.
- 사용자 인터랙션은 Client Component로 유지했습니다.
결국 기준은 단순했습니다. 모든 사용자에게 같은 공개 콘텐츠라면 빌드할 때 완성하고, 요청마다 달라지는 기능만 동적으로 처리한다. 이 원칙으로 SEO와 운영 비용, 기존 React 개발 경험을 함께 가져갈 수 있었습니다.