React-Router-Dom 로더와 액션
1. Loader와 Action은 왜 나오게 되었나?
데이터 API의 변화
Loader와 Action이 나오기 전인 React Router DOM v6.4 이전에는 라우팅과 데이터 페칭이 완전히 분리되어 있었습니다. 라우터는 단순히 URL에 따라 어떤 컴포넌트를 렌더링할지만 결정했고, 데이터 로딩은 각 컴포넌트의 책임이었습니다.
function ProductPage() {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const { id } = useParams();
useEffect(() => {
async function loadProduct() {
setLoading(true);
try {
const response = await fetch(`/api/products/${id}`);
const data = await response.json();
setProduct(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
loadProduct();
}, [id]);
if (loading) return <p>로딩 중...</p>;
if (!product) return <p>상품을 찾을 수 없습니다.</p>;
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>가격: {product.price}원</p>
</div>
);
}
이 방식에는 몇 가지 문제점이 있었습니다:
- 데이터 로딩 시 항상 로딩 상태를 거쳐야 함
- 컴포넌트가 마운트된 후에만 데이터 로딩 시작
- 페이지 전환 시 사용자 경험 저하
- 코드 중복 및 보일러플레이트 증가
선언적 라우팅에서 데이터 중심 라우팅으로의 전환
앞선 문제들을 해결하기위해 React Router v6.4에서는 라우팅 구성에 데이터 로딩 로직을 포함시킬 수 있게 되었습니다. 이를 통해 컴포넌트가 렌더링되기 전에 필요한 데이터를 미리 로드할 수 있습니다.
그리고 또한 createBrowserRouter
는 데이터 로딩 기능을 포함한 라우터 설정을 가능하게 합니다. 기존에 주로 사용되던 <BrowserRouter>
와 달리 객체 구성 방식을 사용하여 라우트를 정의합니다.
import {
createBrowserRouter,
RouterProvider,
Route
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/products/:id",
element: <ProductPage />,
loader: async ({ params }) => {
const response = await fetch(`/api/products/${params.id}`);
if (!response.ok) {
throw new Response("상품을 찾을 수 없습니다", { status: 404 });
}
return response.json();
},
errorElement: <ErrorPage />
}
]);
function App() {
return <RouterProvider router={router} />;
}
createBrowserRouter
의 주요 이점:
- 데이터 로딩과 라우팅의 통합
- 에러 처리 메커니즘 개선
- 중첩 라우트에서의 데이터 로딩 간소화
- 타입스크립트 지원 강화
2. Loader란 무엇인가?
Loader는 React Router DOM v6.4에서 도입된 핵심 개념으로, 라우트 컴포넌트가 렌더링되기 전에 필요한 데이터를 비동기적으로 로드할 수 있게 해주는 함수입니다.
Loader의 기본 개념
Loader는 다음과 같은 특징을 가집니다:
- 라우트 정의 시 함께 선언
- 컴포넌트 렌더링 전에 실행
- 비동기 함수(async/await) 지원
- 다양한 데이터 타입 반환 가능 (객체, 배열, 문자열 등)
export async function loader() {
const response = await fetch("http://localhost:8080/posts");
if (!response.ok) {
throw new Response("글 목록을 불러오는데 실패했습니다", { status: 500 });
}
const resData = await response.json();
return resData.posts;
}
const router = createBrowserRouter([
{
path: "/",
element: <Posts />,
loader: postLoader,
errorElement: <ErrorPage />
}
]);
페이지 렌더링 전 데이터 로드
Loader를 사용하면 컴포넌트가 렌더링되기 전에 데이터를 로드할 수 있습니다. 이는 사용자 경험을 크게 향상시킵니다:
- 사용자가 경로 이동을 시작하면 즉시 데이터 로딩 시작
- 데이터 로딩이 완료될 때까지 이전 페이지 유지
- 데이터 로딩이 완료되면 새 페이지로 전환
이러한 방식은 빈 로딩 상태를 보여주는 대신 데이터가 준비된 상태로 페이지를 렌더링할 수 있게 해줍니다.
useLoaderData 훅 활용하기
useLoaderData
훅은 현재 라우트의 loader 함수에서 반환된 데이터에 접근할 수 있게 해줍니다:
import { useLoaderData, Link } from 'react-router-dom';
import Modal from '../components/Modal';
import classes from './PostDetails.module.css';
function PostDetails() {
const post = useLoaderData();
if (!post) {
return (
<Modal>
<main className={classes.details}>
<h1>Could not find post</h1>
<p>Unfortunately, the requested post could not be found.</p>
<p>
<Link to=".." className={classes.btn}>
Okay
</Link>
</p>
</main>
</Modal>
);
}
return (
<Modal>
<main className={classes.details}>
<p className={classes.author}>{post.author}</p>
<p className={classes.text}>{post.body}</p>
</main>
</Modal>
);
}
export default PostDetails;
로딩 상태 관리하기
React Router는 데이터 로딩 중에도 UI를 제어할 수 있는 방법을 제공합니다:
import { useNavigation } from "react-router-dom";
function Layout() {
const navigation = useNavigation();
return (
<div>
{navigation.state === "loading" && (
<div className="loading-indicator">로딩 중...</div>
)}
<Outlet />
</div>
);
}
useNavigation
훅은 현재 네비게이션 상태를 제공합니다:
idle
: 네비게이션이 진행 중이지 않음loading
: 데이터 로딩 중submitting
: 폼 제출 처리 중
3. Action의 이해
Action은 주로 폼 제출과 같은 데이터 변경 작업을 처리하는 데 사용됩니다.
Action의 기본 개념
Action은 다음과 같은 특징을 가집니다:
- 폼 제출 시 자동으로 호출
- POST, PUT, PATCH, DELETE 같은 데이터 변경 작업에 적합
- 비동기 함수(async/await) 지원
- 폼 데이터 접근 및 처리 가능
// Action 함수 정의
export async function action({ request }) {
const formData = await request.formData();
const postData = Object.fromEntries(formData);
await fetch("http://localhost:8080/posts", {
method: "POST",
body: JSON.stringify(postData),
headers: {
"Content-Type": "application/json",
},
});
return redirect("/");
}
// 라우트 설정에 Action 추가
const router = createBrowserRouter([
{
path: "/create-post",
element: <NewPost />,
action: newPostAction,
}
]);
폼 제출 처리하기
React Router는 <Form>
컴포넌트를 제공하여 폼 제출을 쉽게 처리할 수 있게 해줍니다:
import { Form, Link, redirect } from "react-router-dom";
function NewPost() {
return (
<Modal>
<Form className={classes.form} method="post">
<p>
<label htmlFor="body">Text</label>
<textarea id="body" required rows={3} name="body" />
</p>
<p>
<label htmlFor="name">Your name</label>
<input type="text" id="name" name="author" required />
</p>
<p className={classes.actions}>
<Link type="button" to={".."}>
Cancel
</Link>
<button>Submit</button>
</p>
</Form>
</Modal>
);
}
<Form>
컴포넌트의 이점:
- 자동으로 가장 가까운 라우트의 action 함수 호출
- JavaScript 없이도 작동 (점진적 향상)
- 네비게이션 상태 관리 통합
useActionData 훅 활용하기
useActionData
훅을 사용하면 action 함수에서 반환된 데이터에 접근할 수 있습니다. 이는 폼 유효성 검사 오류나 서버 응답을 처리하는 데 유용합니다:
import { Form, useActionData } from "react-router-dom";
function NewProductPage() {
const actionData = useActionData();
return (
<div>
<h1>새 상품 등록</h1>
{actionData?.errors && (
<ul className="error-list">
{Object.values(actionData.errors).map(error => (
<li key={error}>{error}</li>
))}
</ul>
)}
<Form method="post">
{/* 폼 필드 */}
<button type="submit">등록하기</button>
</Form>
</div>
);
}
// Action 함수
export async function action({ request }) {
const formData = await request.formData();
const productData = {
name: formData.get("name"),
price: Number(formData.get("price")),
description: formData.get("description")
};
const errors = validateProduct(productData);
if (Object.keys(errors).length > 0) {
return { errors };
}
// 유효성 검사 통과 시 데이터 저장
await saveProduct(productData);
return redirect("/products");
}
데이터 변경 후 리다이렉션
Action 함수는 일반적으로 데이터 변경 후 다른 페이지로 리다이렉트합니다. React Router는 redirect
유틸리티 함수를 제공합니다:
import { redirect } from "react-router-dom";
// Action 함수 정의
export async function action({ request }) {
const formData = await request.formData();
const postData = Object.fromEntries(formData);
await fetch("http://localhost:8080/posts", {
method: "POST",
body: JSON.stringify(postData),
headers: {
"Content-Type": "application/json",
},
});
return redirect("/");
}
Action 함수에서 리다이렉션을 수행하면 브라우저 URL이 자동으로 변경되고, 새 경로에 대한 loader 함수가 실행됩니다. 이를 통해 데이터 변경 후 최신 데이터를 보여줄 수 있습니다.