웹 개발에서 라우팅(Routing)은 접속한 URL의 경로(Route)에 따라 사용자를 원하는 페이지로 신속하게 안내하는 과정입니다. 리믹스(Remix)의 라우팅 시스템은 내부적으로 react-router의 중첩 라우트(Nested routes)를 활용합니다. 중첩 라우트는 react-router나 Next.js 사용 경험이 있는 개발자들에게 친숙한 개념일 것입니다. 간단히 말해, 중첩 라우트는 특정 URL의 중첩된 경로에 있는 모든 레이아웃 요소를 해당 페이지에서 계층적으로 조합하여 사용하는 파일 기반 라우팅 시스템입니다.
리믹스에서는 중첩된 경로에 있는 모든 레이아웃 요소를 해당 라우트 페이지에서 중첩해서 사용합니다. 이를 통해 중복된 UI 개발 코드를 줄이고 페이지 로드 속도를 향상시킬 수 있습니다. 이미지 출저: 리믹스 홈페이지, https://remix.run/
라우트 V1과 V2의 차이점
리믹스 V1에서는 라우트 설정을 폴더 구조를 중첩해서 정의하도록 되어 있었는데요. V2에서는 flat routes 제안 내용을 받아들여 폴더 중첩 없이 파일 이름에서 .으로 중첩을 구분하여 라우트 구조를 정의하도록 변경되었습니다.
V1의 경우…
V1의 라우트는 아래처럼 app/routes/내에 중첩된 폴더 구조 기준으로 라우팅 경로가 정의되는 방식이었습니다.
app/
├── routes/
│ ├── calendar
│ │ ├── details.tsx <- /calendar/details
│ │ └── index.tsx <- /calendar
│ ├── projects/
│ │ ├── $projectId/
│ │ │ ├── details.tsx <- /projects/a1/details
│ │ │ ├── edit.tsx <- /projects/a1/edit
│ │ │ └── index.tsx <- /projects/a1
│ │ └── settings.tsx <- /projects/settings
│ └── projects.tsx <- /projects
└── root.tsx
Plain Text
복사
V1 방식은 페이지 경로를 폴더와 파일 구조를 통해 트리 형태로 확인할 수 있습니다.
V1의 중첩 폴더와 파일 정의 방식은 소규모 프로젝트에서 라우트 구조를 직관적으로 파악할 수 있는 장점이 있습니다. 하지만 대규모 프로젝트에서는 전체 라우트 구조를 한눈에 확인하기 어렵고, 복잡한 경로의 폴더와 파일을 찾는 데 상당한 시간이 소요됩니다. 이는 개발 효율성을 저하시키는 요인이 될 수 있습니다.
V2에서는
V2는 더 평평한 형태의 "flat routes"를 도입했습니다. 이로 인해 기존의 폴더 중첩이 사라졌고, app/routes/디렉토리에서 모든 라우트 구조를 한눈에 확인할 수 있게 되었습니다. 다른 폴더를 찾아다닐 필요 없이 말이죠. 폴더 구조 대신, 파일 이름에서 .를 사용해 중첩 구조를 설정합니다.
app/
├── routes/
│ ├── calendar.details.tsx <- /calendar/details
│ ├── calendar._index.tsx <- /calendar
│ ├── projects.$projectId.details.tsx <- /projects/a1/details
│ ├── projects.$projectId.edit.tsx <- /projects/a1/edit
│ ├── projects.$projectId._index.tsx <- /projects/a1
│ ├── projects.settings.tsx <- /projects/settings
│ └── projects.tsx <- /projects
└── root.tsx
Plain Text
복사
V2 방식은 폴더 트리 대신 파일 네이밍으로 중첩을 구분하기 때문에 routes/ 아래에서 모든 페이지 경로를 확인할 수 있습니다.
V2의 라우트 설정은 V1과 달리 app/routes/ 디렉토리 바로 아래에 평평하게 존재합니다. 이로 인해 전체적인 라우트 구조를 더 빠르게 파악할 수 있습니다. 중첩된 폴더 구조를 찾아다닐 필요가 없어진 거죠. 하지만 V2에도 단점이 있습니다. 경로가 길어질수록 파일 이름도 함께 길어지고, 폴더 트리 구조에 비해 직관성이 다소 떨어집니다.
V2의 파일 네이밍 라우트 사용 방법
리믹스 라우트는 크게 두 가지 구조로 나뉩니다. 하나는 모든 경로의 중첩 시작점이 되는 root 파일이고, 다른 하나는 각 경로마다 파일 이름으로 중첩 라우트를 구성하는 routes/ 폴더입니다.
app/
├── routes/
└── root.tsx
Plain Text
복사
모든 중첩 라우트의 뿌리가 되는 root.tsx
root 파일은 모든 중첩의 밑바탕이 되는 파일로 모든 라우트의 랜더링에 필요한 것 들을 이 곳에서 설정합니다. create-remix로 생성한 프로젝트의 root.tsx 파일을 열어보면 초기 코드가 아래와 같이 되어 있는데요. 프로젝트에서 공통으로 사용하는 <meta> 태그 설정이나 전역 CSS 임포트와 같은 것들을 이 곳에서 처리하도록 하면 됩니다.
// ./app/root.tsx
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet /> {/* 하위 라우트가 랜더링 되는 위치 */}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
TypeScript
복사
root.tsx에서 여러 리믹스 컴포넌트들을 사용하고 있지만 본 포스트는 중첩 라우트에 대해 다루기 때문에 자식 라우트를 중첩해 위치시키는 Outlet 컴포넌트에 대해서만 설명합니다. 다른 리믹스 컴포넌트들은 별도 포스트에서 자세히 다루겠습니다.
자식 중첩 라우트를 위치시키는 Outlet
위 root.tsx 파일 코드에서 <body> 태그 바로 아래에 보면 리믹스의 <Outlet /> 컴포넌트가 위치해 있는데요. 이 <Outlet /> 컴포넌트가 위치한 곳에 중첩된 하위 라우트의 화면 요소가 랜더링 됩니다. 리액트 노드 컴포넌트의 children 프로퍼티와 그 용도가 비슷하다고 볼 수 있겠네요.
이 <Outlet />은 root 뿐만 아니라 화면 중첩을 사용할 모든 라우트에서 사용할 수 있습니다. 리믹스에서 해더(Header), 푸터(Footer) 또는 메뉴 바와 같은 화면 공통 레이아웃은 <Outlet />과 라우트 중첩을 통해 구현합니다.
파일 네이밍 라우트 컨벤션
app/routes 폴더 아래에 있는 모든 파일은 이름을 리믹스가 구문 분석하여 라우터(Router)를 형성하게 됩니다. 가장 기본적이고 우선되는 규칙은 URL 경로가 파일 이름에서 .으로 구분되어 결정된다는 것입니다.
app/
├── routes/
│ └── 1depth.2depth.tsx -> /1depth/2depth
└── root.tsx
Plain Text
복사
중첩된 경로를 사용하면 부모 라우트를 레이아웃으로 사용하게 되고 자식 라우트는 부모 라우트의 <Outlet />이 위치한 곳에 랜더링 됩니다.
app/
├── routes/
│ ├── my.tsx -> /my
│ └── my.details.tsx -> /my/details, my.tsx를 레이아웃으로 사용
└── root.tsx
Plain Text
복사
이름 앞에 _(언더바)를 붙이면 해당 이름이 URL 경로로 사용되지 않습니다. _index.tsx로 사용하면 해당 경로의 인덱스 페이지가 됩니다.
app/
├── routes/
│ ├── _index.tsx -> /
│ ├── my.tsx -> /my
│ └── my.details.tsx -> /my/details
└── root.tsx
Plain Text
복사
_프리픽스를 사용하면 URL 경로로 사용되지 않고 레이아웃으로만 기능하는 모듈을 만들 수 있습니다. 예를 들어, _default.tsx라는 레이아웃 모듈이 있다면, _default를 파일명 앞에 사용한 라우트들은 이 _default.tsx 레이아웃을 적용받게 됩니다.
app/
├── routes/
│ ├── _defualt.tsx URL 경로로 지정되지 않고 레이아웃 모듈로만 사용됨
│ ├── _defualt._index.tsx _defualt 레이아웃 사용
│ ├── my.tsx
│ └── my.details.tsx my 레이아웃 사용
└── root.tsx
Plain Text
복사
_를 이름 끝에 붙이는 suffix로 사용하게 되면 경로는 사용하지만 레이아웃 중첩을 사용하지 않는다는 뜻이 됩니다. 사용 방법은 해당 네임 끝에 _를 붙이는 식입니다.
app/
├── routes/
│ ├── _defualt.tsx
│ ├── _defualt._index.tsx
│ ├── my.tsx
│ └── my_.details.tsx -> /my/details, my 레이아웃은 사용하지 않음
└── root.tsx
Plain Text
복사
$를 프리픽스로 사용하면 URL 세그먼트가 됩니다. 아래와 같이 reservation 경로 뒤에 $id로 세그먼트를 정의하면 /reservation/a123 경로로 접속했을 때 a123을 id 값으로 받아오게 되죠.
app/
├── routes/
│ └── reservation.$id.tsx
└── root.tsx
Plain Text
복사
세그먼트로 정의 한 값은 loader()에서 params로 사용할 수 있게 됩니다.
// app/routes/reservation.$id.tsx
export const loader = async ({ params }: LoaderFunctionArgs) => {
const reservation = await getReservation(params.id);
return { reservation };
};
TypeScript
복사
라우트 구성에 파일 이름 대신 폴더를 사용하여 조직화할 수 있습니다. 폴더 이름으로 대체하면 해당 폴더 내부의 route.tsx 파일로 경로가 연결됩니다. 폴더 내의 다른 파일들은 경로에 영향을 주지 않습니다.
폴더로 라우트를 조직화하는 주된 이유는 단일 파일의 라우트가 크고 복잡해질 때 여러 파일로 나누어 관리하기가 더 쉽기 때문입니다. 예를 들어, 레이아웃으로 사용되는 라우트의 경우 아래 코드처럼 같은 폴더 내에 레이아웃 요소를 각각의 파일로 나누어 두면 요소들이 가까이 있어 찾기 쉽고 유지보수가 훨씬 편리해집니다.
app/
├── routes/
│ ├── _default/
│ │ ├── header.tsx
│ │ ├── footer.tsx
│ │ ├── menu-bar.tsx
│ │ └── route.tsx
│ └── _default.contact/
│ ├── modal.tsx
│ └── route.tsx -> /contact, ../default/route.tsx를 레이아웃으로 사용
└── root.tsx
Plain Text
복사