배경
Next.js로만 개발하다가 얼마 전부터 순수 React로 개발하게 되었고 오랜만에 React Router를 사용하게 되었습니다. 이 과정에서 loader라는 개념을 접했는데, TanStack Query와의 조합이 괜찮아서 이에 대해 정리해 보려 합니다.
loader란 무엇인가
loader는 React Router에서 제공하는 기능으로, 컴포넌트가 렌더링 되기 전에 데이터를 미리 불러올 수 있습니다. 즉, 페이지가 마운트 되기 전에 필요한 데이터를 가져와서 첫 화면을 즉시 렌더링 할 수 있습니다. UI와 데이터의 동기화를 보장하기 때문에 단순히 데이터를 미리 로드하는 것 이상의 의미를 가집니다.
loader 실행 과정
loader의 실행은 다음과 같은 단계를 따릅니다.
사용자가 특정 경로로 이동 요청
→ URL과 일치하는 route 식별
→ route와 연결된 loader 실행
→ loader가 완료될 때까지 route 전환 지연
→ 데이터 준비 완료 시 해당 컴포넌트 렌더링
→ 컴포넌트에서 useLoaderData
로 데이터 접근
사용 예시
loader를 사용해 데이터를 불러오는 예시 코드를 살펴보겠습니다.
// 라우터 설정
const router = createBrowserRouter([
{
path: "/users/:userId",
element: <UserProfile />,
loader: userLoader, // loader 함수
}
]);
먼저 createBrowserRouter
를 사용해 라우터를 생성합니다.
/users/:userId
경로에 대해 <UserProfile />
컴포넌트를 렌더링하도록 설정했습니다.
이 경로에 접근할 때 데이터를 로드할 함수는 loader: userLoader
로 지정할 수 있습니다.
// 기본적인 loader 함수 구조
export const userLoader = async ({ request, params }: LoaderFunctionArgs) => {
// path parameter, query parameter 활용 가능
// ex. /user/123?view=settings
const url = new URL(request.url);
const view = url.searchParams.get("view");
const userId = params.userId;
// 데이터 패칭
const userData = await fetchUserData(userId);
if (!userData) {
throw redirect("/not-found");
}
if (view !== "settings") {
throw redirect("/not-allowed");
}
return userData;
}
loader를 사용하지 않으면 컴포넌트 내에서 useParams, useSearchParams, useEffect
를 통해 처리해야 하므로 관심사 분리가 어려워집니다.
반면 loader를 사용하면 라우팅과 데이터 로딩을 라우터 레벨에서 처리하고, 컴포넌트는 UI 렌더링에 집중할 수 있어 관심사를 분리할 수 있습니다.
userLoader
함수는 URL에서 파라미터를 추출해 데이터를 가져오고, 그 결과를 반환합니다.
라우팅이 완료되기 전에 redirection이 처리되므로, redirection으로 인한 불필요한 렌더링이 발생하지 않습니다.
// 컴포넌트에서 활용
import { useLoaderData } from "react-router-dom";
const UserProfile = () => {
const data = useLoaderData() as UserData;
return (
// 생략
);
};
React Router에서 제공하는 useLoaderData
훅을 사용해 loader에서 반환된 데이터에 접근할 수 있습니다.
loader를 사용하기 좋은 경우
페이지가 렌더링되기 전에 무언가를 처리해야 하는 상황에 적합합니다. 자세한 예시는 다음과 같습니다.
- 페이지 진입 시점에 데이터가 필수적일 때
- URL 파라미터에 따라 다른 데이터를 표시해야 할 때
- 페이지 접근 가능 여부를 확인하고 권한에 따라 다른 페이지로 redirect 해야 할 때
loader와 TanStack Query를 함께 써야 하는 이유
TanStack Query만 사용하면 컴포넌트가 마운트된 이후에 데이터 패칭이 시작됩니다. 따라서 loader를 사용해 페이지 진입 시 데이터를 미리 로드하고, TanStack Query를 사용해 해당 데이터를 캐싱하면 성능을 극대화할 수 있습니다.
loader에서 queryClient.ensureQueryData
를 사용하면 데이터가 TanStack Query 캐시에 저장됩니다.
ensureQueryData
는 캐시된 데이터가 있으면 이를 반환하고, 없으면 queryClient.fetchQuery
를 호출합니다.
이로 인해 useLoaderData
로 데이터에 바로 접근할 수 있으며, 동일한 데이터가 필요할 때는 불필요한 API 호출 없이 캐시된 데이터를 활용할 수 있습니다.
함께 사용하는 예시
/invitation?invitation_code=123
이라는 URL에서 초대 코드를 추출하고, 초대 코드의 유효성을 API 호출로 검증하는 예시를 준비했습니다.
초대 코드가 유효하면 관련 초대 정보를 반환하고, 유효하지 않으면 에러 처리를 통해 적절한 경로로 넘기려고 합니다.
이는 앞에서 설명한 것처럼 페이지가 렌더링 되기 전에 무언가 처리해야 하는 상황에 해당합니다.
- 페이지 진입 시점에 데이터가 필수적일 때
- 초대 코드가 유효해야만 초대 정보를 보여줄 수 있으므로, 페이지가 렌더링 되기 전에 데이터가 필요합니다.
- URL 파라미터에 따라 다른 데이터를 표시해야 할 때
- 초대 코드가 URL 파라미터로 전달되므로 URL 파라미터가 다르면 반환되는 데이터도 달라집니다.
- 페이지 접근 가능 여부를 확인하고 권한에 따라 다른 페이지로 redirect 해야 할 때
- 초대 코드가 유효하지 않을 경우 사용자를 다른 페이지로 redirect 합니다.
const queryClient = new QueryClient();
// 라우터 설정
const router = createBrowserRouter([
{
path: "/invitation",
element: <Invitation />,
loader: invitationLoader(queryClient), // queryClient를 loader에 전달
},
]);
loader는 훅이 아니기 때문에 useQueryClient
를 사용할 수 없습니다.
QueryClient
를 직접 import 하는 것은 권장되지 않으므로 queryClient
를 loader에 전달하는 방식으로 구현해야 합니다.
// loader 함수
export const invitationLoader = (queryClient: QueryClient) => async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const invitationCodeFromPath = url.searchParams.get("invitation_code") ?? '';
try {
const { data: invitationInfo } = await queryClient.ensureQueryData(
invitationQueries.invitationInfo(invitationCodeFromPath)
);
return {
invitationCode: invitationCodeFromPath,
invitationInfo
};
} catch (error) {
const errorCode = isAxiosError(error) ? error.response?.status : undefined;
const urlForException = getUrlForException(errorCode);
return replace(urlForException);
}
}
invitationLoader
함수는 URL에서 invitation_code
를 추출해 데이터를 패칭합니다.
데이터 패칭 중 에러가 발생할 경우, redirect 하는 구조를 가지고 있습니다.
이처럼 loader와 TanStack Query는 각기 다른 수준에서 오류를 처리합니다.
loader는 페이지 진입 전에 발생하는 치명적 오류를 처리하고,
TanStack Query는 컴포넌트 내부에서 세밀한 오류를 처리합니다.
// 컴포넌트에서 활용
const Invitation = () => {
const { invitationCode, invitationInfo } = useLoaderData() as InvitationData;
return (
// 생략
);
};
결론
loader와 TanStack Query를 함께 사용하면 사용자 경험을 개선하고, 데이터 관리의 효율성을 높이며, 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다. 이러한 접근 방식은 데이터 패칭과 UI 렌더링 간의 경계를 명확히 하고, 복잡성을 줄여 더 나은 개발 환경을 제공합니다. 페이지가 렌더링 되기 전에 무언가를 처리해야 하는 상황이라면 loader와 TanStack Query를 함께 사용해 보는 것을 추천합니다.