웹 프레임워크 라우팅: 파일 기반부터 타입 안전한 라우팅까지

웹 프레임워크 라우팅: 파일 기반부터 타입 안전한 라우팅까지

새 프레임워크를 배울 때 가장 먼저 부딪히는 게 라우팅인데요. 어떤 프레임워크는 폴더에 파일만 만들면 페이지가 생기고, 어떤 프레임워크는 설정 파일에 경로를 적어야 합니다. 동적 경로를 [slug]로 쓰는 곳이 있는가 하면 $slug로 쓰는 곳도 있고요. 🤔

겉보기에는 제각각이지만, 요즘 프레임워크가 제공하는 라우팅 기능은 놀랄 만큼 비슷한 문제를 풀고 있습니다. 이 글에서는 TanStack, Astro, React Router, SvelteKit, Nuxt, Next.js를 예로 들어, 프레임워크들이 공통으로 제공하는 라우팅 전략을 하나씩 비교해 보겠습니다.

파일 기반 라우팅과 코드 기반 라우팅

라우트를 정의하는 방법은 크게 두 가지입니다.

파일 기반 라우팅(file-based routing)은 폴더와 파일 구조가 그대로 URL 경로가 되는 방식입니다. 정해진 폴더에 파일을 만들면 프레임워크가 알아서 경로를 등록해 줍니다.

프레임워크라우트 폴더/about 페이지
TanStacksrc/routes/src/routes/about.tsx
Astrosrc/pages/src/pages/about.astro
SvelteKitsrc/routes/src/routes/about/+page.svelte
Nuxtapp/pages/app/pages/about.vue
Next.jsapp/app/about/page.tsx

파일 목록만 봐도 어떤 페이지가 있는지 바로 보이고, 라우트를 등록하는 코드를 따로 관리할 필요가 없다는 게 장점입니다. 그래서 요즘 메타 프레임워크는 대부분 파일 기반 라우팅을 기본으로 씁니다.

반대편에는 코드 기반 라우팅(code-based routing)이 있습니다. 경로와 컴포넌트를 코드로 직접 짝지어 등록하는 방식인데요. 전통적인 React Router가 대표적이고, React Router v7의 프레임워크 모드도 app/routes.ts 설정 파일에 라우트를 적습니다.

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

export default [
  index("./home.tsx"),
  route("about", "./about.tsx"),
  route("posts/:postId", "./post.tsx"),
] satisfies RouteConfig;

코드 기반은 파일 이름 규칙에 묶이지 않아서, 폴더 구조를 자유롭게 짜거나 조건에 따라 라우트를 바꾸기 좋습니다. TanStack Router처럼 두 방식을 모두 지원하는 라우터도 있습니다.

동적 경로와 캐치올

블로그 글이나 상품 페이지처럼 경로의 일부가 값으로 바뀌는 경우에는 동적 경로(dynamic route)를 씁니다. 그리고 남은 경로 전체를 한 번에 받는 캐치올(catch-all) 경로도 거의 모든 프레임워크가 지원하는데요. 문법은 프레임워크마다 다릅니다.

프레임워크동적 경로캐치올
TanStack$slug$
Astro[slug][...path]
React Router:slug*
SvelteKit[slug][...rest]
Nuxt[slug][...slug]
Next.js[slug][...slug]

기호 계열(TanStack, React Router)과 대괄호 계열(Astro, SvelteKit, Nuxt, Next.js)로 나뉘는 셈입니다. 캐치올은 /docs/guide/install처럼 깊이가 정해지지 않은 경로를 한 페이지에서 처리할 때 쓰는데요. TanStack Router는 $로 받은 나머지 경로를 _splat이라는 이름의 파라미터로 넘겨 줍니다.

일부 프레임워크는 선택적(optional) 파라미터도 지원합니다. SvelteKit의 [[lang]]/home은 /home과 /en/home을 모두 받고, Nuxt도 같은 이중 대괄호 문법을 씁니다. Next.js의 [[...slug]]는 경로가 아예 없는 경우까지 포함하는 캐치올입니다. 다국어 사이트에서 언어 코드를 생략할 수 있게 할 때 유용합니다.

중첩 라우트와 레이아웃

여러 페이지가 같은 헤더나 사이드바를 공유할 때는 중첩 라우트(nested route)와 레이아웃(layout)을 씁니다. 바깥 라우트가 공통 화면을 그리고, 그 안에 자식 라우트의 화면을 끼워 넣는 구조인데요. 파일 기반 프레임워크는 대부분 레이아웃 전용 파일 이름을 정해 둡니다.

