일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Database
- SSR
- 모던자바스크립트
- 초기마운트
- CSR
- docker
- react-hook-form
- ErrorBoundary
- 리액트
- reactquery
- key
- react-hook
- 리액트 훅
- lazy()
- msw
- next-cookies
- 리액트훅
- react
- express
- useEffect
- NextJs
- 클래스
- useLayoutEffect
- Firebase
- Today
- Total
한우의 개발일기
[Next.js] ReactQuery 적용기 본문
프로젝트에 리액트 쿼리 적용기
나는 프로젝트에 next AppRouter 를 적용하여 프로젝트를 진행을 하고 있던중 밑에 보이는 화면의 오른쪽 창을 앱라우터를 쓰니까 Parallel Routes 한번 적용을 해보면 어떨까? 라는 생각에 Parallel Routes로 오른쪽 창을 제작하게 되었다
Parallel Routes를 적용을 하여 위에 보이는 사진의 오른쪽 창은 모달 혹은 다른 컴포넌트가 아닌 하나의 페이지가 되고있다.
트러블 슈팅 및 리액트 쿼리 적용기
여기서 오는 트러블슈팅이 하나있었는데 오른쪽 창과 뒤에 보이는 페이지는 아예 서로다른 페이지이다.
예를들어 뒤에있는 페이지는 PageA.tsx
이고 오른쪽에 있는 페이지는 pageB.tsx
인것이다
여기서 왔던 트러블슈팅은 완료하기
버튼을 누르거나 해당 글을 수정 혹은 삭제를 했을때 뒤에 있는 페이지와 완전이 다른 페이지이기 때문에 오른쪽에 보이는 pageB.tsx
에서 했던 작업들이 뒤에있는 PageA.tsx
에 바로 반영이 되지않는 트러블 슈팅을 겪게 되었다.
어떻게 해결을 할까??
처음엔 작업을 완료하면 다른 페이지이기 때문에 router.replace
와 같이 페이지를 새로 고침하거나 아예 다시 refetch
를 받아야겠다 라고 생각을 했는데 리액트 쿼리를 이용하면 해결을 할 수 있지 않을까?? 라는 생각을 해봤다.
리액트 쿼리를 어떻게 써야할까?
리액트 쿼리의 장점 중 하나인 캐싱을 이용하기로 했다.
API 명세서를 읽다보니 PageA.tsx
에서 보이는 카드 컴포넌트에 들어있는 reponseData
와 pageB.tsx
에서 필요로하는 reponseData
중 글 작성자
를 제외한 모든 reponseData
가 동일하다 라는걸 알았고 과감하게 pageB.tsx
에서 필요로 하는 API 는 사용하지 않기로 했다.(댓글 관련된 API 는 따로 있었다.
캐싱을 어떻게 이용하면 좋을까?
React Query의 주요 장점 중 하나인 캐싱을 이용할때 동일한 쿼리 키를 이용하면 좋지 않을까? 라는 생각을 해봤다
동일한 쿼리 키를 사용하면
- 첫 번째 페이지에서 데이터를 fetch 할 때 React Query가 해당 데이터를 캐시에 저장하고
- 다른 페이지에서 같은 쿼리 키로 useQuery를 사용하면, 캐시된 데이터를 즉시 반환하게 된다
- 불필요한 네트워크 요청을 방지할 수 있어서
pageB.tsx
에서 불필요한 네트워크 요청 또한 줄일 수 있을거 같았다.
간단하게 코드를 약간 가져와 보면
먼저 pageA.tsx
에서
const { data: tasks } = useQuery({
queryKey: ["tasks", currentTeamId, currentListId, CurrentDate],
queryFn: () =>
getTasks({
groupId: currentTeamId,
taskListId: currentListId,
date: CurrentDate,
}),
});
와 같이 tasks 라는 쿼리 키를 이용하여 데이터를 받고 캐싱을 진행한뒤
pageB.tsx
에서는 또한 동일한 쿼리키를 통하여
const { data: tasks } = useQuery({
queryKey: ["tasks", Number(groupId), Number(taskListId), currentDate],
queryFn: () =>
getTasks({
groupId: Number(groupId),
taskListId: Number(taskListId),
date: currentDate!,
}),
enabled: !!currentDate,
});
해당 작업을 진행을 해주면 동일한 쿼리 키를 통해 캐싱된 데이터를 받아올 수 있었다.
그렇다면 트러블 슈팅은??
내가 처음 말한 트러블 슈팅은 pageB.tsx
에서 한 동작이 바로 pageA.tsx
에서 반영이 안되는게 트러블 슈팅이었는데
이것도 리액트 쿼리의 동일 키로 해결을 할 수 있었다
위에서 말했듯이 리액트 쿼리를 통해 캐싱을 한다면 해당 데이터가 캐싱이 되어지고 동일한 쿼리키를 사용하는 곳에선 캐싱된 데이터를 가져와서 사용하게 된다.
하지만 delete
혹은 put
과 같은 삭제
와 수정
과 같은 작업은 어떻게 진행을 해야할까??
pageA.tsx
에서도 캐밥버튼을 누르면 해당 카드의 제목과 내용을 수정을 할 수 있엇고 카드를 삭제할 수 있었다. 체크버튼을 통해서 완료기능을 이용할 수 있었다
또한 pageB.tsx
에서도 캐밥버튼을 통해서 수정 삭제가 가능하고
완료하기 버튼을 통해서 완료 기능을 이용할 수 있었다.
두개의 페이지에서 동일한 작업을 할 수 있으고 하나의 API를 사용하기 때문에 이 작업을 묶어놓은 커스텀 훅을 하나 만들면 좋겠다 라는 생각이 들어서 커스텀 훅을 만들었다.
그래서 만든 커스텀 훅은
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import editTaskDetail from "@/lib/api/task-detail/edit-task-detail";
import { TaskDetailData, TaskEditData } from "@/types/task-detail/index";
const useTaskMutation = (
groupId: number,
taskListId: number,
taskId: number,
setIsTaskCompleted: (done: boolean) => void,
) => {
const queryClient = useQueryClient();
const editTaskMutation = useMutation({
mutationFn: (data: TaskEditData) =>
editTaskDetail(groupId, taskListId, taskId, data),
onMutate: async (newData) => {
await queryClient.cancelQueries({
queryKey: ["tasks"],
});
const previousTasks = queryClient.getQueryData<TaskDetailData[]>([
"tasks",
groupId,
taskListId,
]);
queryClient.setQueriesData<TaskDetailData[]>(
{ queryKey: ["tasks"], exact: false },
(old) => {
if (old) {
return old.map((task) =>
task.id === taskId
? {
...task,
doneAt: newData.done ? new Date().toISOString() : null,
}
: task,
);
}
return old;
},
);
setIsTaskCompleted(newData.done);
return { previousTasks };
},
onError: (err, newData, context) => {
queryClient.setQueryData(
["tasks", groupId, taskListId],
context?.previousTasks,
);
setIsTaskCompleted(!newData.done);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["tasks"],
});
},
});
return { editTaskMutation };
};
export default useTaskMutation;
이렇게 생겨먹었는데 여기서 낙관적 업데이트와 쿼리 무효화를 통해서 작업을 진행 하고 있다
- 기본 설정:
const useTaskMutation = ( groupId: number, taskListId: number, taskId: number, setIsTaskCompleted: (done: boolean) => void, ) => { const queryClient = useQueryClient();
- 그룹ID, 태스크리스트ID, 태스크ID와 완료상태 설정 함수를 파라미터로 받는다
- mutation 정의:
mutationFn: (data: TaskEditData) => editTaskDetail(groupId, taskListId, taskId, data),
- 실제 API 호출 함수를 정의
- onMutate (낙관적 업데이트 처리):
onMutate: async (newData) => { // 진행 중인 쿼리들 취소 await queryClient.cancelQueries({ queryKey: ["tasks"], }); // 이전 데이터 백업 const previousTasks = queryClient.getQueryData<TaskDetailData[]>([ "tasks", groupId, taskListId, ]); // 캐시 데이터 즉시 업데이트 (UI 바로 반영) queryClient.setQueriesData<TaskDetailData[]>( { queryKey: ["tasks"], exact: false }, (old) => { if (old) { return old.map((task) => task.id === taskId ? { ...task, doneAt: newData.done ? new Date().toISOString() : null, } : task, ); } return old; }, ); // UI 상태 업데이트 setIsTaskCompleted(newData.done); // 이전 데이터 반환 (에러시 복구용) return { previousTasks }; },
- 에러 처리:
onError: (err, newData, context) => { // 에러 발생시 이전 데이터로 롤백 queryClient.setQueryData( ["tasks", groupId, taskListId], context?.previousTasks, ); // UI 상태도 원복 setIsTaskCompleted(!newData.done); },
- 작업 완료 후 처리:
onSettled: () => { // 서버의 최신 데이터로 갱신 queryClient.invalidateQueries({ queryKey: ["tasks"], }); },
주요 특징:
- 낙관적 업데이트(Optimistic Update) 사용
- API 응답을 기다리지 않고 UI를 먼저 업데이트
- 실패시 이전 상태로 롤백 가능
- 에러 처리
- 실패시 이전 상태로 복원
- UI도 이전 상태로 되돌림
- 데이터 일관성
- 작업 완료 후 서버 데이터로 동기화
- 관련된 모든 쿼리 무효화
쿼리 무효화를 통해 데이터 일관성 유지, 실시간성 제공, 동시성 문제 해결 도 해주면서 또한 이전 작업을 getQueryData 통하여 백업도 진행을 하여 실패시 이전 작업으로 돌아가게 해주었다
이런 방식으로 사용자 경험을 개선하면서도 데이터 일관성을 유지하는 구조로 진행을 하게 되었다.
동일 한 쿼리키로 뮤테이션 또한 작업을 하게 되고 낙관적 업데이트를 통해서 뒤에 보이는 페이지에서도 바로 UI 업데이트를 진행을 해주고 최신쿼리로 업데이트를 주게 되어 트러블슈팅을 해결 할 수 있었다.
여기서 생겼던 의문점 invalidateQuery와 setQuery는 무슨차이가 있을까?
invalidateQueries
- 서버에 다시 요청을 보내서 최신 데이터를 가져옴
- 더 안전하지만 추가 네트워크 요청 발생
setQueryData
- 캐시를 직접 수정하여 즉시 UI 업데이트
- 네트워크 요청 없어서 더 빠름
- 서버 상태와 불일치할 가능성 있음
그래서 나는 setQuery를 통해 낙관적 업데이트를 진행을 해주고
invalidateQueries를 진행해 주므로써 서버와 클라이언트의 불일치를 줄여보았다.
'NextJS' 카테고리의 다른 글
[Next.js]next 프로젝트 도커 빌드하기(2) (0) | 2024.11.11 |
---|---|
[Next.js]next 프로젝트에 도커 적용기 (0) | 2024.11.11 |
[Next.js]Nextjs 프로젝트 도커로 빌드하기 (0) | 2024.11.11 |
[Next.js] next-cookies(서버/클라이언트 쿠키 사용기) (0) | 2024.11.11 |
[Next.js]Next.js에 MSW 적용하기 (0) | 2024.11.11 |