티스토리 뷰
⇩ (주절주절)
작년 첫 번째 팀 프로젝트 이후 개인 프로젝트만 줄곧 해왔던 내가 두 번째 팀 프로젝트에 참여하게 되었다...!
첫 대면 회의에서 어떤 프레임워크를 사용할지에 얘기하다가 팀원분이 추천한 Remix!!
React, Next만 알던 내게 Remix의 등장이라...
Remix를 파헤져보자!!
🩵 Remix
SSR만을 지원하는 풀스택 웹 개발 프레임워크
Web Fetch API를 기반으로 구축되어 Node.js가 아닌 환경에서도 실행할 수 있다.
1. Remix 주요기능
React Router를 기반으로 구축된 Remix는 네 가지 기능을 제공한다.
- 컴파일러
- 서버 사이드 HTTP 핸들러
- 서버 프레임워크
- 브라우저 프레임워크
- 컴파일러
Remix의 모든 것은 컴파일러로 시작된다!
remix vite:build
Vite를 사용해 빌드하면 몇 가지가 생성된다.
- build/server/index.js
- 서버에서 렌더링하고 리소스에 대한 다른 서버 측 요청을 처리할 수 있도록 모든 경로와 모듈을 함께 포함하는 서버 HTTP 처리기
- build/client/*
- 브라우저에서 애플리케이션을 실행하는 데 필요한 모든 것이 포함됨
- ex) 경로별 자동 코드 분할, CSS와 이미지 등 인증된 자산(?) 가져오기 등등
- asset manifest
- 클라이언트와 서버 모두 asset manifest를 사용해 전체 종속성 그래프를 파악
- 초기 서버 렌더링에서 리소스를 미리 로드하고 클라이언트 측 전환을 위해 리소스를 미리 가져오는데 유용함
- => 웹 앱에서 흔히 발생하는 render + fetch waterfalls를 제거할 수 있는 방법!
- HTTP 핸들러 및 어댑터
Remix는 실제 서버가 아닌 JavaScript 서버에 제공되는 핸들러이다.
Node.js 대신 Web Fetch API를 기반으로 구축되어 Vercel, Netlify 등 모든 Node.js 서버뿐만 아니라
Cloudflare Workers, Deno Deploy와 같은 Node.js가 아닌 환경에서도 실행할 수 있다.
즉,
Remix 핸들러는 서버에 구애를 받지 않는다!
⇩ Express에서 Remix를 실행하는 코드
const remix = require("@remix-run/express");
const express = require("express");
const app = express();
app.all(
"*",
remix.createRequestHandler({
build: require("./build/server"),
})
);
여기서 remix는 express 서버의 핸들러이다.
remix를 가져오는 패키지인 "@remix-run/express"를 어댑터라고 부른다.
어댑터는 서버의 request/response API를 도중에 Fetch API로 변환한 다음 Remix에서 오는 Fetch Response를 서버의 응답 API에 적용하여 특정 서버에서 동작하도록 만든다.
이때 어댑터가 수행하는 작업에 대한 의사코드는 아래와 같다.
export function createRequestHandler({ build }) {
// 서버 build로부터 Fetch API 요청 핸들러 생성
const handleRequest = createRemixRequestHandler(build);
// Express 서버에 대한 express.js 특정 핸들러 리턴
return async (req, res) => {
// express.req를 Fetch API request에 맞게 조정
const request = createRemixRequest(req);
// 앱 핸들러를 호출하고 Fetch API response 받기
const response = await handleRequest(request);
// Fetch API response를 express.res에 적용
sendRemixResponse(res, response);
};
}
- 서버 프레임워크
Remix는 View이자 Controller이지만 모델은 개발자에게 달려있다.
View와 Controller를 분리하는 대신 Remix Route 모듈이 두 역할을 모두 수행한다.
대부분의 서버 사이드 프레임워크는 "model focused"이며 Controller는 단일 모델에 대한 여러 URL을 관리한다.
Remix는 UI에 중점을 둔다.
Routes는 전체 URL을 처리하거나 URL의 일부만 처리할 수 있다.
route가 세그먼트에만 맵핑되면, 중첩된 URL 세그먼트는 UI에서 중첩된 레이아웃이 된다.
이러한 방식으로 각 레이아웃(view)은 자체 controller가 될 수 있으며 Remix는 데이터와 구성 요소를 집계하여 완전한 UI를 구축한다.
대개 Remix Route 모듈에는 동일한 파일에 UI와 모델과의 상호 작용이 모두 포함될 수 있어 개발자의 인체 공학적 측면(?)과 생산성으로 이어진다. (생산성이 올라간다는 의미?)
경로 모듈에는 세 가지 기본 내보내기가 있다.
- loader
- action
- default(컴포넌트)
loader
Remix에서 최초 로딩 시 호출되는 함수
loader는 서버에서만 실행되며 GET 요청 시 컴포넌트에 데이터를 제공한다. <- 다른 소스로부터 데이터를 가져옴
최초에 로딩되어야 하는 UI에 API를 호출할 때 유리!!
action
web에서 어떤 동작이 주어지게 될 경우 실행되는 함수 (버튼이 눌리거나 input의 value가 변하는 경우)
action은 서버에서만 실행되며 POST, PUT, PATCH 그리고 DELETE를 처리한다.
또한 컴포넌트에 데이터를 제공할 수 있다.
default
경로가 URL과 일치할 때 렌더링되는 컴포넌트
서버와 클라이언트 모두에서 실행된다.
export async function loader() {
return json(await db.projects.findAll());
}
export default function Projects() {
const projects = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<div>
{projects.map((project) => (
<Link key={project.slug} to={project.slug}>
{project.title}
</Link>
))}
<Form method="post">
<input name="title" />
<button type="submit">Create New Project</button>
</Form>
{actionData?.errors ? (
<ErrorMessages errors={actionData.errors} />
) : null}
{/*
outlets는 이 경로보다 더 깊은 URL과 일치하는 첩된 하위 경로를 렌더링하여
각 레이아웃이 동일한 파일에서 UI와 controller 코드를 같은 위치에 배치할 수 있도록 함
*/}
<Outlet />
</div>
);
}
export async function action({
request,
}: ActionFunctionArgs) {
const form = await request.formData();
const errors = validate(form);
if (errors) {
return json({ errors });
}
await createProject({ title: form.get("title") });
return json({ ok: true });
}
실제 브라우저 JavaScript를 사용하지 않고도 Remix를 서버 사이드 프레임워크로 사용할 수 있다.
loader를 사용한 데이터 로드, action과 HTML 폼의 변형, URL에서 렌더링되는 컴포넌트에 대한 route 규칙은 많은 웹 프로젝트의 핵심 기능 세트를 제공할 수 있다.
이러한 방식으로 Remix의 규모가 축소된다.
Remix에서는 먼저 간단한 방법으로 구축한 다음 기본 모델을 변경하지 않고 확장할 수 있다.
또한 대부분의 앱은 JavaScript가 브라우저에 로드되기 전에 작동하므로 Remix 앱은 설계상 고르지 못한 네트워크 조건에 탄력적으로 대처할 수 있다.
- Browser Framework
Remix가 문서를 브라우저에 제공하면, 브라우저 빌드의 JavaScript 모듈을 사용해 페이지를 "hydrate" 한다.
이 곳이 Remix의 "브라우저 에뮬레이팅"에 대해 많이 이야기 된다.
유저가 링크를 클릭하면 전체 문서와 모든 assets에 대해 서버를 왕복하는 대신 Remix는 단순히 다음 페이지의 데이터를 가져와 UI를 업데이트한다.
추가로 유저가 데이터 업데이트를 위해 <Form>을 제출할 때, 일반적인 HTML 문서 요청을 수행하는 대신 브라우저 런타임은 서버에 fetch를 수행하고 페이지의 모든 데이터를 자동으로 재검증하고 React로 업데이트 한다.
이는 전체 문서를 요청하는 것보다 많은 성능 이점을 가진다.
- Assets를 다시 다운로드하거나 캐시에서 가져올 필요가 없다.
- Assets를 브라우저에서 다시 구문 분석할 필요가 없다.
- 가져온 데이터는 전체 문서보다 훨씬 작다.(때로는 수십 배나!)
- Remix는 HTML API(<a> 와 <form>)를 향상시키기 때문에 JavaScript가 페이지에 로드되기 전에도 앱이 작동하는 경향이 있다.
Remix에는 클라이언트 사이드 탐색을 위한 최적화 기능도 내장되어 있다.
두 URL 사이에 어떤 레이아웃이 유지될지 알고 있으므로 변경되는 레이아웃에 대한 데이터만 가져온다.
전체 문서를 요청하려면 모든 데이터를 서버에서 가져와야 하므로 백엔드의 리소스가 낭비되고 앱 속도가 느려진다.
이 접근 방식은 사이드바 탐색의 스크롤 위치를 재설정하지 않고 문서 상단보다 더 의미 있는 항목으로 초점을 이동할 수 있는 것과 같은 UX 이점도 있다.
Remix는 사용자가 링크를 클릭하려고 할 때 페이지의 모든 리소스를 미리 가져올 수도 있다.
브라우저 프레임워크는 컴파일러의 asset manifest에 대해 알고있다. 링크의 URL을 일치시키고 manifest를 읽고 나서 다음 페이지에 대한 모든 데이터, JavaScript 모듈 및 CSS 리소스까지 프리패치 할 수 있다.
이것이 네트워크가 느릴 때에도 Remix 앱이 빠르게 느껴지는 방식이다.
Remix는 클라이언트 사이드 API를 제공하므로 HTML 및 브라우저의 기본 모델을 변경하지 않고도 풍부한 사용자 경험을 만들 수 있다.
이전의 route 모듈을 보면 브라우저에서 JavaScript로만 수행할 수 있는 양식에 대한 적지만 유용한 UX 개선 사항이 있다.
- 양식을 제출할 때 버튼을 비활성화한다.
- 서버 사이드 form 유효성 검사가 실패할 경우 input에 집중한다.
- 오류 메세지에 애니메이션을 준다.
export default function Projects() {
const projects = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const { state } = useNavigation(); // navigation의 state를 가져옴
const busy = state === "submitting";
const inputRef = React.useRef();
React.useEffect(() => {
if (actionData.errors) {
inputRef.current.focus();
}
}, [actionData]);
return (
<div>
{projects.map((project) => (
<Link key={project.slug} to={project.slug}>
{project.title}
</Link>
))}
<Form method="post">
<input ref={inputRef} name="title" />
<button type="submit" disabled={busy}>
{busy ? "Creating..." : "Create New Project"}
</button>
</Form>
{actionData?.errors ? (
<FadeIn>
<ErrorMessages errors={actionData.errors} />
</FadeIn>
) : null}
<Outlet />
</div>
);
}
이 코드에서 가장 흥미로운 점은 "only additive" 라는 것이다.
전체 상호 작용은 기본적으로 동일하며 JavaScript가 로드되기 전의 기본 수준에서도 작동한다.
유일한 차이점은 피드백이 앱(useNavigation().state) 대신 브라우저(회전하는 파비콘 등)에서 제공된다는 것이다.
Remix는 백엔드의 controller 수준에 도달하기 때문에 이 작업을 원활하게 수행할 수 있다.
애니메이션 유효성 검사 메세지, 포커스 관리 및 보류 중인 UI가 포함된 환경으로 넘어가려면 즉시 코드를 근본적으로 변경해야 하지만
Remix를 사용하면 기본적으로 작동하는 방식을 변경하지 않고 기존 "server-side view" 주위에 일부 코드를 추가하면 된다.
브라우저 런타임은 서버 통신을 대신하여 기본 브라우저 동작을 뛰어넘는 향상된 사용자 경험을 제공한다.
이것을 Remix의 Progressive Enhancement라고 부른다.
2. Route 구성
Remix 라우팅 시스템의 기본 개념 중 하나는 중첩된 경로를 사용하는 것이다.
이 접근 방식은 root를 Ember.js로 추적하는 접근 방식으로 중첩된 경로를 사용하면 URL의 세그먼트가 데이터 종속성과 UI의 컴포넌트에 결합된다. /sales/invoices/102000와 같은 URL은 애플리케이션의 명확한 경로를 보여줄 뿐만 아니라 다양한 구성 요소에 대한 관계와 종속성을 설명한다.
- 모듈형 디자인
중첩된 라우트는 URL을 여러 부분으로 분할하여 명확성을 제공한다.
각 세그먼트는 특정 데이터 요구 사항 및 컴포넌트와 직접적으로 연관되어 있다. /sales/invices/102000 URL에서 각 세그먼트는 특정 데이터 포인트 및 UI 섹션과 연결될 수 있으므로 코드 베이스에서 관리하기가 직관적이다.
중첩 라우팅의 특징은 중첩 라우트 트리의 여러 경로가 단인 URL과 일치하는 기능이다. 이러한 세분성을 통해 각 라우트는 주로 특정 URL 세그먼트 및 관련 UI 조각에 집중된다.
이 접근 방식은 모듈성 및 관계 분리 원칙을 지키며 각 라우트가 핵심 책임에 계속 집중할 수 있도록 한다.
- 병렬 로딩 (Parallel Loading)
일부 웹 애플리케이션에서는 데이터와 assets를 순차적으로 로드하면 사용자 환경이 인위적으로 느려지는 경우가 있다.
데이터 종속성이 상호 의존적이지 않은 경우에도 렌더링 계층 구조와 결합되어 바람직하지 않은 요청 체인이 생성되므로 순차적으로 로드될 수 있다.
Remix는 중첩된 라우팅 시스템을 활용하여 로드 시간을 최적화한다.
URL이 여러 경로와 일치하면 Remix는 일치하는 모든 경로에 필요한 데이터와 assets를 병렬로 로드한다.
이를 통해 Remix는 연결된 요청 시퀀스의 기존 함정을 효과적으로 회피한다.
동시에 발생하는 여러 요청을 효율적으로 처리하는 최신 브라우저의 기능과 결합된 이 전략은 Remix를 응답성이 뛰어나고 신속한 웹 애플리케이션을 제공하는 선두 주자로 자리매김하게 한다. 데이터를 빠르게 가져오는 것만이 아닌, 최종 사용자에게 가능한 최상의 경험을 제공하기 위해 체계적인 방식으로 정보를 가져온다.
- 기존 Route 구성
Remix는 라우팅 프로세스를 간소화하는 데 도움이 되는 주요 규칙인 app/routes 폴더를 도입한다.
개발자가 이 폴더 내의 파일을 소개하면 Remix는 본질적으로 이를 route로 이해한다.
이 규칙은 route를 정의하고, route를 URL과 연결하고, 연결된 컴포넌트를 렌더링하는 프로세스를 단순화한다.
⇩ route 폴더 규칙을 사용한 디렉토리 예시
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.tsx
app/routes/concerts로 시작하는 모든 route는 app/routes/concerts.tsx의 하위 route가 된다.
URL | 일치하는 route | Layout |
/ | 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/sait-lake-city | app/routes/concerts.$city.tsx | app/routes/concerts.tsx |
- 기존 Route 폴더
추가 모듈이나 assets가 필요한 route의 경우 route.tsx파일이 존재하는 app/routes폴더를 사용할 수 있다.
- 모듈 공동 배치
- 특정 route에 연결된 모든 요소를 수집하여 로직, 스타일 및 컴포넌트가 긴밀하게 결합되도록 함
- 가져오기(Import) 단순화
- 관련 모듈을 한 곳에 모아 가져오기 관리가 간편해지며 코드 유지 관리성이 향상됨
- 자동 코드 구성 촉진
- route.tsx 설정을 사용하면 본질적으로 잘 구성된 코드 베이스가 촉진되어 애플리케이션 확장에 유리함
대신 위와 동일한 route를 아래와 같이 구성할 수 있다.
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 바로 아래 폴더만 route로 등록된다.
- _index
- about
- concerts
- concerts._$city
- concerts._index
- concerts.trending
app/
├── routes/
│ └── about/
│ ├── header/
│ │ └── route.tsx
│ └── route.tsx
└── root.tsx
여기서 app/routes/about/header/route.tsx는 경로를 생성하지 않음!!
- 수동 Route 구성
app/routes 폴더는 개발자에게 편리한 규칙을 제공하지만 Remix는 한 가지 크기가 모든 것에 적합하지 않다는 점을 높이 평가한다.
제공된 규칙이 특정 프로젝트 요구 사항이나 개발자의 기본 설정돠 일치하지 않는 경우가 있다.
Remix는 remix.config.js를 통해 수동 route 구성을 허용한다.
이러한 유연성을 통해 개발자는 자신의 프로젝트에 적합한 방식으로 애플리케이션을 구성할 수 있다.
앱을 구성하는 일반적인 방법은 최상위 기능 폴더를 사용하는 것이다.
concerts와 같은 특정 주제와 관련된 route로는 여러 모듈을 공유할 가능성이 높다는 점을 고려하면 단일 폴더 아래에 정리하는 것이 합리적이다!
app/
├── about/
│ └── route.tsx
├── concerts/
│ ├── card.tsx
│ ├── city.tsx
│ ├── favorites-cookie.ts
│ ├── home.tsx
│ ├── layout.tsx
│ ├── sponsored.tsx
│ └── trending.tsx
├── home/
│ ├── header.tsx
│ └── route.tsx
└── root.tsx
이 구조를 이전 예제와 동일한 URL로 구성하려면 remix.config.js의 routes 기능을 사용할 수 있다.
/** @type {import('@remix-run/dev').AppConfig} */
export default {
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의 route 구성 접근 방식은 관례와 유연성을 혼합한다.
app/routes 폴더를 사용해 route를 쉽고 체계적으로 설정할 수 있고,
더 많은 제어를 원하거나 파일 이름이 마음에 들지 않거나 고유한 요구 사항이 있다면 remix.config.js가 있다!
많은 앱이 remix.config.js를 선호해 route 폴더 규칙을 포기할 것이라는 걸 예상한다.
3. Route 모듈
- action
route action은 데이터 뮤테이션과 다른 액션들을 처리하는 서버 전용 함수이다.
만약 라우트에 GET요청이 없는 경우 (DELETE, PATCH, POST, 또는 PUT) 해당 action은 loader 전에 호출된다.
action은 loader와 동일한 API를 가지지만, 호출될 때 유일한 차이점이 있다. 이를 통해 단일 라우트 모듈에서 데이터 세트에 대한 모든 것(읽은 데이터, 데이터를 렌더링하는 컴포넌트 및 데이터 쓰기)을 같은 위치에 배치할 수 있다.
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import { Form } from "@remix-run/react";
import { TodoList } from "~/components/TodoList";
import { fakeCreateTodo, fakeGetTodos } from "~/utils/db";
export async function action({
request,
}: ActionFunctionArgs) {
const body = await request.formData();
const todo = await fakeCreateTodo({
title: body.get("title"),
});
return redirect(`/todos/${todo.id}`);
}
export async function loader() {
return json(await fakeGetTodos());
}
export default function Todos() {
const data = useLoaderData<typeof loader>();
return (
<div>
<TodoList todos={data} />
<Form method="post">
<input type="text" name="title" />
<button type="submit">Create Todo</button>
</Form>
</div>
);
}
URL에 대한 POST작업이 수행되면, 라우트 계층에 있는 여러 라우트가 URL과 매치할 것이다.
UI를 빌드하기 위해 모두 호출되는 GET loader와는 달리, 오직 하나의 action만이 호출된다.
가장 깊이 일치하는 라우트가 "인덱스 라우트"가 아닌 한 호출된 라우트가 가장 깊이 일치하는 라우트가 될 것이다.
이 경우 동일한 URL을 공유하면 상위 라우트가 우선되기 때문에 인덱스의 상위 라우트에 post된다.
인덱스 라우트를 사용해 post하길 원한다면 action에서 ?index를 사용하라.
<Form action="/accounts?index" method="post"/>
action URL | route action |
/accounts?index | app/routes/accounts._index.tsx |
/accounts | app/routes/accounts.tsx |
또한 action prop(<Form method="post" >)이 없는 폼은 렌더링되는 동일한 라우트에 자동으로 게시되므로, 상위 라우트와 인덱스 라우트를 구분하기 위해 ?index 매개변수를 사용하는 것은 오직 인덱스 라우트가 아닌 다른 곳에서 인덱스 라우트에 게시하는 경우에만 유용하다.
인덱스 라우트에서 자체 경로로 게시하거나 상위 라우트에서 자체 경로로 게시하는 경우, <Form action >을 전혀 정의할 필요가 없다.
그냥 생략해라!
<Form method="post">
- loader
각 라우트는 렌더링시에 라우트에 데이터를 제공하는 loader 함수를 정의할 수 있다.
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async () => {
return json({ ok: true });
};
이 함수는 서버에서만 실행된다. 초기 서버 렌더링 시 HTML 문서에 데이터를 제공한다.
브라우저 탐색 시 Remix는 브라우저로부터 fetch로 함수를 호출한다.
즉, 데이터베이스와 직접 통신할 수 있고 서버 전용 API 비밀(?) 등을 사용할 수 있다.
UI를 렌더링하는 데 사용하지 않는 코드는 브라우저 번들로부터 제거된다.
⇩ ORM Prisma 데이터베이스를 사용한 예시
import { useLoaderData } from "@remix-run/react";
import { prisma } from "../db";
export async function loader() {
return json(await prisma.user.findMany());
}
export default function Users() {
const data = useLoaderData<typeof loader>();
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
prisma는 오직 loader에서만 사용되기 때문에 컴파일러에 의해 브라우저 번들로부터 제거된다.
'loader'로부터 반환되는 모든 것은 컴포넌트가 렌더링하지 않더라도 클라이언트에 노출된다.
공공 API 엔드포인트처럼 'loader'를 주의 깊게 다루세요!
▪︎ 타입 안전
useLoaderData<typeof loader>를 사용하면 loader 및 컴포넌트에 대해 네트워크를 통해 타입 안전성을 얻을 수 있다.
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader() {
return json({ name: "Ryan", date: new Date() });
}
export default function SomeRoute() {
const data = useLoaderData<typeof loader>();
}
- data.name -> 문자열이라는 것을 알게될 것이다.
- data.date -> json에 날짜 객체로 전달되었을지라도 문자열이라는 것을 알게될 것이다.
- 클라이언트 전환을 위해 데이터가 fetch되었을때, 값은 JSON.stringify로 네트워크에서 직렬화되고 타입은 이를 인식한다.
▪︎ params
라우트 매개변수는 라우트 파일 이름으로 정의된다.
세그먼트가 $invoiceId 와 같이 $로 시작하는 경우, 해당 세그먼트의 URL로 부터 가져온 값이 loader에 전달될 것이다.
// app/routes/invoices.$invoiceId.tsx
// 사용자가 /invoices/123에 방문한 경우
export async function loader({
params,
}: LoaderFunctionArgs) {
params.invoiceId; // "123"
}
매개변수는 주로 ID별로 레코드를 찾는 데 유용하다.
// /invoices/123에 방문한 경우
export async function loader({
params,
}: LoaderFunctionArgs) {
const invoice = await fakeDb.getInvoice(params.invoiceId);
if (!invoice) throw new Response("", { status: 404 });
return json(invoice);
}
▪︎ request
Fetch Request 인스턴스
loader에서 가장 일반적인 사용 케이스는 요청에서 쿠키와 같은 헤더와 URL의 URLSearchParams를 읽는 것이다.
export async function loader({
request,
}: LoaderFunctionArgs) {
// 쿠키 읽기
const cookie = request.headers.get("Cookie");
// `?q=`에 대한 파라미터 분석
const url = new URL(request.url);
const query = url.searchParams.get("q");
}
▪︎ context
서버 어댑터의 getLoadContext( )함수에 전달된 컨텍스트
어댑터의 요청/응답 API와 Remix 앱 간의 격차를 해소하는 방법이다.
이 API는 탈출구이므로 필요한 경우는 드물다!
⇩ Express 어댑터를 사용한 예시
// server.tsx
const {
createRequestHandler,
} = require("@remix-run/express");
app.all(
"*",
createRequestHandler({
getLoadContext(req, res) {
// loader context가 됨
return { expressUser: req.user };
},
})
);
이제 loader는 context에 접근할 수 있다.
// app/routes/some-route.tsx
export async function loader({
context,
}: LoaderFunctionArgs) {
const { expressUser } = context;
// ...
}
▪︎ response instance 반환
loader에서 Fetch Response를 반환해야 한다.
export async function loader() {
const users = await db.users.findMany();
const body = JSON.stringify(users);
return new Response(body, {
headers: {
"Content-Type": "application/json",
},
});
}
json 헬퍼를 사용하면 이 작업이 단순화되므로 직접 구성할 필요가 없다.
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async () => {
const users = await fakeDb.users.findMany();
return json(users);
};
훨씬 더 깨끗하게 loader를 만들기 위해 json이 약간의 작업을 어떻게 수행하는지 알 수 있다.
또한 json 헬퍼를 사용해 헤더나 응답에 상태 코드를 추가할 수 있다.
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
const project = await fakeDb.project.findOne({
where: { id: params.id },
});
if (!project) {
return json("Project not found", { status: 404 });
}
return json(project);
};
▪︎ Loader에 response 던지기
응답을 반환하는 것 이외에도 loader에서 response 객체를 던질 수도 있다.
이를 통해 호출 스택을 중단하고 다음 두 가지 중 하나를 수행할 수 있다.
- 다른 URL로 리다이렉션
- ErrorBoundary를 통해 상황별 데이터가 포함된 대체 UI를 표시
+ Route File Conventions (Route File Naming)
route 플러그인 옵션을 통해 route를 구성할 수 있지만 대부분의 route는 이 파일 시스템 규칙을 사용하여 생성된다.
.js, .jsx 또는 .ts, .tsx 파일 확장자를 사용할 수 있다.
- Root Route
app/
├── routes/
└── root.tsx
app/root.tsx에 있는 파일은 root 레이아웃 또는 root route이다.
다른 모든 경로와 동일하게 작동하므로, loader, action 등을 내보낼 수 있다.
root route는 일반적으로 다음과 같다.
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>
);
}
root route는 전체 앱의 루트 레이아웃 역할을 하며, 다른 모든 route는 <Outlet /> 안에서 렌더될 것이다.
- Basic Routes
app/routes 디렉토리의 모든 JavaScript 또는 TypeScript 파일은 애플리케이션의 route가 된다.
파일 이름은 root route의 _index.tsx 인덱스 route를 제외하고 route의 URL 경로 이름에 맵핑된다.
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx
이러한 route는 중첩된 라우팅으로 인해 app/root.tsx 의 outlet에서 렌더가 될 것이다.
URL | 일치하는 경로 |
/ | app/routes/_index.tsx |
/about | app/routes/about.tsx |
- 점 구분 기호(Dot Delimiters)
route 파일 이름에 .을 추가하면 URL에 /가 추가된다.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.salt-lake-city.tsx
│ └── concerts.san-diego.tsx
└── root.tsx
URL | 일치하는 경로 |
/concerts/trending | app/routes/concerts.trending.tsx |
concerts/salt-lake-city | app/routes/concerts.salt-lake-city.tsx |
/concerts/san-diego | app/routes/concert/san-diego.tsx |
점 구분 기호는 중첩을 생성하기도 한다! <- 아래에서 나올 예정
- 동적 세그먼트
일반적으로 URL은 정적이 아니라 데이터 기반이다.
동적 세그먼트를 사용하면 URL 세그먼트를 일치시키고 코드에서 해당 값을 사용할 수 있다.
$ 접두사를 사용해 동적 세그먼트를 생성한다.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ └── concerts.trending.tsx
└── root.tsx
URL | 일치하는 경로 |
/concerts/trending | app/routes/concerts.trending.tsx |
/concerts/salt-lake-city | app/routes/concerts.$city.tsx |
/concerts/san-diego | app/routes/concerts.$city.tsx |
Remix는 URL의 값을 구문 분석하여 다양한 API에 전달한다.
이러한 값을 "URL 매개변수"라고 부르며 URL 매개변수에 접근하는 가장 유용한 장소는 loader와 action이다.
export async function loader({
params,
}: LoaderFunctionArgs) {
return fakeDb.getAllConcertsForCity(params.city);
}
params 객체의 속성 이름이 $city.tsx 파일 이름에 직접 맵핑되어 params.city가 된다.
Route는 concerts.$city.$date와 같은 여러 동적 세그먼트를 가질 수 있고 둘 다 params 객체에서 이름으로 접근된다.
export async function loader({
params,
}: LoaderFunctionArgs) {
return fake.db.getConcerts({
date: params.date,
city: params.city,
});
}
- 중첩된 Routes
중첩 라우팅은 URL 세그먼트를 컴포넌트 계층 구조 및 데이터에 연결하는 일반적인 아이디어이다.
점 구분 기호를 사용해 중첩된 route를 만들고 .(점)앞의 파일 이름이 다른 route 파일 이름과 일치하면 자동으로 일치하는 상위 경로에 대한 하위 경로가 된다.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx // 중첩
│ ├── concerts.$city.tsx // 중첩
│ ├── concerts.trending.tsx // 중첩
│ └── concerts.tsx
app/routes/concerts.로 시작하는 모든 경로는 app/routes/concerts.tsx 의 하위 경로가 되며 상위 경로의 outlet 컴포넌트 안에서 렌더링된다.
일반적으로 사용자가 상위 URL을 직접 방문할 때 상위 outlet 내부에서 무언가가 렌더링 되도록 중첩 라우트를 추가할 때 인덱스 라우트를 추가하기를 원한다. 예를 들어 URL이 /concerts/salt-lake-city인 경우 UI 계층 구조는 아래와 같다.
<Root>
<Concerts>
<City />
</Concerts>
</Root>
- 레이아웃 중첩 없이 중첩된 URL
때로는 URL이 중첩되기를 원하지만 자동 레이아웃 중첩을 원하지 않는 경우가 있다.
상위 세그먼트에 밑줄이 붙은 중첩을 선택 해제할 수 있다.
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.tsx
│ └── concerts_.mine.tsx // 레이아웃은 중첩 X
└── root.tsx
URL | 일치하는 경로 | 레이아웃 |
/ | app/routes/_index.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 |
/concerts/mine은 더 이상 app/routes/concerts.tsx와 레이아웃이 중첩되지는 않지만 trailing_밑줄은 경로 세그먼트를 생성한다.
- 중첩된 URL이 없는 중첩된 레이아웃
이것은 경로가 없는 라우트라고 불린다.
URL에 경로 세그먼트를 추가하지 않고 라우트 그룹과 레이아웃을 공유하려는 경우가 있다. 일반적으로 공개 페이지나 로그인된 앱 환경과 다른 머리글/바닥글을 가진 인증 경로 집합이 예이다.
_leading밑줄을 사용해 이를 수행할 수 있다.
-> _leading 밑줄을 파일 이름 위에 덮어 URL에서 파일 이름을 숨긴다!
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/salt-lake-city | app/routes/concerts.$city.tsx | app/routes/concerts.tsx |
- 선택적 세그먼트
경로 세그먼트를 괄호로 묶으면 세그먼트가 선택 사항이 된다.
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 |
/american-flag-speedo가 app/routes/($lang).$productId.tsx가 아닌 app/routes/($lang)._index.tsx 라우트에 매칭되는걸까?
이는 선택적인 동적 매개변수 세그먼트 뒤에 또 다른 동적 매개변수가 있는 경우 Remix가 /american-flag-speedo와 같은 단일 세그먼트 URL이 /:lang/:productId와 매치될 것이지의 여부를 안정적으로 결정할 수 없기 때문이다.
선택적 세그먼트는 열심히 매치하기 때문에 /:lang에 매칭될 것이다. 만약 이러한 유형의 설정이 있는 경우 ($lang)._index.tsx로더에 있는 params.lang을 살펴보고 params.lang이 유효한 언어 코드가 아닐 경우 현재/기본 언어인 /:lang/american-flag-speedo로 리다이렉트하는 것을 권유한다.
- Splat Routes
동적 세그먼트는 단일 경로 세그먼트(URL에서 /사이의 두 경로)와 일치하는 반면, splat route는 슬래시를 포함하여 URL의 나머지 부분과 일치한다.
app/
├── routes/
│ ├── _index.tsx
│ ├── $.tsx
│ ├── about.tsx
│ └── files.$.tsx
└── root.tsx
URL | 일치하는 경로 |
/ | app/routes/_index.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.pdf | /app/routes/files.$.tsx |
동적 경로 매개변수와 유사하게, "*"키를 사용하여 splat route의 params와 일치하는 경로 값에 접근할 수 있다.
// app/routes/files.$.tsx
export async function loader({
params,
}: LoaderFunctionArgs) {
const filePath = params["*"];
return fake.getFileInfo(filePath);
}
- 특수 문자 이스케이프
이러한 라우트 규칙에 사용하는 Remix 특수 문자 중 하나가 실제로 URL의 일부가 되도록 하려면, []문자를 가지고 규칙을 탈출 할 수 있다.
파일 이름 | URL |
app/routes/sitemap[.]xml.tsx | /sitemap.xml |
app/routes/[sitemap.xml].tsx | /sitemap.xml |
app/routes/weird-url.[_index].tsx | /weired-url/_index |
app/routes/dolla-bills-[$].tsx | /dolla-bills-$ |
app/routes/[[so0weird]].tsx | /[so-weird] |
- 조직용 폴더
Route는 내부에서 라우트 모듈을 정의하는 route.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 모듈을 포함하는 폴더일 수 있다.
app/
├── routes/
│ ├── _landing._index/
│ │ ├── route.tsx
│ │ └── scroll-experience.tsx
│ ├── _landing.about/
│ │ ├── employee-profile-card.tsx
│ │ ├── get-employee-data.server.ts
│ │ ├── route.tsx
│ │ └── team-photo.jpg
│ ├── _landing/
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── app._index/
│ │ ├── route.tsx
│ │ └── stats.tsx
│ ├── app.projects/
│ │ ├── get-projects.server.ts
│ │ ├── project-buttons.tsx
│ │ ├── project-card.tsx
│ │ └── route.tsx
│ ├── app/
│ │ ├── footer.tsx
│ │ ├── primary-nav.tsx
│ │ └── route.tsx
│ ├── app_.projects.$id.roadmap/
│ │ ├── chart.tsx
│ │ ├── route.tsx
│ │ └── update-timeline.server.ts
│ └── contact-us.tsx
└── root.tsx
라우트 모듈을 폴더로 바꾸면 라우트 모듈은 folder/route.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
- 스케일링
확장에 대한 일반적인 권장 사항은 모든 라우트를 폴더로 만들고 해당 라우트에서 독점적으로 사용되는 모듈을 폴더에 넣은 다음 공유 모듈을 라우트 폴더 외부의 다른 곳에 두는 것이다.
여기에는 몇 가지 이점이 있다.
- 공유 모듈을 쉽게 식별할 수 있으므로 변경할 때 가볍게 밟으면 된다.
- '파일 구성 약화'를 생성하거나 앱의 다른 부분을 어지럽히지 않고 특정 경로에 대한 모듈을 쉽게 구성하고 리팩토링할 수 있다.
'코딩 > 코딩노트' 카테고리의 다른 글
[Remix]Remix 공식문서 파헤치기 3탄 - Form vs fetcher (0) | 2024.04.19 |
---|---|
[Remix]Remix 공식문서 파헤치기 2탄 - 데이터의 흐름과 상태 관리 (0) | 2024.04.15 |
[Web Socket] 웹 소켓으로 채팅 구현하기 (+ MongoDB 사용하기) (0) | 2024.04.04 |
[React] useContext로 데이터에 쉽게 접근하기 (0) | 2024.03.28 |
[React] React Hooks를 알아보자 (부제: 프로젝트 되돌아보기) (0) | 2024.03.24 |