프레임워크레이아웃 파일
TanStackroute.tsx 또는 부모 라우트 파일
Astro레이아웃 컴포넌트로 직접 감싸기
React Routerlayout()로 감싼 라우트
SvelteKit+layout.svelte
Nuxtapp/layouts/ 폴더 + definePageMeta
Next.jslayout.tsx

예를 들어 SvelteKit에서 src/routes/blog/+layout.svelte를 만들면 /blog와 그 아래 모든 페이지가 이 레이아웃으로 감싸집니다. 레이아웃은 페이지를 이동해도 다시 그려지지 않아서, 사이드바의 스크롤 위치나 입력 상태가 그대로 유지됩니다. 중첩 라우팅의 동작 원리는 React Router로 중첩 라우팅 하기에서 자세히 다뤘습니다.

Astro는 조금 다른데요. 레이아웃을 파일 이름 규칙이 아니라 일반 컴포넌트로 만들고, 각 페이지에서 직접 감싸서 씁니다. 페이지가 이동할 때마다 HTML을 새로 받는 다중 페이지 앱이 기본이라, 레이아웃을 유지할 필요가 적기 때문입니다.

자주 쓰이는 응용으로 경로에 나타나지 않는 그룹이 있습니다. 로그인 화면과 대시보드에 서로 다른 레이아웃을 쓰고 싶은데 URL에는 그룹 이름이 들어가지 않았으면 할 때인데요. TanStack과 SvelteKit, Nuxt, Next.js는 폴더 이름을 괄호로 감싸 (marketing), (app)처럼 쓰면 URL에서 빠지는 라우트 그룹(route group)을 만듭니다.

src/routes/
├── (marketing)/
│   ├── about/
│   └── +layout.svelte
└── (app)/
    ├── dashboard/
    └── +layout.svelte

이 SvelteKit 예에서 /about과 /dashboard는 각자 다른 레이아웃을 쓰지만, 주소에는 (marketing)이나 (app)이 들어가지 않습니다. TanStack은 이름 앞에 _를 붙인 _pathlessLayout처럼 경로 없는 레이아웃 라우트도 따로 지원하고, React Router는 layout()으로 감싸기만 하면 URL에 경로가 추가되지 않습니다.

클라이언트 라우팅과 서버 라우팅

라우팅이 어디서 일어나느냐도 중요한 차이입니다.

전통적인 웹사이트는 링크를 누를 때마다 서버에 새 HTML을 요청하고 페이지 전체를 다시 그립니다. 이를 서버 라우팅이라고 하는데요. 반대로 클라이언트 라우팅은 브라우저의 History API로 주소만 바꾸고, JavaScript로 필요한 부분만 다시 그립니다. 페이지 전체를 새로 받지 않으니 화면 전환이 빠르고, 레이아웃 상태도 유지됩니다.

요즘 메타 프레임워크는 대부분 둘을 섞습니다. 첫 페이지는 서버가 HTML로 그려서 빠르게 보여 주고, 그 뒤로 이동할 때는 클라이언트 라우팅으로 전환하는 방식인데요. TanStack Start, React Router의 프레임워크 모드, SvelteKit, Nuxt, Next.js가 모두 이렇게 동작합니다. 이 렌더링 방식의 차이는 SPA, SSG, SSR 비교 글에서 다뤘습니다.

Astro는 반대로 서버 라우팅이 기본입니다. 링크를 누르면 다음 페이지의 HTML을 새로 받는데, 그 대신 페이지마다 JavaScript가 거의 없어서 가볍습니다. 콘텐츠 중심 사이트라면 이쪽이 오히려 단순하고 빠를 수 있습니다.

데이터 로딩과 프리페치

페이지를 보여 주려면 대개 데이터가 필요한데요. 컴포넌트 안에서 데이터를 불러오면 화면이 그려진 뒤에야 요청이 시작되고, 중첩된 컴포넌트마다 요청이 줄줄이 이어지는 폭포(waterfall) 문제가 생기기 쉽습니다. 그래서 요즘 프레임워크는 라우트 단위로 데이터를 미리 불러오는 로더(loader)를 제공합니다.

src/routes/posts.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts")({
  loader: async () => {
    const posts = await fetchPosts();
    return { posts };
  },
  component: Posts,
});

function Posts() {
  const { posts } = Route.useLoaderData();
  return <ul>{/* ... */}</ul>;
}

