URL을 입력하면 무슨 일이 벌어지는가 — React, Next.js, NestJS, Docker 아키텍처 해부
React는 Node.js가 아닙니다. Next.js도 Node.js 그 자체는 아닙니다. 그런데 Next.js는 Node.js 없이 SSR을 할 수 없습니다. NestJS도 Node.js 위에서 돌아갑니다. 이 관계가 처음엔 헷갈렸습니다.
개인 블로그를 운영하면서 Next.js, NestJS, Docker를 모두 사용하고 있지만, "유저가 URL을 입력했을 때 어떤 경로로 화면이 만들어지는가"를 한 번도 처음부터 끝까지 정리해본 적이 없었습니다. 각 기술을 따로따로 공부하고, 따로따로 사용해왔을 뿐입니다.
이 글에서는 하나의 요청이 브라우저에서 출발해 화면으로 돌아오기까지, 각 기술이 어떤 역할을 하는지 정리해보겠습니다.
React는 Node.js가 아닙니다
React는 브라우저에서 실행되는 UI 라이브러리입니다. Node.js는 서버에서 JavaScript를 실행하는 런타임 환경입니다. 실행되는 곳이 다릅니다.
그런데 React 프로젝트를 시작하려면 npm install을 실행해야 하고, 개발 서버를 띄우려면 npm run dev를 해야 합니다. 빌드도 Node.js 위에서 돌아가는 Vite나 Webpack이 담당합니다. 그래서 "React 프로젝트 = Node.js 프로젝트"처럼 보이지만, React 코드 자체는 최종적으로 브라우저에서 돌아가는 클라이언트 코드입니다.
Node.js는 React를 개발하고 빌드하는 데 필요한 도구이지, React의 실행 환경은 아닙니다. 적어도, 전통적인 CSR(Client Side Rendering)에서는 그렇습니다.
Next.js가 바꿔놓은 것
Next.js는 이 관계를 바꿔놓았습니다. Next.js는 Node.js로 React를 실행해서 HTML을 미리 만드는 프레임워크입니다.
CSR에서는 브라우저가 빈 HTML을 받고, JavaScript를 다운로드한 뒤, 브라우저에서 React를 실행해서 화면을 그렸습니다. 검색 엔진 크롤러가 봤을 때 빈 페이지였고, 이것이 SEO 문제의 시작이었습니다.
Next.js는 이 렌더링 작업을 서버(Node.js)에서 먼저 수행합니다. React 컴포넌트를 Node.js가 실행해서 완성된 HTML을 만들고, 그 HTML을 브라우저에 전달합니다. 브라우저는 이미 완성된 HTML을 받아 즉시 화면을 표시하고, 이후 JavaScript가 로드되면 React가 이벤트를 연결(hydration)하여 인터랙티브한 앱으로 전환합니다.
이 렌더링을 언제 하느냐에 따라 방식이 나뉩니다.
SSG (Static Site Generation) → 빌드 시 HTML 생성 → 이후 변경 안 됨
ISR (Incremental Static Regeneration) → 빌드 시 생성 + 주기적 재생성
SSR (Server Side Rendering) → 매 요청마다 HTML 생성
공통점은 Node.js가 React를 실행해서 HTML을 만든다는 것입니다. 시점만 다를 뿐입니다.
두 개의 Node.js 프로세스
제 블로그 아키텍처에서 중요한 포인트가 하나 있습니다. Next.js가 NestJS에게 렌더링을 요청하는 것이 아니라, Next.js 자체가 Node.js 서버를 내장하고 있다는 점입니다.
Next.js 자체 Node.js 서버 (port 3001)
├── SSR/ISR 렌더링 ← React 컴포넌트를 실행해서 HTML 생성
├── Route Handler
└── 데이터가 필요하면 → fetch로 NestJS에 요청
NestJS Node.js 서버 (port 3000)
├── REST API
├── MongoDB 접근
└── 비즈니스 로직
두 개의 독립된 Node.js 프로세스가 각자의 역할을 수행하고 있습니다. Next.js는 화면을 만드는 일을, NestJS는 데이터를 제공하는 일을 담당합니다.
요청 하나의 여정
유저가 블로그 글 하나를 열기까지 어떤 일이 벌어지는지 따라가 보겠습니다.
1. 유저가 kichang.info/blog/some-post 접속
2. Next.js의 Node.js 프로세스가 요청을 받음
→ app/(main)/blog/[slug]/page.tsx 서버 컴포넌트 실행
3. 서버 컴포넌트 안에서 fetch('https://api.kichang.info/blog-posts/some-post')
→ NestJS에 데이터 요청
4. NestJS가 MongoDB에서 글 데이터를 조회하여 JSON 응답
5. Next.js의 Node.js가 받은 데이터로 React 컴포넌트를 실행해서 HTML 생성
6. 완성된 HTML을 유저 브라우저에 전달
7. 브라우저가 HTML을 즉시 표시 → JavaScript 로드 후 hydration → React 앱 작동
핵심은 5번 단계입니다. React 컴포넌트를 HTML로 변환하는 작업을 브라우저가 아니라 Next.js가 자체 내장한 Node.js에서 수행합니다. NestJS는 이 과정에 관여하지 않고, 순수하게 데이터만 제공합니다.
React Server Components (RSC)
Next.js App Router에서는 기본적으로 모든 컴포넌트가 Server Component입니다. 서버에서만 실행되고, 브라우저에는 JavaScript 번들로 내려가지 않는 컴포넌트입니다.
// 기본값 = Server Component
// 서버에서만 실행됨, 브라우저에 JS 번들 안 내려감
export default async function BlogPost({ params }) {
const res = await fetch(`https://api.kichang.info/blog-posts/${params.slug}`);
const post = await res.json();
return <article>{post.title}</article>;
}
// 'use client' 선언 = Client Component
// 브라우저에서도 실행됨, JS 번들에 포함
'use client';
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(true)}>좋아요</button>;
}
Server Component는 데이터 fetch, 마크다운 파싱 같은 서버 로직을 담당하고, Client Component는 useState나 onClick 같은 브라우저 인터랙션을 담당합니다. 서버에서만 필요한 코드가 브라우저에 내려가지 않으니 JavaScript 번들 크기가 줄어드는 것이 핵심 이점입니다.
캐시는 어디에 저장되는가
ISR로 생성된 페이지는 어디에 저장될까요? Next.js Docker 컨테이너 내부의 디스크입니다. 메모리가 아닙니다.
Next.js 컨테이너
├── 이미지 레이어 (읽기 전용)
│ └── .next/server/ ← 빌드 시 생성된 SSG/ISR 초기 페이지
│
└── Writable Layer (읽기/쓰기)
└── .next/cache/ ← 런타임에 ISR 재생성된 페이지
빌드 시 생성된 페이지는 Docker 이미지 레이어에 포함되어 있어서 재배포 후에도 즉시 서빙됩니다. next build 과정에서 SSG와 ISR 페이지의 초기 버전이 이미 HTML로 만들어지기 때문입니다.
재배포 시 사라지는 것은 런타임에 ISR이 갱신한 최신 캐시뿐입니다. 하지만 빌드 시점에 최신 데이터를 fetch해서 생성하므로, 재배포 직후에도 거의 최신 상태입니다.
On-Demand Revalidation
ISR의 시간 기반 재생성만으로는 부족한 경우가 있습니다. 글을 수정했는데 캐시 만료까지 기다려야 한다면 불편합니다. 이때 NestJS에서 Next.js에 "이 페이지를 지금 재생성해달라"고 요청할 수 있습니다.
일반 ISR (시간 기반)
유저 요청 → 캐시 만료됐나? → 만료면 백그라운드 재생성
On-Demand Revalidation (이벤트 기반)
백엔드에서 글 수정 → Next.js에 "이 페이지 재생성해" 요청 → 즉시 반영
이것은 NestJS가 Next.js 컨테이너에 접근하는 것이 아니라, Next.js가 제공하는 API 엔드포인트에 HTTP 요청을 보내는 것입니다. 네트워크 통신일 뿐, 컨테이너 내부 접근이 아닙니다.
Docker 컨테이너의 격리
Coolify에서 Next.js와 NestJS를 각각 별도의 Docker 컨테이너로 운영하고 있습니다. 여기서 궁금해진 것이 보안 측면이었습니다. 만약 Next.js에 보안 취약점이 발생하면 어디까지 영향을 미칠까요?
2025년 3월에 발견된 CVE-2025-29927처럼 미들웨어 인증을 우회하는 취약점의 경우, 보호된 페이지에 비인가 접근은 가능하지만 컨테이너 자체가 장악되는 것은 아닙니다.
하지만 만약 RCE(Remote Code Execution)급 취약점이 발견된다면 이야기가 달라집니다.
Next.js 컨테이너 장악 시
├── 네트워크 접근 (가능)
│ └── NestJS 포트 3000으로 HTTP 요청 가능
│ └── 일반 API 호출과 동일한 수준
│
├── NestJS 컨테이너 내부 접근 (불가)
│ └── 파일시스템, 환경변수, 프로세스 → 격리됨
│
└── 호스트 접근 (기본적으로 불가)
네트워크 접근과 컨테이너 접근은 완전히 다른 수준입니다. 같은 Docker 네트워크에 있으면 HTTP 요청은 보낼 수 있지만, 다른 컨테이너의 파일시스템이나 환경변수에 접근하는 것은 별개의 문제입니다.
진짜 위험한 시나리오는 시크릿이 집중된 NestJS 컨테이너가 직접 장악되는 경우입니다. MongoDB URI, JWT Secret, API 키 등 모든 민감 정보가 백엔드 환경변수에 있기 때문입니다. 그래서 Next.js 컨테이너에는 불필요한 시크릿을 넣지 않고, NestJS API도 내부 네트워크라고 해서 인증을 생략하지 않는 것이 중요합니다.
정리
처음에는 React, Node.js, Next.js의 관계가 모호했습니다. 정리해보니 각자의 역할이 명확했습니다.
React → UI를 정의하는 라이브러리 (무엇을 그릴지)
Node.js → JavaScript를 서버에서 실행하는 런타임 (어디서 실행할지)
Next.js → Node.js로 React를 렌더링하는 프레임워크 (언제, 어떻게 렌더링할지)
NestJS → Node.js 위에서 API를 제공하는 프레임워크 (데이터를 어떻게 제공할지)
Docker → 각 프로세스를 격리하여 실행하는 컨테이너 (어떻게 격리할지)
유저가 URL을 입력하면, Next.js의 Node.js가 NestJS에 데이터를 요청하고, 받은 데이터로 React를 실행해 HTML을 만들고, Docker 컨테이너의 디스크에 캐시하고, 브라우저에 전달합니다. 최종적으로 유저가 보고 조작하는 것은 React입니다.
각 기술을 따로 공부할 때는 보이지 않던 전체 그림이, 하나의 요청을 따라가 보니 비로소 연결되었습니다.
관련 글
내가 Next.js ISR을 선택한 이유: 블로그 SEO, 그 고민의 시작과 해결
Next.js ISR을 선택하여 블로그 SEO 문제를 해결하는 방법을 알아보세요. React CSR의 한계를 극복하고, 검색 엔진 최적화와 소셜 미리보기를 완벽 지원하는 ISR의 핵심 원리를 소개합니다.
Next.js 배포 시 빈 캐시 문제 해결: 런타임 워밍에서 빌드 타임 정적 생성으로
Next.js + Coolify 환경에서 배포 직후 빈 캐시와 no available server 에러가 발생하는 문제를 해결한 경험. 런타임 캐시 워밍의 한계를 겪고, 빌드 타임 정적 생성으로 전환하여 근본적으로 해결했습니다.
NestJS 서버로 추적하는 이벤트 루프 — 이론에서 실전으로 (5편)
이벤트 루프 이론을 실제 NestJS 서버 코드에 대입합니다. 서버 시작부터 HTTP 요청 처리까지, 6개 페이즈가 실제로 어떤 역할을 하는지 추적합니다.