기존까지 우리가 React에서 데이터를 가져오는 방식은 꽤 고정된 패턴이었다. 페이지 컴포넌트가 마운트되고, useEffect 안에서 fetch를 돌리고, 응답이 오면 setState로 상태를 업데이트하는 것. React Router v6.4에서 "Data Router" 라는 개념이 등장하면서 이 흐름이 근본적으로 달라졌다. 데이터 로직이 컴포넌트 안에서 밖으로 올라간 거다.
이 글에서는 그 변화의 중심인 Loader와 Action이 뭔지, 기존 패턴과 어떤 차이가 있는지, 그리고 중첩 라우트에서 에러가 발생했을 때 실제로 어떤 전략을 쓰는지까지 다루겠다.
기존 패턴은 대략 아래와 같은 형태이다. 컴포넌트가 마운트되는 타이밍에 데이터를 요청하고, 응답이 오면 상태로 저장하는 것.
function UserPage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const { id } = useParams();
useEffect(() => {
setLoading(true);
fetch(`/api/users/${id}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [id]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
단순해 보이지만 이 패턴에는 여러 문제가 숨어 있다. 첫째로, 컴포넌트가 먼저 마운트되어야 fetch가 시작되는 구조라서 깊은 중첩에서 각 레벨마다 순차적으로 데이터를 기다리는 waterfall이 생긴다. 둘째로, id가 바뀌는 타이밍과 응답이 돌아오는 타이밍이 달라질 수 있어서 race condition을 방지하려면 직접 AbortController를 관리해야 한다. 셋째로, loading, error, data 같은 상태를 컴포넌트 안에서 직접 챙기는 수고가 있다.
이 세 문제를 전부 해결한 것이 React Router v6.4의 Loader / Action 패턴이다.
Loader는 해당 라우트로 navigation이 일어나는 순간, 컴포넌트 렌더 전에 실행되는 함수이다. 비유하자면, 호텔 체크인을 하는데 방에 들어가기 전에 이미 침대, 수건, 미니바가 준비된 상황과 같다. 앉으면 바로 사용할 수 있는 상태.
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/users/:id",
// ← 라우트 설정 단계에서 데이터 준비
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
return res.json();
},
element: <UserPage />,
},
]);
// 컴포넌트는 그냥 준비된 데이터를 꺼내면 된다
function UserPage() {
const user = useLoaderData(); // useState, useEffect 없음
return <div>{user.name}</div>;
}
컴포넌트 안에 useState도, useEffect도 없다. 데이터 로직은 라우트 정의에, 렌더 로직만 컴포넌트에 남은 구조이다.
Loader가 가장 빛을 발하는 곳은 중첩 라우트이다. 기존 useEffect 패턴은 부모가 먼저 렌더되고 fetch한 후, 자식이 렌더되고 다시 fetch하는 순차적 구조이었다. Loader는 이를 완전히 뒤집어준다.
❌ useEffect 패턴: 순차적 (Waterfall)
Layout (마운트 → fetch)
↓ 응답 후
UserList (마운트 → fetch)
↓ 응답 후
UserDetail (마운트 → fetch)
→ 렌더
✅ Loader 패턴: 동시 실행 (Parallel)
Layout loader ───┐
UserList loader ─┼─→ 모두 완료 후 → 렌더
UserDetail loader┘
중첩 라우트의 모든 loader가 동시에 실행되고, 전부 완료되면 그때 렌더가 일어난다. 네트워크 요청 시간이 깊은 구조에서 절반 이상 줄어들 수 있다.