TanStack의 loader와 React Router의 loader, SvelteKit의 +page.js에 두는 load 함수가 같은 역할입니다. 라우터가 페이지로 이동하기로 결정한 순간 데이터 요청이 시작되니, 화면을 그리기 전에 데이터가 준비됩니다. Nuxt는 페이지 컴포넌트에서 useFetch나 useAsyncData로, Astro는 페이지 상단의 프런트매터(frontmatter)에서, Next.js는 서버 컴포넌트에서 await로 바로 데이터를 가져옵니다. 방식은 달라도 “라우트가 데이터를 책임진다”는 생각은 같습니다.

여기서 한 걸음 더 나간 게 프리페치(prefetch)입니다. 사용자가 링크를 누르기 전에 다음 페이지의 코드와 데이터를 미리 받아 두는 건데요. 프레임워크마다 기본값이 꽤 다릅니다.

프레임워크설정기본 동작
TanStackpreload="intent", defaultPreload직접 켜야 함
Astroprefetch 설정 + data-astro-prefetch직접 켜야 함
React Router<Link prefetch="intent">하지 않음 (none)
SvelteKitdata-sveltekit-preload-data새 프로젝트 템플릿에서 마우스를 올리면
Nuxt<NuxtLink prefetch-on>링크가 화면에 보이면
Next.js<Link prefetch>링크가 화면에 보이면 (프로덕션만)

intent는 링크에 마우스를 올리거나 포커스했을 때를 뜻합니다. 사용자가 실제로 누르기까지 보통 수백 밀리초가 걸리니, 그 사이에 데이터를 받아 두면 클릭하자마자 화면이 바뀌는 것처럼 느껴집니다. 데이터 캐싱과 함께 쓰면 효과가 더 큰데, TanStack Query와 같이 쓰는 패턴이 대표적입니다.

타입 안전한 라우팅

마지막으로 최근 가장 빠르게 퍼지고 있는 흐름이 타입 안전한 라우팅입니다. 경로를 문자열로 다루다 보면 /abuot처럼 오타를 내거나, 파라미터 이름을 잘못 쓰는 실수가 런타임에야 드러나곤 하는데요. 이걸 컴파일 단계에서 잡아 주는 겁니다.

TanStack이 이 흐름을 이끌었습니다. 라우트 파일을 읽어서 routeTree.gen.ts라는 타입 파일을 자동으로 만들고, 이 타입으로 <Link>의 경로와 파라미터, 검색 파라미터까지 검사합니다.

<Link to="/posts/$postId" params={{ postId: "1" }}>
  첫 번째 글
</Link>

to에 없는 경로를 쓰거나 params를 빠뜨리면 에디터에서 바로 오류가 납니다.

다른 프레임워크도 비슷한 방향으로 가고 있습니다. React Router는 라우트마다 +types 파일을 생성해 loader의 params와 반환값에 타입을 붙여 주고, SvelteKit도 ./$types에서 PageLoad 같은 타입을 가져와 load 함수의 파라미터를 검사합니다. Nuxt는 아직 실험 기능인 experimental.typedPages를 켜면 <NuxtLink>와 navigateTo()의 경로에 타입이 붙고, Next.js는 typedRoutes: true를 켜면 <Link>의 href를 실제 라우트와 대조해 검사합니다.

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    typedPages: true,
  },
});

공통점은 파일이나 설정에서 라우트 정보를 읽어 타입을 자동으로 생성한다는 점입니다. 파일 기반 라우팅이 “라우트 목록을 따로 관리하지 않아도 된다”는 편리함에서 출발했다면, 타입 안전한 라우팅은 그 목록을 타입으로 바꿔 실수를 막는 데까지 나아간 셈입니다.

마치며

프레임워크마다 문법은 달라도, 라우팅 기능이 푸는 문제는 거의 같습니다. 경로를 어떻게 정의할지, 값이 바뀌는 경로를 어떻게 받을지, 공통 화면을 어떻게 나눌지, 데이터를 언제 불러올지, 그리고 실수를 어떻게 막을지입니다.

그래서 새 프레임워크를 배울 때 이 질문들을 기준으로 문서를 보면 훨씬 빨리 익숙해질 수 있습니다. “이 프레임워크에서는 레이아웃을 어떻게 만들지?”, “로더는 어디에 두지?”처럼요. 문법은 금방 외워지고, 결국 남는 건 그 아래의 개념이니까요. 😊

각 라우터의 규칙을 더 자세히 보고 싶다면 TanStack Router의 라우팅 개념 문서가 다른 프레임워크와 비교하며 읽기 좋습니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord