라우팅: 중첩 라우팅과 라우트 구성
라우팅: 중첩 라우팅과 라우트 구성
화면이 커지면 URL과 화면, 그리고 그 화면이 쓰는 데이터가 뒤엉키기 쉬워요. Remix의 라우팅은 이런 문제를 **중첩 라우팅(nested routes)**으로 풀어요. 이 접근의 뿌리는 Ember.js까지 거슬러 올라가요 — URL의 각 세그먼트를 데이터 의존성과 컴포넌트 계층에 짝지어 놓는 방식이죠. /sales/invoices/102000 같은 URL 하나가 애플리케이션 안의 명확한 경로를 드러낼 뿐 아니라, 서로 다른 컴포넌트들의 관계와 의존성도 함께 보여줘요.
중첩 라우팅이 주는 것
모듈식 설계 — URL을 여러 조각으로 나누면 각 조각이 특정한 데이터 요구와 컴포넌트에 직접 대응돼요. /sales/invoices/102000에서 sales, invoices, 102000 각각이 특정 데이터와 UI 영역에 연결되니, 코드베이스에서 관리하기가 직관적이죠. 무엇보다 중첩 라우트 트리에서 여러 라우트가 하나의 URL에 동시에 매칭될 수 있어요. 각 라우트는 자기 URL 세그먼트와 그에 딸린 UI 조각에 집중하게 되고, 이게 모듈성과 관심사 분리(single responsibility) 원칙을 그대로 살려줘요.
병렬 로딩 — 일반적인 웹 앱에서는 데이터 의존성이 서로 얽혀 있지 않아도, 렌더링 계층 순서를 따라 순차적으로 로딩되곤 해요. 그러면 불필요한 요청 체인이 생기면서 체감 속도가 느려지죠. Remix는 URL 하나에 여러 라우트가 걸리면 매칭된 모든 라우트의 데이터와 자산을 병렬로 가져와요. 이렇게 하면 요청이 체인처럼 이어지는 흔한 함정을 피할 수 있고, 현대 브라우저의 동시 요청 처리 능력과 어우려져 훨씬 빠른 응답을 만들어내요. 데이터를 빨리 가져오는 것뿐 아니라 "질서 있게" 가져와서 사용자 경험을 좋게 만드는 게 핵심이죠.
관례 기반 라우트 구성 (app/routes 폴더)
Remix가 라우팅을 간단하게 만들어 주는 핵심 관례가 바로 app/routes 폴더예요. 이 폴더에 파일을 하나 만들면 Remix가 그 파일을 라우트로 알아서 인식해요. 라우트 정의, URL 연결, 컴포넌트 렌더링이 하나의 파일 안에서 정리되는 셈이죠.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.tsx
app/routes/concerts.로 시작하는 모든 라우트는 app/routes/concerts.tsx의 자식 라우트가 돼요.
| URL | 매칭되는 라우트 | 레이아웃 |
|---|---|---|
/ |
app/routes/_index.tsx |
app/root.tsx |
/about |
app/routes/about.tsx |
app/root.tsx |
/concerts |
app/routes/concerts._index.tsx |
app/routes/concerts.tsx |
/concerts/trending |
app/routes/concerts.trending.tsx |
app/routes/concerts.tsx |
/concerts/salt-lake-city |
app/routes/concerts.$city.tsx |
app/routes/concerts.tsx |
라우트 폴더 관례 — 라우트에 딸린 모듈이나 자산이 많다면, app/routes 안에 폴더를 만들고 그 안에 route.tsx 파일을 두는 방법도 있어요. 이렇게 하면 해당 라우트와 연결된 로직·스타일·컴포넌트가 한곳에 모이고(모듈 공동 배치), import 관리가 단순해지며, 프로젝트가 커져도 코드가 잘 정리되도록 유도돼요. 위 예시를 같은 라우트를 폴더 구조로 풀면 이렇게 돼요.
app/
├── routes/
│ ├── _index/
│ │ ├── signup-form.tsx
│ │ └── route.tsx
│ ├── about/
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── favorites-cookie.ts
│ │ └── route.tsx
│ ├── concerts.$city/
│ │ └── route.tsx
│ ├── concerts._index/
│ │ ├── featured.tsx
│ │ └── route.tsx
│ └── concerts.trending/
│ ├── card.tsx
│ ├── route.tsx
│ └── sponsored.tsx
└── root.tsx
주의할 점은 app/routes 바로 아래 폴더만 라우트로 등록된다는 거예요. 더 깊이 중첩된 폴더는 무시돼요. app/routes/about/header/route.tsx 같은 파일은 라우트를 만들지 않아요.
수동 라우트 구성 (vite.config.ts)
app/routes 폴더가 편리하지만, Remix는 "하나의 관례가 모든 프로젝트에 맞진 않는다"는 점도 잘 알고 있어요. 프로젝트 요구나 선호에 따라 vite.config.ts에서 라우트를 수동으로 구성할 수도 있어요. (아직 Vite로 마이그레이션하지 않았다면 Classic Remix Compiler 기준 remix.config.js에서도 가능해요.)
기능별 최상위 폴더로 구조를 짜고 싶다면, vite.config.ts의 routes 함수로 이렇게 매핑할 수 있어요.
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
remix({
routes(defineRoutes) {
return defineRoutes((route) => {
route("/", "home/route.tsx", { index: true });
route("about", "about/route.tsx");
route("concerts", "concerts/layout.tsx", () => {
route("", "concerts/home.tsx", { index: true });
route("trending", "concerts/trending.tsx");
route(":city", "concerts/city.tsx");
});
});
},
}),
],
});
요점은 Remix의 라우트 구성이 관례와 유연성의 조합이라는 거예요. app/routes 폴더로 쉽고 정돈된 시작을 하고, 파일명이 마음에 안 들거나 더 세밀한 제어가 필요하면 vite.config.ts로 넘어가면 되죠. 실제로 많은 앱은 routes 폴더 관례를 버리고 vite.config.ts 쪽을 선호하기도 해요.
더 알아보기
- 공식 문서 (1차): Remix — Route Configuration, Route File Conventions
- 관련 문서: 데이터 로딩 · 로더