React Router v7 - (4) API Framework conventions, routers

장유진·2026년 3월 8일

React Router v7

목록 보기
4/5
post-thumbnail

1. Framework Conventions

1-1. root.tsx

root 라우트(app/root.tsx)는 유일하게 필수로 사용해야 하는 라우트이다. 모든 라우트의 최상위 부모 라우트이며 <html> 문서를 렌더링하는 역할을 하기 때문이다.

import { Outlet, Scripts } from "react-router";

import "./global-styles.css";

export default function App() {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="/favicon.ico" />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

root 라우트에서 렌더링해야 할 컴포넌트들

따라서 root 라우트는 전체 HTML 문서를 관리하는 곳이기 때문에 document 레벨의 핵심 컴포넌트들을 렌더링하기에 적합한 위치이다. 이 핵심 컴포넌트들은 전체 어플리케이션에서 딱 한 번만 렌더링되어야 하며 페이지들이 제대로 렌더링되기 위해 React Router를 구축하는 데 필요한 것들을 포함한다.

핵심 컴포넌트 목록

  • Link
  • Meta
  • Scripts
  • ScollRestoration

만약 React 19를 사용중이지 않거나 React의 <link>, <title>, <meta> 컴포넌트를 사용하지 않는다면 각 라우트에서 link와 meta export를 사용하기보다는 root 라우트에서 <Link><Meta> 컴포넌트를 사용하는 것이 좋다.

import { Outlet, Links, Meta, Scripts, ScrollRestoration } from "react-router";

export default function App() {
  return (
    <html lang="en">
      <head>
        {/* All `meta` exports on all routes will render here */}
        <Meta />

        {/* All `link` exports on all routes will render here */}
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

root 라우트에서의 Layout export

root 라우트에서는 추가로 Layout을 사용할 수 있고, 목적은 크게 두 가지다.

  • root component, HydrateFallback, ErrorBoundary 에 app의 shell이 중복으로 생성되는 것을 피하기
  • root 컴포넌트, HydreateFallback, ErrorBoundary 가 교체될 때 다시 mount되는 것을 피하기 (<link rel="stylesheet">가 제거되었다가 다시 추가되면 FOUC가 발생할 수 있음)

❓ app shell 이란?

변하지 않고 항상 유지되는 app의 기본 골격을 의미한다. 즉 root Layout에서 렌더링하는 것들은 모든 페이지에서 동일하게 유지되므로 root Layout 그 자체가 app shell이라고 이해해도 될 것 같다.

Layout은 유일한 prop으로 children을 받고, children은 root 라우트의 default export나 HydreateFallback, ErrorBoundary가 될 수 있다.

export function Layout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1"
        />
        <Meta />
        <Links />
      </head>
      <body>
        {/* children will be the root Component, ErrorBoundary, or HydrateFallback */}
        {children}
        <Scripts />
        <ScrollRestoration />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

export function ErrorBoundary() {}

Layout 컴포넌트에서 useLoaderData 사용 시 주의할 점

useLoaderData 는 데이터가 성공적으로 로드되었을 때 사용되는 것을 가정하므로 ErrorBoundary 내부에서는 사용되어서는 안 된다. 대신 반환 값이 undefined일 수 있는 useRouteLoaderData를 사용해야 한다. Layout 컴포넌트는 성공/실패 케이스 모두에서 사용될 수 있어 동일한 제약을 가지므로 useRouteLoaderData(’root’) 또는 useRouteError()를 사용해야 한다.

1-2. routes.ts

URL 패턴을 route module과 매칭하기 위해 사용하는 설정 파일이다.

import {
  type RouteConfig,
  route,
} from "@react-router/dev/routes";

export default [
  route("some/path", "./some/file.tsx"),
  // pattern ^           ^ module file
] satisfies RouteConfig;

route entry 설정을 위해 다음과 같은 helper 함수들을 사용할 수 있다.

  • route: 일반 route entry 생성
  • index: index 라우트용 route entry 생성
  • layout: layout 라우트용 route entry 생성
  • prefix: 별도의 부모 라우트를 적용하지 않고 여러 라우트에 prefix 경로를 추가
  • relative: 특정 경로를 기준으로 파일 경로 해석

파일 기반 라우팅

만약 설정 파일보다는 파일 구조와 이름으로 라우트를 설정하기를 원한다면 @react-router/fs-routes를 사용하면 된다.

import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes() satisfies RouteConfig;

1-3. react-router.config.ts

React Router 프레임워크의 전반적인 동작 방식을 설정하는 파일이다. SSR 사용 여부, 폴더 구조, 빌드 설정 등을 커스터마이징할 수 있다.

Options

  • allowedActionOrigins: action 함수를 제출할 수 있는 도메인 주소 목록을 설정한다. micromatch glob 패턴을 지원하여 와일드카드(*, **)를 사용하여 도케인 주소를 작성할 수 있다. 런타임에 이 값을 설정하고 싶다면 서버를 빌드할 때 설정할 수 있다.
  • appDirectory: React Router 프로젝트에서 실제 소스 코드가 들어있는 디렉토리의 경로 (Default: ‘app’)
  • basename: 모든 라우트의 경로 앞에 들어가는 prefix 경로 (Default: ‘/’)
  • buildDirectory: 빌드 결과물이 들어가는 디렉토리의 경로 (Default: 'build')
  • buildEnd: 빌드 완료 후 실행할 함수
  • future: 다음 버전의 기능들을 선택적으로 활성화할 수 있는 플래그
  • prerender: 빌드 타임에 prerender할 URL 배열
  • preset: 특정 플랫폼이나 환경에 앱의 설정을 맞춰주는 패키지 배열
  • routeDiscovery: 라우트들이 어떤 방식으로 로드될지를 설정 (Default: mode: ‘lazy’, manifestPath: ‘/__manifest’)
  • serverBuildFile: 서버 빌드 결과물 파일 이름으로, .js 형식으로 끝나야 함 (Default: ‘index.js’)
  • serverBundles: 서버 빌드 결과물을 단일 파일이 아닌 여러 개의 번들로 나눌 때 사용
  • serverModuleFormat: 서버 빌드 결과물의 포맷 (Default: ‘esm’)
  • ssr: SSR 렌더링 여부 설정 (Default: true)

1-4. entry.client.tsx

브라우저에서의 앱의 진입점으로 hydration 과정을 담당한다. 브라우저에서 가장 먼저 실행되는 코드이며 여기에서 다른 클라이언트용 코드를 초기화할 수 있다.

import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>
  );
});

