본문으로 건너뛰기

Next.js App Router 요청 흐름 지도

·7 min read

브라우저가 주소를 요청한 순간부터 데이터가 바뀌고 화면이 갱신되기까지, 그 사이에 등장하는 이름이 많다. Proxy, Server Component, Route Handler, Hydration, Server Action, Revalidation, TanStack Query. 각각은 알겠는데 이것들이 한 요청 위에서 어디에 놓이는지가 매번 흐릿했다.

한 장으로 그려놓고 각 자리에 이름을 붙여봤다.

요청 하나가 화면이 되기까지 지나는 자리

위에서 아래로 한 줄기로 내려오다가, Route가 매칭되는 지점에서 세 갈래로 갈린다.

그림 위쪽의 Nginx (리버스 프록시)와 그 아래 Proxy (proxy.ts)는 이름만 닮았지 다른 계층이다. 앞의 것은 외부 요청을 받아 내부 서버로 넘기는 인프라 쪽 서버고, 뒤의 것은 Next.js 안에서 요청을 검사·변경·분기하는 계층이다. Next.js 16 이전 이름은 Middleware였다.

Proxy와 Route 매칭을 나란히 놓고 싶어지는데, 이 둘은 병렬이 아니라 순차다. Proxy가 먼저 실행되고 그다음에 파일 시스템 라우트 매칭이 이어진다. 그래서 인증이 필요한 경로를 Proxy에서 /login으로 Redirect하면 애초에 Route가 잡히지도 않는다.

Proxy는 Server Component가 아니다. Server Component는 서버에서 실행되는 React Component고, Proxy는 React 트리에 들어가지도 않는 요청 처리 계층이다.

Route가 매칭되면 세 갈래로 갈린다. Server Component가 실행되는 길, Route Handler로 가는 길, 이미 만들어둔 Static 응답으로 끝나는 길. Route Handler는 HTTP API endpoint다. 브라우저든 외부 클라이언트든 fetch로 부르고 HTTP Response를 돌려받는다.

Server Component가 만드는 건 HTML만이 아니다

이 자리에서 이름이 가장 많이 겹친다. SSR은 서버에서 HTML을 렌더링하는 방식이고, RSC는 Server Component를 사용하는 React 아키텍처다. 둘은 같은 말이 아니다.

SSR은 HTML을 만드는 문제이고, RSC는 Server Component의 결과와 React Tree를 서버와 클라이언트 사이에서 다루는 문제다.

RSC Payload는 Server Component 트리의 렌더 결과를 직렬화한 데이터다. HTML이 아니다. 그림에서 Server Component 아래 상자를 HTML / RSC Payload라고 둘을 나란히 적어둔 것도 그래서다. 초기 로딩에서는 HTML과 RSC Payload를 함께 활용하고, 이후 Client Navigation에서는 RSC Payload만 받아서 React Tree를 갱신한다. 이 두 흐름은 서로 다르다.

브라우저에 도착한 트리는 다시 Server Component 자리와 Client Component 자리로 나뉜다. Client Component는 브라우저에서 상호작용을 담당하는 Component인데, 초기 로딩에서는 이쪽도 서버에서 HTML로 프리렌더된다. 그래서 처음 받은 화면은 이미 그려져 있고, 그 위에 이벤트 핸들러와 상태를 붙이는 작업이 따로 필요하다. 그게 Hydration이다.

사용자가 버튼을 누른 뒤 캐시가 두 갈래로 갈린다

Hydration이 끝나야 버튼이 반응한다. 여기서부터가 그림의 아래쪽 절반이다.

Server Action은 서버 함수를 React/Next.js Action 흐름에서 호출하는 방식이다. 방식이지 작업 자체가 아니다. 데이터를 변경하는 작업은 Mutation이고, Mutation을 실행하는 통로 중 하나가 Server Action이다. Route Handler로도, 외부 API 호출로도 Mutation은 일어난다.

Server Action은 API도 아니다. Route Handler처럼 endpoint를 열어두는 게 아니라, 클라이언트에서 서버 함수를 직접 부르는 형태로 쓴다. 그림에서도 Route Handler는 위쪽 갈래에, Server Action은 아래쪽 갈래에 따로 놓였다.

Service와 Repository는 Server Action 안쪽의 서버 계층이다. Service가 비즈니스 로직을 맡고 Repository가 Database 접근을 맡는다. 여기까지 내려가서 데이터가 바뀐다.

데이터가 바뀐 뒤 그림은 둘로 벌어진다. 한쪽은 Next.js Revalidation이다. 서버 쪽 캐시를 다시 검증해서 Server Component의 결과를 새로 만든다.

다른 한쪽은 TanStack Query다. TanStack Query는 Server State(서버에서 가져와 브라우저에 들고 있는 데이터)의 fetch, cache, sync, mutation lifecycle을 관리하는 브라우저 쪽 라이브러리고, invalidate로 캐시를 무효화하면 Refetch가 일어나 최신 UI로 이어진다.

TanStack Query는 API가 아니다. 데이터를 가져오는 건 fetch나 Route Handler 쪽이고, TanStack Query는 그 결과를 브라우저에 어떻게 보관하고 언제 다시 가져올지를 관리한다.

데이터를 한 번 바꿨는데 갱신 경로가 둘인 건, 캐시가 애초에 두 벌이기 때문이다. Next.js 서버에 한 벌, 브라우저에 한 벌. 둘은 서로의 존재를 모른다.