데모
도구 데모 페이지
SSR이란?
Demo 페이지는 일반 사용자를 위한 실용 도구라기보다 서버 사이드 렌더링 경로를 확인하기 위한 작은 참고 페이지입니다. Next.js App Router 페이지가 서버에서 데이터를 가져오고, 그 결과를 첫 HTML 응답에 렌더링하며, 현재 언어의 번역 문구도 함께 불러올 수 있음을 보여 줍니다. 환경 변수, 서버 측 서명 요청, API 응답 형식, locale 라우팅, 번역 로딩 같은 공통 기반이 올바르게 연결되어 있는지 확인하는 데 적합합니다. 이 페이지가 정상 동작하면 복잡한 도구 페이지를 디버깅하기 전에 기본 SSR 경로가 건강하다고 볼 수 있습니다. 실패한다면 개별 도구보다 공통 서버 설정이나 API 연결 문제를 먼저 의심해야 합니다.
사용 방법
사용 방법
- 페이지가 서버에서 백엔드 API를 자동으로 호출합니다
- HTML에 미리 렌더링된 콘텐츠가 포함되어 있어 클라이언트 요청이 필요 없습니다
- API에서 반환한 데이터를 확인하세요
- SEO 최적화가 필요한 페이지에 적합합니다
개발 참고사항
- 이 페이지를 SSR, 로케일 로딩, 서명된 서버 요청, API 연결에 대한 스모크 테스트로 활용하세요.
- 이 페이지가 작동하지 않으면 특정 도구 페이지를 디버깅하기 전에 공유 서버 설정을 확인하세요.
활용 사례
기술 원리
이 데모 페이지는 Next.js 16 App Router 기반으로 기본적으로 서버 사이드 렌더링(SSR)을 사용합니다. 사용자가 페이지를 요청하면 Node.js 서버가 먼저 React 컴포넌트를 실행하고, 백엔드 API를 호출하여 데이터를 가져온 뒤 완전히 렌더링된 HTML을 단일 응답으로 브라우저에 반환합니다. 브라우저는 즉시 HTML 파싱과 페인팅을 시작하므로, JavaScript가 다운로드되고 실행될 때까지 기다리지 않고 첫 프레임에서 완전한 콘텐츠를 볼 수 있습니다. 이는 전통적인 클라이언트 사이드 렌더링(CSR)과 대조됩니다. CSR에서는 서버가 거의 빈 HTML 셸과 JS 링크만 반환하고, 브라우저가 JS 번들을 다운로드하고, 프레임워크 초기화를 실행하고, API를 호출하여 데이터를 가져온 뒤에야 페이지를 렌더링합니다. 사용자 관점에서는 "한동안 빈 화면"이며, 검색 엔진 크롤러에게는 더 나쁩니다. 대부분의 크롤러는 JavaScript를 실행하지 않고 빈 셸만 보게 됩니다. SSR 후 Next.js는 "하이드레이션"을 수행하기 위한 JavaScript 청크도 주입합니다. 기존 DOM을 React 컴포넌트 트리에 연결하고 이벤트 리스너를 바인딩하여 페이지를 대화형으로 만듭니다. 전체 흐름은 HTML 파싱 -> CSS 로드 -> JS 다운로드 -> 하이드레이션 -> 대화형입니다. Core Web Vitals 중 LCP(Largest Contentful Paint)와 CLS(Cumulative Layout Shift)는 SSR에서 보통 더 좋고, INP(Interaction to Next Paint)는 하이드레이션 속도와 이벤트 핸들러 구현 품질에 따라 달라집니다.
- Next.js App Router: App Router의 페이지는 기본적으로 서버 컴포넌트로, 서버에서 실행되고 추가 설정 없이 자동으로 HTML을 반환합니다.
- SSR vs CSR: SSR은 서버 자원을 사용하는 대신 빠른 첫 페인트와 SEO 친화성을 제공합니다. CSR은 SEO가 필요 없는 내부 대시보드 등 대화형 시나리오에 적합합니다.
- 첫 페인트 흐름: HTML 파싱 -> CSS 로드 및 렌더링 -> JS 다운로드 및 실행 -> React 하이드레이션 -> 페이지 대화형. 각 단계가 LCP에 영향을 줍니다.
- SEO 크롤링 친화성: Googlebot, Bingbot 등 주요 크롤러는 HTML 텍스트를 직접 읽습니다. SSR의 완전한 콘텐츠 출력은 크롤링 범위를 보장합니다.
- Web Vitals 기준값: LCP < 2.5초, INP < 200ms, CLS < 0.1이 Google의 권장 "양호" 기준입니다. SSR은 LCP와 CLS를 달성하기 쉽게 만듭니다.
- 서명된 요청: 데모 페이지는 서버에서 serverApiFetch를 통해 백엔드 API를 호출하며, X-Timestamp, X-Nonce, X-Sign을 자동으로 전달하여 인증을 완료합니다.
예시
SSR (서버 사이드 렌더링)
// app/page.tsx (기본 서버 컴포넌트)
export default async function Page() {
const data = await fetch('/api/info').then(r => r.json());
return <div>{data.title}</div>;
}
// HTML에 <div>실제 콘텐츠</div>가 이미 포함되어 전송됨CSR (클라이언트 사이드 렌더링)
'use client';
import { useEffect, useState } from 'react';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/info').then(r => r.json()).then(setData);
}, []);
return <div>{data?.title || 'Loading...'}</div>;
}
// 첫 페인트 HTML에는 <div>Loading...</div>만 포함됨첫 페인트 속도 비교
# SSR
FCP: 0.4s LCP: 0.8s TTI: 1.2s
SEO: 크롤러가 완전한 콘텐츠를 즉시 받아감
# CSR
FCP: 0.6s LCP: 1.8s TTI: 2.4s
SEO: 크롤러가 콘텐츠를 얻으려면 JS를 실행해야 함자주 묻는 질문
이 데모 페이지는 실제로 무엇을 보여주는 건가요?
Next.js App Router 페이지가 서버에서 데이터를 가져와 첫 HTML 응답에 결과를 포함시키면서 동시에 올바른 다국어 라벨을 로드하는 방식을 보여주는 서버 사이드 렌더링(SSR) 참조 라우트입니다. 최종 사용자용 도구가 아니라 개발자를 위한 동작 확인용 테스트 페이지입니다.
데이터는 서버에서 가져오나요, 브라우저에서 가져오나요?
서버에서 가져옵니다. 페이지는 React Server Component 내부에서 serverApiFetch(src/lib/sign.ts의 서명 요청 헤더 포함)를 호출하므로, 브라우저는 이미 데이터가 포함된 HTML 페이지를 받게 됩니다. 첫 로딩 시 클라이언트 측 fetch는 발생하지 않습니다.
왜 가끔 오래된 데이터가 표시되나요?
Next.js는 revalidate 설정에 따라 SSR 응답을 캐시할 수 있습니다. 매 요청마다 최신 데이터가 필요하다면 'export const dynamic = "force-dynamic"'을 설정하거나 fetch에 {cache: 'no-store'}를 전달하세요. 데모 라우트는 예시를 단순하게 유지하기 위해 기본 동작을 사용합니다.
이 패턴을 자체 페이지에 복사해서 사용해도 되나요?
네, 그렇게 사용하라고 만든 것입니다. 이 라우트는 next-intl의 getTranslations와 서버 측 데이터 fetch를 결합하고 그 결과를 Client Component에 전달하여 상호작용을 구현하는 방법을 보여줍니다. layout.tsx와 page.tsx를 복사한 다음 데이터 소스만 교체하세요.
내부 API에도 요청 서명이 왜 필요한가요?
X-Timestamp / X-Nonce / X-Sign은 재전송 공격을 차단하고, 공개 웹에서 들어오는 요청이 실제로 신뢰된 클라이언트로부터 왔음을 보장합니다. 서명 키는 로그인 사용자의 경우 세션별로 발급되고, 그 외에는 기본값을 사용합니다. 구현 내용은 src/lib/sign.ts와 src/lib/fetch.ts를 참고하세요.
이 페이지가 프로덕션 빌드에도 포함되나요?
참조용으로 배포된 앱에 포함되어 있지만, 홈 페이지나 도구 목록에서 링크되지 않으며 사용자용 도구로 노출되지도 않습니다. 라우트 형태로 렌더링된 개발자 문서로 이해하시면 됩니다.
프로젝트 구조를 익히고 싶다면 어디부터 봐야 하나요?
이 페이지의 layout.tsx와 page.tsx부터 시작한 다음, src/i18n.ts(로케일 설정), src/i18n/request.ts(전역 번역 로딩), src/lib/load-page-messages.ts(페이지별 번역), src/lib/fetch.ts(서명 요청 래퍼)를 차례로 읽어보세요.