SSR vs CSR — 데이터 요청이 어디서 일어나는지 이해하기
같은 코드인데 SSR이면 서버에서, CSR이면 브라우저에서 요청이 나갑니다. Next.js에서 Server Component와 Client Component의 데이터 요청 흐름 차이와 주의사항을 알아봅니다.
아키텍처 구조도
SSR과 CSR, 뭐가 다른데?
SSR (서버 사이드 렌더링)
- •Node.js 서버에서 HTML을 완성해서 브라우저로 보냄
- •초기 로딩 속도가 빠르고 검색엔진 최적화(SEO)에 유리함
- •Next.js 13+의 Server Component 기본 동작
CSR (클라이언트 사이드 렌더링)
추천- ✓브라우저가 빈 HTML과 JavaScript를 다운받아 직접 렌더링함
- ✓페이지 이동 시 깜빡임 없이 부드러운 전환(SPA) 가능
- ✓useState, useEffect 등 브라우저 API 사용 시 필요
Next.js 환경에서는 이 두 가지 렌더링 방식에 따라 데이터(API)를 요청하는 주체가 완전히 달라집니다.
데이터 요청은 어디서 출발하나
// app/page.tsx (Server Component)
export default async function Page() {
// 브라우저가 아닌 Next.js(Node.js) 서버에서 실행됨
// 따라서 내부 Docker 네트워크를 통해 백엔드(FastAPI 등) 직접 호출 가능
const res = await fetch('http://server-blue:8000/api/data');
const data = await res.json();
return <div>{data.title}</div>;
}'use client'; // Client Component 선언
import { useEffect, useState } from 'react';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
// 사용자 브라우저에서 실행됨
// 내부망을 모르므로 퍼블릭 도메인(NGINX)을 통해 요청해야 함
fetch('https://api.example.com/api/data')
.then(res => res.json())
.then(setData);
}, []);
return <div>{data?.title}</div>;
}SSR 요청 흐름
- •Next.js 컨테이너 ➔ 백엔드 컨테이너
- •인터넷망을 거치지 않는 내부 통신
- •가장 빠르고 안전함
CSR 요청 흐름
추천- ✓사용자 브라우저 ➔ 공용 NGINX ➔ 백엔드
- ✓인터넷망을 통한 외부 통신
- ✓CORS 정책 등 브라우저 제약 적용
왜 이걸 구분해야 하는가?
주의: SSR에서 localhost를 쓰면 안 됩니다.
Next.js 서버가 실행 중인 컨테이너 내부의 localhost를 가리키게 되므로, 백엔드 컨테이너를 찾을 수 없어 연결이 거부(Connection Refused)됩니다.
SSR에서 외부 도메인(NGINX)으로 요청하면 안 되나요?
가능하지만 비효율적입니다. "컨테이너 A -> 컨테이너 B"로 바로 갈 수 있는 길을 두고, "컨테이너 A -> 인터넷 밖으로 나감 -> 공용 NGINX -> 컨테이너 B"로 핑퐁을 치는 꼴(Circular Routing)이 됩니다.
SSR에서는 왜 CORS 에러가 안 나나요?
CORS(교차 출처 리소스 공유)는 브라우저의 보안 정책입니다. SSR은 브라우저가 아닌 서버 간 통신이므로 CORS 검사를 하지 않습니다.
CSR에서 내부 Docker 주소(http://server-blue:8000)를 쓰면 안 되는 이유는?
CSR은 사용자의 스마트폰이나 PC 브라우저에서 실행됩니다. 사용자 기기는 우리 서버의 내부 Docker 네트워크에 접근할 수 없기 때문입니다.
실전: INTERNAL_API_URL 패턴 적용하기
동일한 API 호출 함수(apiClient)를 SSR과 CSR 양쪽에서 문제없이 사용하려면, 실행 환경에 따라 Base URL을 다르게 주입하는 패턴이 필요합니다.
// apiClient.ts
const isServer = typeof window === 'undefined';
// 서버에서 실행 중이면 내부 도메인, 브라우저면 외부 도메인 사용
const baseURL = isServer
? (process.env.INTERNAL_API_URL || 'http://server-blue:8000')
: (process.env.NEXT_PUBLIC_API_URL || '');
export async function fetchCategories() {
const res = await fetch(`${baseURL}/api/categories`);
return res.json();
}환경변수 설정
docker-compose.yml에 INTERNAL_API_URL을 추가하고 백엔드 서비스명으로 지정합니다.
apiClient 수정
typeof window === "undefined"를 사용해 현재 실행 환경을 판별합니다.
라우팅 확인
SSR 시에는 내부 도메인으로 빠르게 통신하고, CSR 시에는 올바른 퍼블릭 도메인을 사용하는지 확인합니다.
