
클라이언트에서 설정하는 Router는 무엇이며, 서버 Router와 어떤 관계가 있을까? ==웹 애플리케이션의 Router는 주소와 요청 조건을 규칙에 대조해 실행할 처리 대상을 고르는 장치다.== 서버에서는 HTTP 요청을 처리할 함수를, 클라이언트에서는 현재 URL에 대응하는 화면 구성을 선택한다. 경로를 선택하는 일과 화면을 만드는 일을 나누면 CSR·SSR·서버 컴포넌트도 구분할 수 있다.

## 서버 Router는 요청과 처리 코드를 연결한다

HTTP는 클라이언트와 서버가 요청·응답을 주고받는 규약이다. 상품 상세를 여는 상황에서 아래는 HTTP/1.1 요청의 일부다.

```http
GET /products/42?currency=KRW HTTP/1.1
Host: shop.example
```

서버는 요청을 받았다고 해서 저절로 상품 화면을 만들지 않는다. 애플리케이션에 등록된 규칙을 보고 담당 코드를 골라야 한다. 예를 들어 `GET /products/:id`라는 규칙에 맞으면 `id`를 `42`로 추출하고 상품 조회 함수를 호출한다. 이런 대응 규칙 하나를 **Route**, 규칙에 맞는 항목을 찾는 일을 **Route matching**, 선택된 처리 함수를 **Handler**라고 부른다. `:id`는 Express 같은 프레임워크의 설정 문법이며 실제 요청 주소에 그대로 들어가는 문자열은 아니다. [Express 라우팅 문서](https://expressjs.com/en/guide/routing/)

<figure>
  <img src="/figures/inbox/routing-dispatch.svg" style="max-width:340px;margin-inline:auto" alt="GET /products/42를 GET /products/:id 규칙에 대조하고 id 42를 추출해 Handler를 호출하는 네 단계." width="340" height="425" loading="lazy" decoding="async" />
  <figcaption>그림 1. 모든 화살표는 처리 순서다. Router가 핸들러를 선택한 뒤, 핸들러가 데이터 조회와 응답 생성을 수행한다.</figcaption>
</figure>

핸들러는 HTML을 만들 수도 있고 JSON이나 파일을 반환할 수도 있다. 같은 상품이라도 `GET /products/42`와 `DELETE /products/42`는 서로 다른 작업이다. 따라서 서버 라우팅을 단순히 ‘URL에 맞는 화면 선택’으로만 정의하면 API 라우팅을 설명하기 어렵다. HTTP는 요청 메서드와 대상 자원을 구분하며, 애플리케이션 Router는 이 정보를 처리 코드에 연결한다. [HTTP 메서드의 의미](https://www.rfc-editor.org/rfc/rfc9110.html#section-9)

또한 URL의 경로가 반드시 디스크의 파일 경로를 뜻하지는 않는다. `/products/42`라는 자원을 DB 조회 결과로 표현할 수도 있다. 주소와 내부 구현을 어떻게 연결할지는 서버가 정한다. [HTTP의 대상 자원 결정](https://www.rfc-editor.org/rfc/rfc9110.html#section-7.1)

## 문서 응답에서 브라우저 안의 이동까지

라우팅의 변화는 ‘주소로 가리키는 대상을 누가 선택하고, 응답을 어디서 구성하는가’로 따라가면 이해하기 쉽다. 다음은 모든 사이트가 같은 순서로 바뀌었다는 연표가 아니라, 책임이 확장된 과정을 보여주는 기술적 기준점이다.

| 기준점 | 해결하려던 문제 | 주소를 해석한 뒤 하는 일 |
| --- | --- | --- |
| 초기 문서 중심 웹 | 다른 컴퓨터의 문서를 주소로 요청하기 | 서버가 요청한 문서를 응답한다 |
| 서버의 동적 문서 생성 | 요청과 데이터에 따라 다른 내용을 보여주기 | 서버 프로그램을 실행해 응답을 만든다 |
| 브라우저의 부분 갱신 | 상호작용마다 문서 전체를 다시 받는 비용 줄이기 | JavaScript가 데이터를 요청하고 화면 일부를 바꾼다 |
| SPA의 클라이언트 라우팅 | 한 문서 안의 여러 화면을 URL·방문 기록과 연결하기 | 브라우저의 앱이 URL에 맞는 화면을 선택한다 |
| 서버·클라이언트 렌더링의 결합 | 초기 표시와 상호작용, 전송량과 서버 작업을 함께 조절하기 | 서버의 화면 구성과 브라우저의 이동·상호작용을 결합한다 |

1991년의 초기 HTTP 문서는 연결한 서버에 `GET`과 문서 주소를 보내고 문서를 받는 방식을 설명한다. 동적 응답의 한 경로인 **CGI(Common Gateway Interface)**는 웹 서버와 외부 프로그램을 연결하는 인터페이스다. 2004년에 발행된 RFC 3875는 CGI가 1993년부터 사용됐다고 기록한다. CGI 자체가 현대 프레임워크의 Router인 것은 아니지만, 요청을 프로그램 실행과 연결했던 배경을 보여준다. [초기 HTTP 원문](https://www.w3.org/Protocols/HTTP/AsImplemented.html), [CGI의 역할과 역사](https://www.rfc-editor.org/rfc/rfc3875.html)

이후에는 브라우저에서 실행 중인 스크립트도 HTTP 요청을 보낼 수 있게 됐다. 2006년 W3C의 첫 XMLHttpRequest 초안은 폼 제출과 원격 데이터 로딩 같은 HTTP 클라이언트 기능을 스크립트에 제공한다고 설명한다. 이런 부분 갱신 방식은 흔히 **AJAX**라고 불린다. 부분 갱신이 가능하다는 것과 URL에 맞는 여러 화면을 관리한다는 것은 별개의 문제이며, 후자의 책임을 클라이언트 Router가 맡는다. [당시 XMLHttpRequest 초안](https://www.w3.org/TR/2006/WD-XMLHttpRequest-20060405/)

**SPA(Single Page Application)**에서는 처음 받은 문서 안에서 앱이 계속 실행되며 여러 화면을 구성한다. 그렇더라도 서버에는 데이터 API나 초기 문서를 제공할 경로가 필요하다. 서버 Router와 클라이언트 Router는 함께 존재할 수 있다. ‘서버가 하던 페이지 선택을 브라우저에서도 수행한다’는 설명은 역할의 이동을 뜻한다. Router라는 단어가 특정 라이브러리에서 처음 차용됐다는 어원·명명 역사를 뜻하지는 않는다.

서버에서 HTML을 만드는 방식도 초기 동적 웹부터 존재했다. 현대 React의 SSR은 그 서버 렌더링에 브라우저의 React 실행을 연결할 수 있다. 한편 React Server Components는 2020년 12월 연구 단계로 공개됐다. **서버의 문서 생성, 클라이언트 이동, SSR, 서버 컴포넌트는 앞선 것을 모두 없애는 교체 단계가 아니다.** 서로 다른 문제를 해결하며 함께 쓰인다. [React의 서버 HTML 연결](https://react.dev/reference/react-dom/client/hydrateRoot), [2020년 서버 컴포넌트 공개](https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components)

## 경로 설정에 숨어 있는 하위 개념

실제로 Router를 설정할 때는 주소 전체를 한 덩어리로 보기보다 어떤 값을 매칭하고 어떤 값을 처리에 넘기는지 구분해야 한다.

| 개념 | 역할 | 예시 |
| --- | --- | --- |
| HTTP method | 같은 자원에 어떤 작업을 요청하는지 표현한다 | `GET`, `POST`, `DELETE` |
| Path | 요청 대상의 경로다 | `/products/42` |
| Path parameter | 경로 규칙에서 추출한 값이다 | `/products/:id`에서 `id = "42"` |
| Query | 주소의 추가 조건을 전달한다 | `?currency=KRW` |
| Fragment | 문서 안의 위치 등을 식별한다 | `#reviews` |

Express에서는 query string이 route path에 포함되지 않는다. `/products/42?currency=KRW`는 경로를 매칭하고, `currency`는 별도로 읽는다. 애플리케이션은 그 값에 따라 통화나 정렬 상태를 바꿀 수 있다. Fragment는 HTTP 요청을 보내기 전에 주소에서 분리되므로 서버가 받는 요청 대상에는 `#reviews`가 없다. 브라우저의 Hash Router는 이 클라이언트 쪽 값을 이용하는 방식이다. [Express의 경로와 query 구분](https://expressjs.com/en/guide/routing/#route-paths), [URI fragment 규정](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.5), [React Router의 HashRouter](https://reactrouter.com/api/declarative-routers/HashRouter)

**Middleware**는 핸들러 전후의 처리 흐름에 참여하는 함수다. 인증 확인, 요청 기록, 본문 해석 등을 조합할 수 있다. **중첩 라우팅**은 부모 경로와 하위 경로의 대응을 구성하는 방법이며, 클라이언트에서는 공통 레이아웃 안에 하위 화면을 넣는 구조와도 연결된다. 매칭 우선순위는 구현에 따라 다르므로 ‘먼저 적은 항목이 항상 우선한다’고 일반화하지 않는다. [Express의 미들웨어](https://expressjs.com/en/guide/using-middleware.html), [React Router의 중첩 경로](https://reactrouter.com/start/declarative/routing)

매칭 결과와 자원 존재 여부도 별개다. `GET /products/:id`에 매칭돼도 DB에 42번 상품이 없을 수 있다. 이때 핸들러가 404를 응답할 수 있다. 경로에 대응하는 자원은 있지만 요청한 메서드를 지원하지 않는 경우는 405의 의미에 해당한다. 라이브러리가 이 판단을 자동으로 모두 해준다고 가정하면 안 된다. [HTTP 404와 405](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.5)

## 조건문으로도 작은 서버 Router를 만들 수 있다

다음은 Node.js의 HTTP 서버에서 경로 매칭과 처리 함수를 직접 연결한 예다. `router.mjs`로 저장하고 `node router.mjs`로 실행한다. 별도 프레임워크 없이 `GET /products/42`를 처리하며, query가 붙어도 pathname으로 매칭한다. [Node.js HTTP 서버 API](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)

```js
import { createServer } from 'node:http';

function showProduct(res, id) {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ productId: id }));
}

createServer((req, res) => {
  const url = new URL(req.url, 'http://router.example');
  const match = /^\/products\/(\d+)$/.exec(url.pathname);

  if (req.method === 'GET' && match) {
    showProduct(res, match[1]);
    return;
  }

  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('해당 요청을 처리하는 경로가 없습니다.');
}).listen(3000, '127.0.0.1');
```

여기서 `req.method`와 정규식을 확인하는 부분이 작은 Router이고, `showProduct`가 Handler다. 예시는 매칭 원리를 보이기 위해 GET 처리와 그 밖의 404만 구현했다. 앞서 구분한 405 처리나 인증 같은 기능까지 갖춘 서버는 아니다.

프레임워크는 이 조건문을 선언형 설정으로 바꾸고, 변수 추출·중첩·미들웨어 연결 같은 반복을 맡는다. Express의 등록 부분만 쓰면 다음처럼 표현한다. URL에 맞는 코드 선택이라는 원리는 같다.

```js
app.get('/products/:id', (req, res) => {
  res.json({ productId: req.params.id });
});
```

## CSR에서는 이미 실행 중인 앱이 주소를 읽는다

**CSR(Client-Side Rendering)**은 브라우저의 JavaScript가 화면을 구성하는 방식이다. 클라이언트 라우팅을 사용하는 CSR 앱에 처음 접속하면 서버가 앱의 기본 HTML과 JavaScript를 전달하고, 실행된 앱이 `window.location`을 읽어 화면을 고른다. **URL 자체가 함수를 자동으로 실행하는 것이 아니라, 실행 중인 앱에 URL을 해석하는 코드가 있는 것이다.** [브라우저 문서와 방문 기록 API](https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface)

브라우저는 HTML을 읽어 문서 구조를 객체로 표현하는 **DOM(Document Object Model)**을 만든다. JavaScript가 DOM을 바꾸면 화면에 표시할 내용도 갱신될 수 있다. 아래는 `<main id="app"></main>`과 `<a id="product-link" href="/products/42">상품 보기</a>`가 있는 문서에서 동작하는 최소 예다. 실제 상품 조회 대신 텍스트만 표시한다.

```js
function renderRoute() {
  const path = window.location.pathname;
  document.querySelector('#app').textContent =
    path === '/products/42' ? '42번 상품 화면' : '기본 화면';
}

document.querySelector('#product-link').addEventListener('click', (event) => {
  event.preventDefault();
  history.pushState(null, '', '/products/42');
  renderRoute();
});

window.addEventListener('popstate', renderRoute);
renderRoute();
```

이 예에서는 링크의 기본 문서 이동을 막고, `pushState`로 URL과 방문 기록을 추가한 뒤 화면을 직접 갱신한다. `pushState` 자체는 새 문서를 가져오거나 `popstate`를 발생시키지 않는다. 뒤로·앞으로 가기로 기록이 바뀔 때는 `popstate`에 연결한 코드가 현재 주소를 다시 읽는다. 실제 Router는 일반 링크의 새 탭 열기 같은 동작도 보존하면서 이 흐름을 관리한다. [History API 규정](https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface)

앱 안에서의 이동과 주소 직접 접속은 구분해야 한다. `/products/42`에서 새로고침하거나 그 링크를 새 탭에서 열면 서버가 문서 요청을 받는다. 서버가 앱 문서 대신 ‘그런 파일이 없다’고 응답하면, 앱 안에서는 이동이 되지만 새로고침할 때 404가 생긴다. History 기반 SPA를 배포할 때는 앱이 담당할 문서 경로에 기본 HTML을 반환하도록 서버를 맞춘다. API·정적 파일·실제 없는 경로까지 모두 같은 200 응답으로 덮는 설정은 따로 구분해야 한다. [React Router의 SPA 호스팅 설정](https://reactrouter.com/how-to/spa)

## SSR은 선택한 페이지의 초기 HTML을 서버에서 만든다

**SSR(Server-Side Rendering)**에서는 요청에 대응해 서버가 선택한 페이지 코드를 실행하고 HTML을 만든다. 서버가 픽셀을 그려 보내는 것은 아니며, HTML을 실제 화면으로 그리는 일은 브라우저가 한다. React 앱에서는 서버가 만든 HTML에 브라우저의 React를 연결해 필요한 상호작용을 이어갈 수 있다. 이를 **Hydration**이라고 한다. [React의 hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot)

<figure>
  <picture>
    <source media="(max-width: 40rem)" srcset="/figures/inbox/routing-rendering-mobile.svg" width="340" height="710" />
    <img src="/figures/inbox/routing-rendering.svg" alt="CSR 최초 접속은 앱 파일을 받은 브라우저가 경로를 선택하고 화면을 구성한다. React SSR 최초 접속은 서버가 경로를 선택하고 HTML을 만들며 브라우저가 표시와 Hydration을 이어간다." width="640" height="350" loading="lazy" decoding="async" />
  </picture>
  <figcaption>그림 2. 같은 주소의 최초 접속을 단순화한 역할 비교다. 화살표는 처리 순서이며 네트워크 지연이나 실제 완료 시간을 나타내지 않는다. React SSR의 Hydration은 필요한 클라이언트 동작을 연결하는 단계다.</figcaption>
</figure>

CSR과 SSR은 화면 구성 위치를 설명하고, Router는 그 전에 어떤 처리 대상을 선택하는지 설명한다. 따라서 ==라우팅 방식과 렌더링 방식은 서로 다른 설계 축이다.== SSR 앱 안에서도 클라이언트 Router로 화면을 전환할 수 있다. Next.js의 `<Link>` 이동은 문서 전체를 다시 로드하는 대신 공통 레이아웃을 유지하고 필요한 내용을 바꾼다. [Next.js의 클라이언트 전환](https://nextjs.org/docs/app/getting-started/linking-and-navigating#client-side-transitions)

## 서버 컴포넌트는 또 다른 경계를 나눈다

**React Server Component**는 브라우저 번들 밖의 서버 환경에서 실행하는 컴포넌트다. 빌드할 때 실행할 수도 있고, 웹 요청에 대응해 실행할 수도 있다. **Client Component**는 브라우저에서 상태와 이벤트 등 상호작용을 처리할 수 있는 컴포넌트다. 따라서 서버 컴포넌트라는 이름만으로 ‘매 요청마다 실행된다’고 판단할 수 없다. [React 서버 컴포넌트 문서](https://react.dev/reference/rsc/server-components)

| 구분할 질문 | 해당하는 개념 |
| --- | --- |
| 이 주소에서 어떤 코드나 화면 구성을 선택할까? | Routing |
| 초기 화면을 구성할 HTML을 어디서 만들까? | CSR·SSR |
| 어떤 컴포넌트 코드가 브라우저 실행 대상인가? | Server·Client Component 경계 |
| 서버의 계산을 언제 수행하고 결과를 다시 쓸까? | 빌드 시 생성·요청 시 실행·캐싱 |

Next.js App Router에서는 첫 접속 때 Server Component의 실행 결과를 **RSC Payload**라는 데이터 형식으로 만들고, 이를 Client Component와 함께 사용해 초기 HTML도 생성한다. 브라우저는 HTML을 표시하고 필요한 Client Component에 Hydration을 수행한다. **Client Component도 첫 접속의 초기 HTML 생성에 참여할 수 있으므로 `Client Component = CSR 전용`, `Server Component = SSR`로 대응시키면 혼란이 생긴다.** [Next.js의 최초 접속 처리](https://nextjs.org/docs/app/getting-started/server-and-client-components#how-do-server-and-client-components-work-in-nextjs)

앱 안에서 이동할 때는 다음 경로에 필요한 RSC Payload를 가져오거나 미리 받은 캐시를 사용할 수 있다. 이때 전체 HTML 문서를 매번 새로 받는다고 가정하면 안 된다. 클라이언트 라우팅으로 이동했다는 사실도 서버 작업이 전혀 없다는 뜻은 아니다. [Next.js의 서버 렌더링과 이동](https://nextjs.org/docs/app/getting-started/linking-and-navigating#server-rendering)

파일 기반 라우팅은 이 선택 규칙을 폴더로 표현하는 방법이다. Next.js의 `app/products/[id]/page.tsx`는 `/products/42` 같은 경로를 받는 페이지를 선언한다. `[id]`가 값을 추출할 자리이며, 폴더 규칙을 프레임워크가 라우트 구성으로 해석한다. 서버에 저장된 HTML 파일을 그대로 찾아 보내는 파일 서버와는 역할이 다르다. [Next.js 페이지와 동적 세그먼트](https://nextjs.org/docs/app/getting-started/layouts-and-pages#creating-a-dynamic-segment)

## FE 서버의 부하와 실제 문제를 판단하는 법

요청마다 SSR을 수행하면 Next.js 같은 FE 서버에도 CPU·메모리 사용과 데이터 요청 비용이 발생한다. ‘프론트엔드’는 담당 영역의 이름이고, 서버에서 실행하는 코드는 실제 서버 자원을 사용한다. 백엔드 API를 따로 두는 구조라면 백엔드는 데이터 처리를, FE 서버는 그 결과를 이용한 페이지 구성을 맡을 수 있다. 이는 역할 분리의 한 예이며 물리 서버가 반드시 두 대여야 한다는 뜻은 아니다.

브라우저가 하던 화면 구성의 일부를 서버가 맡으면 서버 작업이 늘 수 있다. 반면 먼저 표시할 HTML을 보내거나 브라우저에 전달할 코드와 라이브러리를 줄일 수 있다. 비용이 무조건 줄거나 늘어난다고 결론 내리기보다, 무엇을 어디서 실행하는지 살펴야 한다. 서버 컴포넌트는 브라우저 번들에서 일부 코드를 제외할 수 있지만 서버 계산 자체를 없애는 장치는 아니다. [서버 컴포넌트의 실행과 번들 경계](https://react.dev/reference/rsc/server-components)

빌드할 때 HTML을 미리 생성하는 방식은 **SSG(Static Site Generation)**라고 한다. 요청 때 생성한 결과를 캐시하거나 SSG 결과를 전달하면 매 요청의 반복 계산을 줄일 수 있다. 캐시 갱신에도 작업이 들며 사용자별 결과를 얼마나 공유할 수 있는지도 다르다. Next.js의 정확한 기본값과 API는 버전·설정에 따라 달라지므로 여기서는 실행 시점과 재사용 여부의 구분에 집중한다. [Next.js 캐싱 문서](https://nextjs.org/docs/app/getting-started/caching)

실제 증상을 볼 때도 이 경계를 따라가면 된다. 새로고침 때만 404라면 문서 요청을 받는 서버의 대응을, 뒤로 가기에서 화면이 안 바뀐다면 방문 기록과 화면 갱신의 연결을 먼저 본다. FE 서버의 CPU가 높다면 요청 때마다 수행하는 렌더링·데이터 가공과 캐시 재사용을 확인한다. Client Component를 썼는데 서버에서도 실행된 흔적이 보인다면 초기 HTML 생성 단계를 구분한다. 같은 Router라는 이름 아래에서도 **경로 선택, 데이터 처리, HTML 생성, 브라우저 표시와 상호작용이 어디서 이어지는지**를 추적하는 것이 출발점이다.

<small>확인 기준: 2026-10-27의 공식 문서. Next.js 설명은 App Router 기준이다. 코드와 그림은 원리를 드러내기 위해 단순화했으며, 모든 Router 라이브러리의 동작이나 성능 측정 결과를 대표하지 않는다.</small>
