리액트 쿼리와 리액트 라우터를 함께 효과적으로 사용하는 방법을 배워봅니다.
원문 링크입니다. https://tkdodo.eu/blog/react-query-meets-react-router
Remix는 로더와 액션이라는 데이터 가져오기의 새로운 컨셉을 도입했습니다.
그리고 이 개념은 React Router 6.4 버전부터 순수한 CSR에서도 사용할 수 있게 되었습니다.
URL 세그먼트, 레이아웃, 데이터는 좋은 짝꿍입니다.
loader는 리액트 쿼리 등 데이터 가져오기를 url 단위로 캡슐화 해주는 요소입니다.
action은 폼 제출(submit)을 에뮬레이트 하는 녀석입니다.
앱이 라우터에 non-get submit("post", "put", "patch", "delete")을 보낼 때마다 액션이 호출됩니다.
(튜토리얼 살펴보기 : tutorial)
React Router가 데이터 가져오기 게임에 등장하면서
React Query와 같은 기존 데이터 가져오기 및 캐싱 라이브러리와 어떻게 경쟁하거나 상관 관계가 있는지
이해하는 것은 자연스럽습니다.
리액트 라우터와 리액트 쿼리는 환상의 짝궁입니다.
데이터를 가져오는 라우터
- 각 경로(route)에 loaders를 정의할 수 있으며,
- 이 로더는 경로를 방문할 때 호출됩니다.
- 경로 컴포넌트에서 LoaderData()를 사용하여 해당 데이터에 액세스할 수 있습니다.
- 데이터 업데이트는 양식을 제출하는 것만큼 간단합니다. 양식 제출은 action 함수를 호출합니다.
- 액션은 모든 활성 로더를 invalidate하므로 화면에 업데이트된 최신 데이터가 자동으로 표시됩니다.
주 : 리액트 쿼리의 invalidateQueries는 해당 쿼리를 stale 한 것으로 만들어
해당 쿼리를 이용한 리렌더링이 발생하면 api 요청을 다시 해 새로운 데이터를 자동으로 현재 UI에 반영하게 합니다.
invalidate한다고 즉시 반영되는것은 아닙니다.
참고 : https://tanstack.com/query/v4/docs/guides/query-invalidation
이는 쿼리 / 뮤테이션과 매우 유사합니다.
Remixing React Router 발표 이후 아래와 같은 의문을 품는 것은 당연합니다.
- 이제 경로에서 데이터를 가져올 수 있는데 React Query가 필요할까요?
- 현재 React Query를 사용하고 있는데, 새로운 React Router 기능을 활용할 필요가 있을까요?
저는 두 질문에 전부 yes라 대답하겠습니다.
로더에는 캐시 기능이 없습니다 : when과 what
데이터 일찍 가져오기
우리는 일반적으로 컴포넌트 마운트 시 데이터를 가져옵니다.
이는 좋지 않습니다. 로딩 스피너가 필요하기 때문입니다.
프리패칭(Prefetching)은 후속 탐색에만 적용 가능하며, 모든 경로에 수동 적용이 필요합니다.
라우터는 방문하려는 페이지를 항상 알고 있는 첫 번째 컴포넌트이며
이제 로더가 있으므로 해당 페이지에서 렌더링해야 하는 데이터를 알 수 있습니다.
이것은 첫 번째 페이지 방문에 유용하지만, 페이지 방문 시마다 로더가 호출될 수 있습니다
라우터에는 캐시가 없기 때문입니다.
우리가 이에 대해 조치를 취하지 않는 한 매 요청은 계속 서버에 다시 도달합니다.
예를 들어(이전에 언급된 tutorial에서 가져온 것입니다.) 연락처 목록이 있다고 가정합니다.
그 중 하나를 클릭하면 연락처 세부 정보가 표시됩니다.
연락처(Contact) 컴포넌트
import { useLoaderData } from 'react-router-dom'
import { getContact } from '../contacts'
// ⬇️ 세부 경로를 위한 로더입니다.
export async function loader({ params }) {
return getContact(params.contactId)
}
export default function Contact() {
// ⬇️ 로더에서 데이터를 가져옵니다.
const contact = useLoaderData()
// render some jsx
}
라우터 설정
import Contact, { loader as contactLoader } from './routes/contact'
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{
path: 'contacts',
element: <Contacts />,
children: [
{
path: 'contacts/:contactId',
element: <Contact />,
// ⬇️ 세부 경로에 로더 사용을 설정합니다.
loader: contactLoader,
},
],
},
],
},
])
데이터를 너무 자주 가져오는 것
이제 React Query가 등장할 때입니다!
예제로 확인하기
src/routes/contacts.jsx
import { useQuery } from '@tanstack/react-query'
import { getContact } from '../contacts'
// ⬇️ 쿼리를 정의합니다.
const contactDetailQuery = (id) => ({
queryKey: ['contacts', 'detail', id],
queryFn: async () => getContact(id),
})
// ⬇️ 쿼리 클라이언트에 접근이 필요합니다.
// 즉 로더는 쿼리클라이언트에 캐싱해 두는 역할을 수행합니다.
export const loader =
(queryClient) =>
async ({ params }) => {
const query = contactDetailQuery(params.contactId)
// ⬇️ return data or fetch it
return (
queryClient.getQueryData(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}
export default function Contact() {
const params = useParams()
// ⬇️ useQuery는 기존처럼 사용합니다.
const { data: contact } = useQuery(contactDetailQuery(params.contactId))
// render some jsx
}
src/main.jsx
const queryClient = new QueryClient()
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{
path: 'contacts',
element: <Contacts />,
children: [
{
path: 'contacts/:contactId',
element: <Contact />,
// ⬇️ 로더에 쿼리클라이언트를 넘겨줍니다. (qc)=>({param})=>any
loader: contactLoader(queryClient),
},
],
},
],
},
])
로더는 QueryClient 접근이 필요합니다.
로더는 훅이 아니므로 QueryClient를 사용할 수 없습니다.
QueryClient를 직접 가져오는 것은 권장하지 않는 것(I'm not recommending)이므로
파라미터로 명시적으로 전달하는 것이 가장 좋은 대안인 것 같습니다.
getQueryData ?? fetchQuery
export const loader =
(queryClient) =>
({ params }) =>
queryClient.fetchQuery({
...contactDetailQuery(params.contactId),
staleTime: 1000 * 60 * 2,
})
getQueryData 접근 방식은 staleTime을 Infinity로 설정하는 것과 거의 동일합니다.
(수동 쿼리 무효화가 staleTime보다 우선한다는 점을 제외하고 )
따라서 코드가 약간 더 많더라도 getQueryData 접근 방식이 더 좋습니다.
주 : 이렇게 하면 무조건 캐시에서 가져오기 떄문에 적절하게 invalidate를 위한 수단이 필요합니다.
타입스크립트 팁
이를 통해 컴포넌트에서 useQuery를 호출하면 useLoaderData를 호출하는 것처럼 데이터를 사용할 수 있습니다.
그러나 TypeScript는 이를 알 수 있는 방법이 없습니다.
반환된 데이터 타입은 Contact | undefined 입니다.
Matt Pocock의 React Query v4를 위한 contribution 덕택에
initialData가 제공되면 undefined를 유니온에서 제외할 수 있습니다.
export default function Contact() {
const initialData = useLoaderData() as Awaited<
ReturnType<ReturnType<typeof loader>>
>
const params = useParams()
const { data: contact } = useQuery({
...contactDetailQuery(params.contactId),
initialData,
})
// render some jsx
}
액션에서 쿼리 무효화(Invalidating)
퍼즐의 다음 조각은 쿼리 무효화를 포함합니다.
다음은 React Query 없이 액션이 구현되는 모습입니다.
(예, 업데이트를 수행하는 데 필요한 모든 것입니다).
export const action = async ({ request, params }) => {
const formData = await request.formData()
const updates = Object.fromEntries(formData)
await updateContact(params.contactId, updates)
return redirect(`/contacts/${params.contactId}`)
}
액션은 로더를 무효화하지만,
항상 캐시에서 데이터를 반환하도록 로더를 설정했기 때문에 캐시를 무효화하지 않는 한 업데이트는 보여지지 않습니다.
캐시 무효화를 위해 우리는 한 줄의 코드만 추가하면 됩니다.
export const action =
(queryClient) =>
async ({ request, params }) => {
const formData = await request.formData()
const updates = Object.fromEntries(formData)
await updateContact(params.contactId, updates)
// 여기
await queryClient.invalidateQueries(["contacts"]);
return redirect(`/contacts/${params.contactId}`)
}
invalidateQueries의 퍼지 일치(fuzzy matching of invalidateQueries)는
액션이 완료되고 세부 정보 보기로 다시 리디렉션될 때까지
목록과 세부 정보 보기가 캐시에 새 데이터를 가져오도록 합니다.
await은 모드 전환 레버
export const action =
(queryClient) =>
async ({ request, params }) => {
const formData = await request.formData()
const updates = Object.fromEntries(formData)
await updateContact(params.contactId, updates)
queryClient.invalidateQueries(["contacts"]);
return redirect(`/contacts/${params.contactId}`)
}
Await는 말 그대로 양방향으로 당기는 레버(지렛대)가 됩니다
(이 비유는 Ryan의 When To Fetch에 기반합니다. 아직 시청하지 않은 경우 시청하십시오):
- 가능한 한 빨리 상세 보기로 다시 전환하는 것이 중요합니까?
- await을 사용하지 마세요.
- 오래된 데이터를 표시할 경우 발생할 수 있는 잠재적인 레이아웃 변경(shift)을 피하는 것이 중요한가요?
- 모든 새 데이터가 있을 때까지 액션 실행을 보류하고 싶나요?
- await을 사용합니다.
두 가지 접근 방식을 섞어 사용해,
중요한 다시 가져오기는 전체를 다 기다릴 수도 있지만 덜 중요한 것은 백그라운드에서 수행되도록 할 수 있습니다. (캐시 데이터를 보여주며)
요약
'FrontEnd' 카테고리의 다른 글
리액트가 함수 컴포넌트와 클래스 컴포넌트를 구분하는 방법 (0) | 2022.09.30 |
---|---|
리액트 : 리렌더링 하도록 코딩하기 (부제 : memo 대신 useMemo) (0) | 2022.09.27 |
javascript 프로젝트에 d.ts를 이용하여 타입스크립트 도입하기 (0) | 2022.09.22 |
스토리북 개발팀이 알려주는 컨테이너 / 프리젠터 패턴 - Context API를 이용해 의존성 주입하기 (0) | 2022.09.22 |
소프트웨어 합성 : 트랜스듀서(Transducers) (0) | 2022.09.20 |