Remix의 모듈에 대해서 작성을 하려고했는데 어쩌다보니 Route에 관련해서 테스트를 진행해보았고, 그결과 Remix의 Route Folder 구조에 대해서 작성하고자 한다.
Remix의 공식홈페이지에 있는 글을 기반으로 번역하고 실습하였다.
그럼! 먼저 Remix의 Route 구조는 기본적으로 다음과 같다.
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx
먼저 root.tsx파일을 살펴보면 프로젝트의 루트 레이아웃을 역할을 하며, 다른 모든 경로는 <Outlet/>안에서 렌더링이 된다.
// root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export default function Root() {
return (
<html lang="en">
<head>
<Links />
<Meta />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
폴더 구조를 URL로 예를 들자면 아래 표와같이 동작을 한다고 생각하면 된다.
| URL | 파일 경로 |
|---|---|
| / | app/routes/_index.tsx |
| /about | app/routes/about.tsx |
하나의 Route파일 이름에 .을 추가를 할 수 있는데 추가해보자
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.salt-lake-city.tsx
│ └── concerts.san-diego.tsx
└── root.tsx
| URL | 파일 경로 |
|---|---|
| / | app/routes/_index.tsx |
| /about | app/routes/about.tsx |
| /concerts/trending | app/routes/concerts.trending.tsx |
| /concerts/salt-lake-city | app/routes/concerts.salt-lake-city.tsx |
| /concerts/san-diego | app/routes/concerts.san-diego.tsx |
정적인 URL이 아닌 URL에 /about/1234와 같이 동적 값이 들어가는 경우
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ └── concerts.trending.tsx
└── root.tsx
| URL | 파일 경로 |
|---|---|
| / | app/routes/_index.tsx |
| /about | app/routes/about.tsx |
| /concerts/trending | app/routes/concerts.trending.tsx |
| /concerts/12345 | app/routes/concerts.$id.tsx |
| /concerts/idtest | app/routes/concerts.$id.tsx |
이때 동적으로 들어온 값은 loader함수의 params매개변수를 이용하여 가져올 수 있다.
자세한 내용은 참고하자. 나중에 추가
개발을 하다보면 레이아웃도 사용하고 중첩 라우팅을 많이 사용하게 될텐데 그때의 구조는 다음과 같다
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.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 |
URL은 중복이 되지만 레이아웃은 적용하지 싶지않을때 부모 세그먼트 끝에 _를 붙이는 다음과 같은 구조를 사용한다.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.tsx
│ └── concerts_.mine.tsx
└── root.tsx
| URL | 파일 경로 | 레이아웃 |
|---|---|---|
| / | app/routes/_index.tsx | app/root.tsx |
| /about | app/routes/about.tsx | app/root.tsx |
| /concerts/mine | app/routes/concerts_.mine.tsx | app/root.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 |
중첩 URL이 없는 중첩 레이아웃이라고 하는데, 이게 무슨소리냐면 예로 /auth/login과 /auth/register처럼 사용하지 않고 /login과 /register만 사용하여 공통된 레이아웃을 공유하고 싶은 경우를 의미한다.
app/
├── routes/
│ ├── _auth.login.tsx
│ ├── _auth.register.tsx
│ ├── _auth.tsx
│ ├── _index.tsx
│ ├── concerts.$city.tsx
│ └── concerts.tsx
└── root.tsx
| URL | 파일 경로 | 레이아웃 |
|---|---|---|
| / | app/routes/_index.tsx | app/root.tsx |
| /login | app/routes/_auth.login.tsx | app/routes/_auth.tsx |
| /register | app/routes/_auth.register.tsx | app/routes/_auth.tsx |
| /concerts | app/routes/concerts.tsx | app/root.tsx |
| /concerts/salt-lake-city | app/routes/concerts.$city.tsx | app/routes/concerts.tsx |
url의 /kr/about과 /en/about과 같이 url의 경로가 선택적(동적)으로 바뀌는 경우
app/
├── routes/
│ ├── ($lang)._index.tsx
│ ├── ($lang).$productId.tsx
│ └── ($lang).categories.tsx
└── root.tsx
| URL | 파일 경로 |
|---|---|
| / | app/routes/($lang)._index.tsx |
| /categories | app/routes/($lang).categories.tsx |
| /en/categories | app/routes/($lang).categories.tsx |
| /fr/categories | app/routes/($lang).categories.tsx |
| /american-flag-speedo | app/routes/($lang)._index.tsx |
| /en/american-flag-speedo | app/routes/($lang). $productId.tsx |
| /fr/american-flag-speedo | app/routes/($lang). $productId.tsx |
슬래시를 포한한 URL의 나머지 부분과 일치할때
app/
├── routes/
│ ├── _index.tsx
│ ├── $.tsx
│ ├── about.tsx
│ └── files.$.tsx
└── root.tsx
| URL | 파일 경로 |
|---|---|
| / | app/routes/_index.tsx |
| /about | app/routes/about.tsx |
| /beef/and/cheese | app/routes/$.tsx |
| /files | app/routes/files.$.tsx |
| /files/talks/remix-conf_old.pdf | app/routes/files.$.tsx |
| /files/talks/remix-conf_final.pdf | app/routes/files.$.tsx |
| /files/talks/remix-conf-FINAL-MAY_2022.pdf | app/routes/files.$.tsx |
app/
├── routes/
│ ├── _landing._index.tsx
│ ├── _landing.about.tsx
│ ├── _landing.tsx
│ ├── app._index.tsx
│ ├── app.projects.tsx
│ ├── app.tsx
│ └── app_.projects.$id.roadmap.tsx
└── root.tsx
위의 구조를 가진 파일형식의 파일구조를 폴더형식으로 변환하면 다음과 같은 구조로 변환된다.
혹시 route.tsx
app/
├── routes/
│ ├── _landing._index/
│ │ └── route.tsx
│ ├── _landing.about/
│ │ └── route.tsx
│ ├── _landing/
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── app._index/
│ │ └── route.tsx
│ ├── app.projects/
│ │ └── route.tsx
│ ├── app/
│ │ ├── footer.tsx
│ │ ├── primary-nav.tsx
│ │ └── route.tsx
│ └── app_.projects.$id.roadmap/
│ └── route.tsx
└── root.tsx
추가로 아래의 같은 경우에는 동일한 라우터로 인식을 한다고한다.
# these are the same route:
app/routes/app.tsx
app/routes/app/route.tsx
# as are these
app/routes/app._index.tsx
app/routes/app._index/route.tsx
⚠️ 이때 주의할점은 폴더 형식으로 Nested Routes 구성하는 경우 바로 routes폴더 바로 하위에 있는 폴더만 경로로 등록된다고 한다. 예로
app/routes/about/header/route.tsx는 무시되고app/routes/about/route.tsx까지만 경로로 등록된다고 한다.문서 안보고 이것저것 해보다가 개고생함
폴더로 변환하는 형식이 이해가 안가서 실습을 진행해 보았는데 중첩으로 라우터를 사용하는 URL들이 독립적으로 동작하는 것과 레이아웃을 포함하는 라우터를 실습해보았다.
폴더구조는 다음과 같다.
app/
├── routes/
│ ├── loader-and-action._index/
│ │ └── route.tsx
│ └── loader-and-action.$id/
│ └── route.tsx
└── root.tsx
/loader-and-action경로로 들어 갔을때는 독립적인 loader-and-action만 표출하고, /loadfer-and-action/1 경로만 표출한다.
// app/rotues/loader-and-action.$id/route.tsx
export default function LoaderAndAction() {
return <div>단독 페이지 입니다. </div>
}
// app/routes/loader-and-action/$id.tsx
import { LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
console.log("params:", params); // 디버깅용
return {
id: params.id
};
}
export default function IdPage() {
const { id } = useLoaderData<typeof loader>();
return (
<div>
<h1>ID: {id}</h1>
</div>
);
}
| /loader-and-action | loader-and-action/1 |
|---|---|
![]() | ![]() |
app/
├── routes/
│ ├── root-outlet/
│ │ └── route.tsx
│ └── root-outlet.$id/
│ └── route.tsx
└── root.tsx
/root-outlet경로로 들어 갔을때는 부모 레이아웃을 표출하고, /root-outlet/1 경로로 들어갔을때는 부모와 자식 모두 표출한다.
// app/rotues/root-outlet/route.tsx
import { Outlet } from "@remix-run/react";
export default function Page() {
return (
<div>
루트입니다.
<Outlet/>
</div>
);
}
// app/rotues/root-outlet.$id/route.tsx
export default function Page() {
return (
<div>
루트의 자식입니다.
</div>
);
}
| /root-outlet | /root-outlet/1 |
|---|---|
![]() | ![]() |
이때 route.tsx대신 index.tsx를 사용해도 정상 동작을 했는데, Claude에게 물어봤더니 차이점이 있다고 한다. 실제로 사용하면서 차이점을 알아야 할것같다.
- route.tsx
1. 레이아웃 컴포넌트 역할
2. 자식 라우트들의 공통 레이아웃 제공
3. 반드시 <Outlet />을 포함해야 함
- index.tsx 또는 _index.tsx
1. 인덱스 라우트 (기본 페이지)
2. 해당 경로의 메인 페이지 역할
코드만 보았을때는 <Outlet/>때문에 부모가 자식을 포함하는것처럼 보이는데 loader-and-action._index/route.tsx에서 <Outlet/>을 사용해도 자식이 표출이 안된다. 폴더명을 자세히보자

중첩 라우터를 만들때는 어떻게 폴더 구조를 가져가야하는지 실습해보았다.
app/
├── routes/
│ ├── nested/
│ │ └── index.tsx
│ └── nested.depth1/
│ └── route.tsx
│ └── nested.depth1.depth2/
│ └── route.tsx
└── root.tsx
이렇게 폴더를 만들어서 구성을 해주어야 함
pathless의 경우에는 아래와 같은 폴더 구조로 만들어줘야 한다.
app/
├── routes/
│ ├── _auth.signup/
│ │ └── route.tsx
│ └── _auth.login/
│ └── route.tsx
└── root.tsx
Remix v1에서는 pathless route를 __auth 형태로 설정해주었지만 v2에서는 _auth형태로 언더바가 하나 빠진 형태로 폴더구조를 구성해주면 된다.
Dynamic Route의 경우에 는 다음과 같은 구조로 만들어주면 된다.
app/
├── routes/
│ ├── dynamic/
│ │ └── route.tsx
│ └── dynamic.$id/
│ │ └── route.tsx
│ └── dynamic.$id.$code/
│ └── route.tsx
└── root.tsx
https://remix.run/docs/en/main/file-conventions/routes#folders-for-organization