기본적으로 React Router는 이 과정을 내부적으로 처리하므로 파일이 보이지 않지만, 직접 수정하고 싶다면 npx react-router reveal 명령어를 실행해 숨겨진 파일을 보이게 할 수 있다.

1-5. entry.server.tsx

서버에서의 앱의 진입점으로 HTTP 요청에 대한 응답을 제어하는 역할을 한다. 현재 요청의 context와 url을 사용하여 <ServerRouter>로 현재 페이지의 마크업을 렌더링하고, 이 마크업은 클라이언트 entry 모듈에 의해 hydreate된다.

⚠️ Node를 사용할 경우 entry.server.tsx 파일은 필수가 아니고, 기본적으로 구현되어 있는 entry.server.node.tsx 파일을 사용하게 된다. Node가 아닌 다른 JS 런타임(예: Cloudfare)을 사용한다면 entry.server.tsx 파일을 반드시 사용해야 한다.

기본적으로 React Router는 이 과정을 내부적으로 처리하므로 파일이 보이지 않지만, 직접 수정하고 싶다면 npx react-router reveal 명령어를 실행해 숨겨진 파일을 보이게 할 수 있다.

default export

이 모듈에서의 default export는 HTTP 응답을 생성하는 함수로, HTTP status, header, HTML 등 마크업이 생성되고 클라이언트에 전달되기까지의 모든 과정을 제어한다.

import { PassThrough } from "node:stream";
import type { EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { renderToPipeableStream } from "react-dom/server";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  routerContext: EntryContext,
) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <ServerRouter
        context={routerContext}
        url={request.url}
      />,
      {
        onShellReady() {
          responseHeaders.set("Content-Type", "text/html");

          const body = new PassThrough();
          const stream =
            createReadableStreamFromReadable(body);

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            }),
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
      },
    );
  });
}

streamTimeout

스트리밍을 사용하고 있을 경우, 서버가 완료되지 않은 스트림을 기다릴 시간을 streamTimeout으로 설정한다.

⚠️ 서버 응답을 기다리는 timeout 시간과, React 렌더링을 기다리는 timeout을 다르게 설정하는 방법이 권장된다. React 렌더링 timeout 시간을 더 길게 잡아야 streamTimeout으로 인해 거절된 서버 응답 에러 메세지들이 스트림을 타고 전달되기 때문이다.

// Reject all pending promises from handler functions after 10 seconds
export const streamTimeout = 10000;

export default function handleRequest(...) {
  return new Promise((resolve, reject) => {
    // ...

    const { pipe, abort } = renderToPipeableStream(
      <ServerRouter context={routerContext} url={request.url} />,
      { /* ... */ }
    );

    // Abort the streaming render pass after 11 seconds to allow the rejected
    // boundaries to be flushed
    setTimeout(abort, streamTimeout + 1000);
  });
}

handleDataRequest

