티스토리 뷰
💛 Form vs fetcher
Remix에서 개발하면 때로는 기능성에서 중복되어 신규 사용자에게 모호함을 줄 수 있는 풍부한 도구 세트가 제공된다.
Remix의 효과적인 개발의 핵심은 각 도구의 뉘앙스와 적절한 사용 사례를 이해하는 것이다.
- 중점을 두고 있는 API
▪︎ <Form>
fetch를 통해 action에 데이터를 제출하고, 기본 HTML 형식 이상의 향상된 유저 인터페이스를 활성화하는 useNavigation에서 보류 상태를 활성화하는 점직적으로 향상된 HTML이다.
form의 action이 완료된후, 페이지의 모든 데이터는 데이터와의 동기화에서 UI를 유지하기 위해 서버에서 자동으로 재검증된다.
API로부터 HTML을 사용하기 때문에, 서버는 javaScript가 로드되기 전에 기본 수준에서 대화식인 페이지들을 렌더링한다.
Remix가 제출한 것을 관리하는 대신, 브라우저가 (돌아가는 파비콘같은)보류 상태 뿐만 아니라 제출한 것까지 관리한다. javaScript 로드 이후에, Remix가 웹 애플리케이션 사용자 경험을 활성화한다.
Form은 URL도 변경해야 하거나 브라우저 기록 스택에 항목을 추가해야하는 제출에 가장 유용하다.
브라우저 기록 스택을 조작해서는 안되는 폼의 경우 <fetcher.Form>을 사용한다.
props
- action: 폼 데이터를 제출할 URL. undefined인 경우 컨텍스트에서 가장 가까운 경로가 기본값이 됨
- method: 사용할 HTTP 동사를 정함. 기본값은 GET
- encType: 폼 제출에 사용할 인코딩 유형. 기본값은 application/x-www-form-urlencoded, 파일 업로드시에는 multipart/form-data
- navigate: false를 명시해 폼이 네비게이션을 넘기고 내부적으로 fetcher를 사용하도록 지시할 수 있음
- fetcherKey: 탐색하지 않는 Form을 사용할 때, 선택적으로 사용할 자신의 fetcher key를 지정할 수 있음
- preventScrollReset: <ScrollRestoration>을 사용하는 경우, 폼이 제출될때 스크롤 위치가 창 상단으로 재설정되는 것을 방지
- replace: 새 항목을 추가하는 대신 기록 스택의 현재 항목을 변경함
- reloadDocument: true인 경우 클라이언트 측 라우팅 대신 브라우저를 사용해 폼을 제출
- unstable_viewTransition: document.startViewTransition()에서 최종 상태 업데이트를 감싸 탐색에 대한 보기 전환을 활성화함
▪︎ useActionData
가장 최근의 route action에서 직렬화된 데이터를 반환하거나 없는 경우에는 undefined를 반환한다.
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { Form, useActionData } from "@remix-run/react";
export async function action({
request,
}: ActionFunctionArgs) {
const body = await request.formData();
const name = body.get("visitorsName");
return json({ message: `Hello, ${name}` });
}
export default function Invoices() {
const data = useActionData<typeof action>();
return (
<Form method="post">
<input type="text" name="visitorsName" />
{data ? data.message : "Waiting..."}
</Form>
);
}
▪︎ useFetcher
탐색 외부에서 서버와 상호작용하기 위한 훅
Options
- key: 기본적으로 해당 컴포넌트로 범위가 지정된 고유한 fetcher를 발생시킴. 앱의 다른 곳에서 액세스할 수 있도록 자체 키로 fetcher를 식별하고 싶다면 key 옵션을 사용
function AddToBagButton() {
const fetcher = useFetcher({ key: "add-to-bag" });
return <fetcher.Form method="post">...</fetcher.Form>;
}
// Then, up in the header...
function CartCount({ count }) {
const fetcher = useFetcher({ key: "add-to-bag" });
const inFlightCount = Number(
fetcher.formData?.get("quantity") || 0
);
const optimisticCount = count + inFlightCount;
return (
<>
<BagIcon />
<span>{optimisticCount}</span>
</>
);
}
Components
- fetcher.Form: 탐색을 유발하지 않는다는 점을 제외하면 <Form>과 같음
function SomeComponent() {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/some/route">
<input type="text" />
</fetcher.Form>
);
}
Methods
- fetcher.submit(formData, options): 폼 데이터를 라우트에 제출함. 여러개의 중첩 라우트가 URL과 일치할 수 있지만 리프 라우트만 호출됨
- fetcher.load(href, options): 라우트 로더에서 데이터를 로드함. 여러개의 중첩 라우트가 URL과 일치할 수 있지만 리프 라우트만 호출됨
- options.unstable_flushSync: React Router DOM에 기본 React.startTransition 대신 ReactDOM.flushSync 호출에서 fetcher.load에 대한 상태 업데이트를 감싸도록 지시함. 이를 통해 업데이트가 DOM에 전달된 직후 동기식 DOM action을 수행할 수 있음
Properties
- fetcher.state: fetcher의 상태를 알 수 있음
- idle - 아무것도 가져오지 않음
- submitting - 폼이 제출됨. 메소드가 GET이면 loader 호출, DELETE, PATCH, POST, PUT이면 action 호출
- loading - action 제출 후 라우트의 로더가 다시 로드됨
- fetcher.data: action 또는 loader에서 반환된 응답 데이터가 저장됨
- fetcher.formData:서버에 제출된 FormData 인스턴스가 저장됨
- fetcher.formAction: 제출물의 URL
- fetcher.formMethod: 제출 폼 메소드
▪︎ useNavigation
보류 중인 페이지 탐색에 대한 정보를 제공한다.
Properties
- navigation.formAction: 제출된 폼의 action(있는 경우)
- navigation.formMethod: 제출된 폼의 메소드(있는 경우)
- navigation.formData: <Form> 또는 useSubmit에서 시작된 모든 DELETE, PATCH, POST, PUT 탐색은 폼의 제출 데이터가 첨부됨. submission.formData의 FormData 객체를 사용해 "낙관적 UI"를 빌드하는데 주로 유용함
- navigation.location: 다음 위치가 무엇인지 알려줌
- navigation.state
- idle - 보류 중인 탐색 없음
- submitting - POST, PUT, PATCH, DELETE를 사용한 폼 제출로 인해 route action이 호출되고 있음
- loading - 다음 페이지를 렌더링하기 위해 다음 경로에 대한 loader가 호출됨
URL 고려사항
이러한 도구 중에서 선택을 할 때 주요 기준은 URL을 변경할지 여부이다.
- URL 변경이 필요함: 페이지 간 탐색하거나 전환할 때, 또는 레코드 생성 또는 삭제와 같은 특정 action 이후. 사용자의 브라우저 기록이 애플리케이션을 통한 여정을 정확하게 반영할 수 있음
- URL 변경이 필요하지 않음: 현재 뷰의 컨텍스트나 기본 콘텐츠를 크게 변경하지 않는 action. 개별 필드 업데이트나 새 URL 또는 페이지 새로고침을 보장하지 않는 사소한 데이토 조작이 포함될 수 있음. 팝오버, 콤보 박스 등 fetcher를 사용해 데이터를 로드하는 경우에도 적용됨
- API 비교
두 API 세트에는 많은 유사점이 있다.
Navigation/URL API | Fetcher API |
<Form> | <Fetcher.Form> |
useActionData( ) | fetcher.data |
navigation.state | fetcher.state |
navigation.formAction | fetcher.formAction |
navigation.formData | fetcher.formData |
- 예시
1. 새 레코드 만들기
// app/routes/recipes/new.tsx
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
// 직관적인 레코드 생성 프로세스를 촉진하기 위해 <Form>, useActionData, useNavigation 사용
import {
Form,
useActionData,
useNavigation,
} from "@remix-run/react";
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const errors = await validateRecipeFormData(formData);
if (errors) {
return json({ errors });
}
const recipe = await db.recipes.create(formData);
return redirect(`/recipes/${recipe.id}`);
}
export function NewRecipe() {
// useActionData를 호출해 제출 문제에 대한 즉각적인 피드백 제공
const { errors } = useActionData<typeof action>();
// useNavigation으로 폼 제출 상태를 동적으로 반영
const navigation = useNavigation();
const isSubmitting =
navigation.formAction === "/recipes/new";
return (
<Form method="post">
<label>
Title: <input name="title" />
{errors?.title ? <span>{errors.title}</span> : null}
</label>
<label>
Ingredients: <textarea name="ingredients" />
{errors?.ingredients ? (
<span>{errors.ingredients}</span>
) : null}
</label>
<label>
Directions: <textarea name="directions" />
{errors?.directions ? (
<span>{errors.directions}</span>
) : null}
</label>
<button type="submit">
{isSubmitting ? "Saving..." : "Create Recipe"}
</button>
</Form>
);
}
<Form>을 사용해 직접적이고 논리적인 탐색을 보장한다. 기록을 생성한 후 사용자는 자연스럽게 새로운 레시피의 고유 URL로 안내되어 action의 결과를 강화한다.
useActionData는 서버와 클라이언트를 연결하여 제출 문제에 대한 즉각적인 피드백을 제공한다. 이 빠른 응답을 통해 사용자는 방해 없이 오류를 수정할 수 있다.
마지막으로 useNavigation은 폼 제출 상태를 동적으로 반영한다. 버튼 라벨 전환과 같은 미묘한 UI 변경을 통해 사용자는 자신의 작업이 처리되고 있음을 확인할 수 있다.
이러한 API를 결합하면 구조화된 탐색과 피드백이 균형 있게 혼합되어 있다.
2. 기록 업데이트
각 항목에 삭제 버튼이 있는 레시피 목록을 보고 있다는 가정 하에, 사용자가 삭제 버튼을 클릭하면 데이터베이스에서 레시피를 삭제하고 목록에서 벗어나지 않고 제거하려고 한다.
먼저 페이지에서 레시피 목록을 얻기위해 기본 경로 설정을 고려한다.
// app/routes/recipes/_index.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
return json({
recipes: await db.recipes.findAll({ limit: 30 }),
});
}
export default function Recipes() {
const { recipes } = useLoaderData<typeof loader>();
return (
<ul>
{recipes.map((recipe) => (
<RecipeListItem key={recipe.id} recipe={recipe} />
))}
</ul>
);
}
레시피를 삭제하는 action과 각 레시피를 렌더링하는 컴포넌트를 추가한다.
// app/routes/recipes/_index.tsx
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id");
await db.recipes.delete(id);
return json({ ok: true });
}
const RecipeListItem: FunctionComponent<{
recipe: Recipe;
}> = ({ recipe }) => {
// useFetcher로 내부 업데이트하기
const fetcher = useFetcher();
// 삭제를 호출하면 상태 전환 관리하기
const isDeleting = fetcher.state !== "idle";
return (
<li>
<h2>{recipe.title}</h2>
<fetcher.Form method="post">
<button disabled={isDeleting} type="submit">
{isDeleting ? "Deleting..." : "Delete"}
</button>
</fetcher.Form>
</li>
);
};
사용자가 레시피를 삭제하면 호출된 action과 fetcher도구가 해당 상태 전환을 관리한다.
여기서 가장 큰 장점은 컨텍스트를 유지한다는 것이다.
삭제가 완료되면 사용자는 목록에 유지된다.(페이지 이동을 하지 않는다는 의미?)
fetcher도구의 상태 관리 기능은 실시간 피드백을 제공하는 데 활용된다: "Deleting..."과 "Delete" 사이를 전환하여 진행 중인 프로세스를 명확하게 표시
또한 각 fetcher가 자체 상태를 관리할 수 있는 자율성을 가지므로 개별 목록 항목에 대한 작업은 독립적이 되어 한 항목에 대한 작업이 다른 항목에 영향을 주지 않도록 보장한다. (페이지 데이터 재검증은 Network Concurrency Management에서 다룬다)
본질적으로 useFetcher가 URL이나 탐색의 변경이 필요하지 않은 작업에 대한 원활한 매커니즘을 제공하여 실시간 피드백과 컨텍스트 보존을 제공함으로써 사용자 경험을 향상시킨다.
- 결론
Remix는 다양한 개발 요구 사항을 충족할 수 있는 다양한 도구를 제공한다.
일부 기능이 중복되는 것처럼 보일 수도 있지만 각 도구는 특정 상황을 염두에 두고 제작되었다. <Form>, useActionData, useFetcher, useNavigation의 복잡성과 이상적인 애플리케이션을 이해함으로써 개발자는 보다 직관적이고 반응성이 뛰어나며 사용자 친화적인 웹 애플리케이션을 만들 수 있다.
'코딩 > 코딩노트' 카테고리의 다른 글
[Remix]Remix 공식문서 파헤치기 5탄 - Hooks (0) | 2024.04.23 |
---|---|
[Remix]Remix 공식문서 파헤치기 4탄 - 파일 컨벤션 (0) | 2024.04.21 |
[Remix]Remix 공식문서 파헤치기 2탄 - 데이터의 흐름과 상태 관리 (0) | 2024.04.15 |
[Remix] Remix 공식 문서 파헤치기 1탄 - Remix가 뭔데?!(route 집중 파보기) (0) | 2024.04.11 |
[Web Socket] 웹 소켓으로 채팅 구현하기 (+ MongoDB 사용하기) (0) | 2024.04.04 |