컴포넌트 밖으로의 데이터 관리

기존까지 우리가 React에서 데이터를 가져오는 방식은 꽤 고정된 패턴이었다. 페이지 컴포넌트가 마운트되고, useEffect 안에서 fetch를 돌리고, 응답이 오면 setState로 상태를 업데이트하는 것. React Router v6.4에서 "Data Router" 라는 개념이 등장하면서 이 흐름이 근본적으로 달라졌다. 데이터 로직이 컴포넌트 안에서 밖으로 올라간 거다.

이 글에서는 그 변화의 중심인 Loader와 Action이 뭔지, 기존 패턴과 어떤 차이가 있는지, 그리고 중첩 라우트에서 에러가 발생했을 때 실제로 어떤 전략을 쓰는지까지 다루겠다.

1. 기존 패턴: useEffect + fetch


기존 패턴은 대략 아래와 같은 형태이다. 컴포넌트가 마운트되는 타이밍에 데이터를 요청하고, 응답이 오면 상태로 저장하는 것.

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 패턴이다.

2. Loader: 페이지 열기 전의 준비실


2-1) Loader가 뭐냐

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도 없다. 데이터 로직은 라우트 정의에, 렌더 로직만 컴포넌트에 남은 구조이다.

2-2) Parallel Loading과 Waterfall 해결

Loader가 가장 빛을 발하는 곳은 중첩 라우트이다. 기존 useEffect 패턴은 부모가 먼저 렌더되고 fetch한 후, 자식이 렌더되고 다시 fetch하는 순차적 구조이었다. Loader는 이를 완전히 뒤집어준다.

❌ useEffect 패턴: 순차적 (Waterfall)

  Layout (마운트 → fetch)
    ↓ 응답 후
  UserList (마운트 → fetch)
    ↓ 응답 후
  UserDetail (마운트 → fetch)
                            → 렌더

✅ Loader 패턴: 동시 실행 (Parallel)

  Layout loader ───┐
  UserList loader ─┼─→ 모두 완료 후 → 렌더
  UserDetail loader┘

중첩 라우트의 모든 loader가 동시에 실행되고, 전부 완료되면 그때 렌더가 일어난다. 네트워크 요청 시간이 깊은 구조에서 절반 이상 줄어들 수 있다.