데이터 요청의 응답을 변경할 수 있는 함수이다. HTML을 렌더링하는 요청 말고, hydration 이후의 loader와 action 데이터를 반환하는 요청들에 사용할 수 있다.

export function handleDataRequest(
  response: Response,
  {
    request,
    params,
    context,
  }: LoaderFunctionArgs | ActionFunctionArgs,
) {
  response.headers.set("X-Custom-Header", "value");
  return response;
}

❓ 왜 hydration 이후의 요청들에만 적용 가능할까?

최초 접속 시에는 loader가 별도의 HTTP 요청으로 오지 않고, 서버가 만드는 HTML 내부에 포함되어 내려오기 때문이다.

handleError

기본적으로 React Router는 server-side에서 발생한 에러를 콘솔에 출력한다. 만약 콘솔에 로그를 다르게 출력하고 싶거나 외부 서비스에 에러 로그를 전송하고 싶을 경우 handleError 함수를 사용할 수 있다.

export function handleError(
  error: unknown,
  {
    request,
    params,
    context,
  }: LoaderFunctionArgs | ActionFunctionArgs,
) {
  if (!request.signal.aborted) {
    sendErrorToErrorReportingService(error);
    console.error(formatErrorForJsonLogging(error));
  }
}

⚠️ React Router의 요청 취소 및 race condition 제어 로직이 많은 요청을 abort시킬 수 있기 때문에 요청이 abort되었을 때 로깅하는 것을 피하는 것이 좋다.

⚠️ renderToPipeableStream이나 renderToReadableStream을 사용해 스트리밍으로 응답을 전송할 경우 handleError는 첫 HTML shell 렌더링 중에 발생하는 에러까지만 감지할 수 있고, 그 뒤에 스트리밍 렌더링 중 발생하는 에러는 감지할 수 없으므로 개발자가 수동으로 처리해야 한다.

⚠️ handleErrorloaderaction에서 throw된 Response 객체를 다루지 않는다. handleError의 목적은 예상하지 못한 코드 상의 버그를 찾아내는 것이고, 401, 404 등의 응답을 throw하는 것은 앱의 정상적인 시나리오 중 하나로 간주되어 코드 내부에서 처리되어야 한다.

1-6. .client modules

window, document, localStorage 등 브라우저 API를 사용하는 파일들의 이름을 *.client.ts로 짓거나 .client 디렉토리 내부에 위치하게 하면 서버 번들에 포함되지 않도록 할 수 있다.

export const supportsVibrationAPI = "vibrate" in window.navigator;

클라이언트 모듈에서 export된 값들은 서버에서 접근할 경우 undefined가 된다.

import { supportsVibrationAPI } from "./feature-check.client.ts";

console.log(supportsVibrationAPI);
// server: undefined
// client: true | false

1-7. .server modules

서버에서만 사용할 파일들의 이름을 *.server.ts로 짓거나 .server 디렉토리 내부에 위치하게 하면 클라이언트 번들에 포함되지 않도록 할 수 있다.

// This would expose secrets on the client if not exported from a server-only module
export const JWT_SECRET = process.env.JWT_SECRET;

export function validateToken(token: string) {
  // Server-only authentication logic
}

클라이언트에서 서버 모듈로 선언된 파일의 코드를 사용한다면 빌드가 실패한다.

⚠️ Route module에는 .server.client를 사용해서는 안 된다. Route module은 서버와 클라이언트에서 둘 다 사용되어야 한다.

2. Framework Routers

2-1. HydreatedRouter

HydreatedRouterServerRouter에서 보내준 정적 페이지에 라우터를 hydrate하는 역할을 하며, 보통 entry.client.tsx 파일 안에서 사용된다.

props

  • getContext: 네비게이션이나 데이터 페칭이 발생할 때마다 context 인스턴스를 생성하는 context factory 함수이다. 생성된 contextclientAction이나 clientLoader 함수에서 사용할 수 있다.
  • onError: middleware, loader, action, 렌더링 등 앱 전체에서 발생하는 에러를 감지하는 error handler 함수이다. ErrorBoundary와 다르게 UI 리렌더링이 발생하지 않고 에러당 한 번 씩만 실행되므로 에러 로깅에 적합하다. 특히 errorInfo 파라미터는 componentDidCatch 함수에서 넘어와서 렌더링 시에만 존재하므로 렌더링 에러를 확인하기 적합하다.

2-2. ServerRouter

ServerRouter는 서버에서 HTML을 생성하는 역할을 하며, 보통 entry.server.tsx 파일 안에서 사용된다.

props

  • context: manifest, route module 등 렌더링에 필요한 모든 데이터
  • url: 현재 서버가 처리 중인 요청 주소
  • nonce: CSP를 지키기 위한 일회용 암호 값 (optional)
profile
프론트엔드 개발자

0개의 댓글