vue query 는 일반적으로 query keys
를 바탕으로 쿼리 캐싱을 관리한다.
이 query keys
는 그냥 문자열 도 가능하고 배열도 가능하다.
일단 serializable 하고 고유하다면 사용가능하다.
// 할 일 목록
useQuery(['todos'], ...)
// 그 밖의 다른 것, 무엇이든!
useQuery(['something', 'special'], ...)
보통은 이런식으로 mutation 에 들어갈 id, index, 혹은 매개변수로 통상 key 값을 세팅한다.
// 개별 할 일
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// "미리보기" 형식의 개별 할 일
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// "완료된" 할 일 목록
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
이때 변환되는 값에 따라 id 나 매개변수를 넣고 싶다면 그냥 ref 를 선언하여 reactive 한 값을 넣어주면 된다.
키 순서에 상관이 없다는 것이다. 다만, array 는 순서에 상관이 있기 때문에
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
위 query key 는 각각 다른 키가 된다는 것이다.
useQuery
를 사용할 때 다음과 같은 방법으로 사용할 수 있다.
useQuery(["todos"], fetchAllTodos);
useQuery(["todos", todoId], () => fetchTodoById(todoId));
useQuery(["todos", todoId], async () => {
const data = await fetchTodoById(todoId);
return data;
});
useQuery(["todos", todoId], ({ queryKey }) => fetchTodoById(queryKey[1]));
그리고 대부분은 http utility library 로 axios 를 사용하기 때문에 구조분해로 error 를 꺼내와서 사용하면 되잠 만약 다른 library 를 사용하여 오류를 던지지 않는 일부 유틸리티 라이브러리를 사용한다면 fetch API 로 직접 에러를 만들어서 던지를 로직을 작성하면 된다.
useQuery(["todos", todoId], async () => {
const response = await fetch("/todos/" + todoId);
if (!response.ok) {
throw new Error("네트워크 응답이 정상이 아님");
}
return response.json();
});
QueryFunctionContext 로는
string, number, array, etc...
unknown
| undefined
옵셔널로 사용된다. query 를 중간에 취소할 때 사용한다.
Record<string, unknown>
쿼리에 추가적인 정보를 기입할 때 사용된다.
import { useQuery } from 'vue-query';
import axios from 'axios';
// 쿼리 함수 정의
async function fetchResource({ queryKey, pageParam, meta }) {
// queryKey에서 필요한 파라미터 추출
const [resourceType, resourceId] = queryKey;
// API 요청 URL 구성
const url = `https://example.com/api/${resourceType}/${resourceId}?page=${pageParam || 1}`;
// Axios를 이용한 데이터 요청
const response = await axios.get(url, {
headers: {
// 메타데이터 활용 예시 (예: API 키)
'API-KEY': meta?.apiKey || 'default-api-key',
},
});
// 응답 데이터 반환
return response.data;
}
// 컴포넌트 내에서 useQuery를 이용하여 쿼리 실행
export default {
setup() {
// 쿼리 키와 쿼리 함수를 useQuery에 전달하여 데이터 요청
const resourceType = 'posts';
const resourceId = 123;
const { data, isLoading, isError } = useQuery(['resource', resourceType, resourceId], fetchResource, {
// 페이지 정보나 메타데이터 전달 예시
queryFnParams: {
pageParam: 1,
meta: {
apiKey: 'your-api-key',
},
},
});
return { data, isLoading, isError };
},
};
function useTodos(status, page) {
const result = useQuery(["todos", { status, page }], fetchTodoList);
}
// 쿼리 함수에서 키, 상태 및 페이지 변수에 접근가능!
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey;
return new Promise();
}
fetchTodoList 함수에서 useQuery 로 던져진 key, 매개변수 등에 접근할 수 있다.
물론 Query function 에서 파라미터로 넘겨도 되지만 Obj 로도 사용가능하다는 것을 알려주고 있다.
import { useQuery } from "vue-query";
useQuery({
queryKey: ["todo", 7],
queryFn: fetchTodo,
...config,
});
useQuery 와 useMuation 을 사용할 때의 네트워크 상태를 나타낸다.
import { createApp } from 'vue';
import App from './App.vue';
import { VueQueryPlugin, QueryClient } from 'vue-query';
// QueryClient 인스턴스 생성
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// networkMode를 'offlineFirst'로 설정
networkMode: 'offlineFirst',
},
},
});
// Vue 애플리케이션 생성 및 Vue Query 플러그인 설치
const app = createApp(App);
// Vue Query 플러그인에 QueryClient 인스턴스 제공
app.use(VueQueryPlugin, { queryClient });
app.mount('#app');
가장 기본 모드이며 이 online 모드일 때만 query, mutation 이 동작한다.
그리고 status 는 항상 loading, error, success 중 하나로 존재하고
fetstatus 는 fetching, paused, idle 중 하나로 존재한다.
이 네트워크 모드가 Online 일 때 쿼리가 시행되지만 fetching 가 진행되는 중에 offline 이 되면 retry 메커니즘을 비롯한 qeury 가 pause 된다.
그리고 다시 네트워크가 연결되면 다시 재실행 되는데 이는 refetchOnReconnect
와는 개념이 조금 다르다.
취소(cancel
)가 아니라 중지(pause
)이기 때문이다.
이 상태는 뭐 offline 이든 online 이든 가리지 않고 항상 fetch 를 진행한다.
이 모드는 쿼리가 작동할 때 네트워크 연결이 필요하지 않은 환경에서 사용한다.
예를들어 AsyncStorage 에서 값을 읽기만 한다거나 하니면 그냥 Promise 객체를 사용하고 싶을 때 사용한다.
네트워크 연결이 없다고 해서 query 가 중지(paused
) 되진 않는다.
재시도(retry
) 도 중지(pause
)되지 않고 계속 조지고 실패하면 error
status 가 된다.
refetchOnReconnect
는 false 로 설정된다. 네트워크에 다시 연결됐다고 해서 다시 refetch 를 하는 것이 좋은 것은 아니기 때문이다. 다만, 원한다면 true 로 옵션을 바꿀 순 있다.
이 모드는 첫 번째 두 옵션 사이의 중간 선택지 이다.
여기서 Vue Query는 queryFn을 한 번 실행하지만, 그 후에 재시도를 일시 중지한다.
이는 오프라인 우선 PWA와 같이 서비스 워커가 요청을 캐싱하기 위해 가로채거나, Cache-Control 헤더를 통한 HTTP 캐싱을 사용하는 경우에 매우 유용하다.
이러한 상황에서는 처음 패칭은 오프라인 저장소/캐시에서 오기 때문에 성공할 가능성이 있다. 하지만 만약 캐시가 누락되었다면 바로 fail 로 간주하고 온라인 쿼리처럼 동작한다. 이때 retry 는 중지된다.
서비스 워커
서비스 워커(Service Worker)는 웹 애플리케이션, 특히 프로그레시브 웹 앱(PWA, Progressive Web App)에서 중요한 역할을 하는 웹 기술이다. 서비스 워커는브라우저
와서버
사이에서프록시 서버
역할을 하며, 웹 애플리케이션을 보다 빠르고 신뢰성 있게 만드는 데 도움을 준다.
1. 오프라인 경험 개선: 서비스 워커는 네트워크 요청을 가로채고 캐싱을 통해 오프라인에서도 콘텐츠를 제공할 수 있게 한다.
이를 통해 네트워크 연결이 불안정하거나 없는 환경에서도 사용자에게 일관된 경험을 제공할 수 있다.
2. 백그라운드 동기화: 네트워크 연결이 다시 확립되었을 때, 서비스 워커를 이용해 백그라운드에서 데이터를 동기화할 수 있다.
이를 통해 애플리케이션은 최신 상태를 유지할 수 있다.
3. 푸시 알림: 서비스 워커는 웹 애플리케이션에서 푸시 알림을 구현할 수 있게 해준다.
이를 통해 사용자가 애플리케이션을 사용하지 않을 때도 중요한 정보를 전달할 수 있다.
4. 성능 향상: 캐싱 전략을 통해 자주 사용되는 리소스를 로컬에 저장하고 빠르게 로드할 수 있어 애플리케이션의 로딩 시간을 단축시킬 수 있다.
서비스 워커의 작동 방식은 비교적 복잡하며, 사용하기 위해서는 HTTPS 환경이 필요하다.
웹 애플리케이션의 root 디렉토리에 서비스 워커 파일을 등록하고, 이를 통해 네트워크 요청을 가로채고 관리한다.
서비스 워커는 웹 애플리케이션의 생명주기와 별개로 동작하며, 웹 페이지가 닫혀 있더라도 백그라운드에서 활동할 수 있다.
동시에 쿼리를 날리는 것이다.
만약 동시에 날릴 쿼리의 수가 정해져 있는 경우엔 굉장히 간단하다.
그냥 useQuery
혹은 useInfiniteQuery
를 나열하면 된다.
// The following queries will execute in parallel
const usersQuery = useQuery('users', fetchUsers)
const teamsQuery = useQuery('teams', fetchTeams)
const projectsQuery = useQuery('projects', fetchProjects)
다만 동적으로 여러개의 쿼리를 처리하고 싶을 때, 예를 들어 사용자의 수대로 무언가를 처리하고 싶을 땐 아래 로직을 사용하면 된다.
const users = computed(...)
const usersQueriesOptions = computed(() => users.value.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
);
const userQueries = useQueries({queries: usersQueriesOptions})
직렬 쿼리로도 변역할 수 있겠다. 즉, 쿼리가 실행되기 이전 선행되어야하는 쿼리가 있어야한다.
// Main Query - get the user
function useUserQuery(email) {
return useQuery(["user", email], () => getUserByEmail(email.value));
}
// Dependant query - get the user's projects
function useUserProjectsQuery(userId, { enabled }) {
return useQuery(["projects", userId], () => getProjectsByUser(userId.value), {
enabled, // The query will not execute until `enabled == true`
});
}
// Get the user
const { data: user } = useUserQuery(email);
const userId = computed(() => user.value?.id);
const enabled = computed(() => !!user.value?.id);
// Then get the user's projects
const { isIdle, data: projects } = useUserProjectsQuery(userId, { enabled });
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)
여기서 핵심은 가장 마지막 줄이다. 여기서 comupted 로 끊임없이 계산되고 있을 때 user id 값이 들어오면 이제 enabled 가 true 로 바뀌고 이때 "projects" 라는 쿼리 키를 갖는 쿼리가 실행되는 것이다.
프로젝트가 시작되었을 때 status, fetchStatus 상태
status: "loading";
fetchStatus: "idle";
위 종속쿼리에서 user 가 enabled 가 되어서 다음 쿼리를 날렸을 때
status: "loading";
fetchStatus: "fetching";
프로젝트 로드가 완료되었을 때
status: "success";
fetchStatus: "idle";
앞서 status, fetchStatus 로 조건에 따라 현재 진행 상태를 표시해줄 수 있었다.
하지만 이걸 전역으로 묶어서도 사용가능하다.
<script setup>
import { useIsFetching } from "vue-query";
const isFetching = useIsFetching();
</script>
<template>
<span v-if="isFetching">쿼리가 백그라운드에서 데이터를 가져오고 있습니다...</span>
</template>
이때 isFetching 은 number 로 완료되었으면 0
, loading, fetching 중일 땐 1
로 표시된다.
앞서 일정 시간이 지난 데이터는 stale 데이터라고 칭하였는데 이를 특정 조건에 refetch 된다고 하였다.
이중, window 가 forcous 될 때도 있었는데 이를 refetchOnWindowFocus
옵션으로 수정할 수 있다.
import { createApp } from "vue";
import { VueQueryPlugin, VueQueryPluginOptions } from "vue-query";
import App from "./App.vue";
const vueQueryPluginOptions: VueQueryPluginOptions = {
queryClientConfig: {
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
},
};
createApp(App).use(VueQueryPlugin, vueQueryPluginOptions).mount("#app");
useQuery("todos", fetchTodos, { refetchOnWindowFocus: false });
만약 setInterval 이런거를 걸어두었을 때 window 가 focous 가 아닐 경우엔 그냥 취소하고 싶을 수 있다. 이럴 때 사용하면 좋은 Window Focus Event 커스텀 하기 이다.
import { focusManager } from 'vue-query';
const intervalId = ref<number|undefined>(undefined);
const startFetching = () => {
intervalId.value = setInterval(() => {
console.log("데이터 새로 고침");
}, 1000);
};
const stopFetching = () => {
if (intervalId.value) {
clearInterval(intervalId.value);
intervalId.value = undefined;
}
};
focusManager.setEventListener((handleFocus:any) => {
if (typeof window !== "undefined" && window.addEventListener) {
window.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible') {
console.log('포커스 감지 - 데이터 새로고침 시작');
handleFocus();
startFetching();
} else {
console.log('포커스 손실 - 데이터 새로고침 중단');
stopFetching();
}
}, false);
window.addEventListener("focus", () => {
console.log('포커스 감지 - 데이터 새로고침 시작');
handleFocus();
startFetching();
}, false);
window.addEventListener('blur', () => {
console.log('포커스 손실 - 데이터 새로고침 중단');
stopFetching();
}, false)
}
return () => {
window.removeEventListener("visibilitychange", handleFocus);
window.removeEventListener("focus", handleFocus);
window.removeEventListener("blur", stopFetching);
};
});
// 초기 데이터 새로 고침 시작
startFetching();
이때 focusManager.setEventListener
를 호출하면 이전에 설정된 핸들러는 제거되고 새로운 핸들러가 사용된다.
저어어엉말 간혼 가다 Iframe 을 사용해야할 때도 있다. 이때 Iframe 에도 fourse 이벤트가 걸리니 이를 빼줘야한다.
import { focusManager } from "vue-query";
import onWindowFocus from "./onWindowFocus"; // The gist above
focusManager.setEventListener(onWindowFocus);
이에 대한 예제 코드
일부 브라우저 내부 대화 상자(예: alert()
에 의해 생성되거나 <input type="file"\>
에 의해 생성된 파일 업로드 대화 상자 등)는 닫힌 후 다시 포커스 refetching 을 야기할 수 있다. 이로 인해 원치 않는 부작용이 발생할 수 있으며, 이러한 dialogues 박스가 처리되기 직전에 컴포넌트 unmount, mount 가 될 수 있다는 것을 유의해야한다.
자동으로 실행되는 쿼리를 비활성화하고 싶을 때는, enabled = false 옵션을 사용할 수 있다.
쿼리에 캐시된 데이터가 있는 경우: 쿼리는 status === success
또는 isSuccess
상태로 초기화.
쿼리에 캐시된 데이터가 없는 경우: 쿼리는 status === idle
또는 isIdle
상태로 시작.
또 false 일 때,
쿼리는 마운트 시 자동으로 데이터를 가져오지 않음.
새 인스턴스가 마운트되거나 나타날 때 쿼리가 백그라운드에서 자동으로 다시 가져오지 않음.
쿼리는 일반적으로 쿼리를 다시 가져오게 만들 query client의 invalidateQueries 및 refetchQueries 호출을 무시함.
refetch를 사용하여 수동으로 쿼리가 데이터를 가져오도록 트리거할 수 있음.
<script setup>
import { useQuery } from "vue-query";
const filter = ref("");
const isEnabled = computed(() => !!filter.value);
const { data } = useQuery(
["todos", filter],
fetchTodoList,
// ⬇️ disabled as long as the filter is empty
{ enabled: isEnabled }
);
</script>
<template>
<span v-if="data">Filter was set and data is here!</span>
</template>
enabled 옵션으로 최초 로드시에만 실행할 수 있도록 할 수 있다.
useQuery
의 결과가 error 를 던질 때, vue query 는 자동적으로 연속하여 재시도를 한다. (기본 3번)
import { useQuery } from "vue-query";
// Make a specific query retry a certain number of times
const result = useQuery("todos", fetchTodos, {
retry: 10, // Will retry failed requests 10 times before displaying an error
retry: (failureCount, error) => ...
});
이때, 숫자 말고 bool 값도 넣을수 있는데 true 면 무한 재시도 false 면 재시도를 하지 않는다.
또 콜백함수를 넣을 수도 있다. 참고로 재시도가 fail 뜨지마자 시도하진 않는다.
아래 코드는 기본 1초 를 시작으로 하여 2배씩 재시도하는 간격이 늘어난다. 이때 최대 30 초를 넘지는 않도록 작성한 예제 코드이다.
new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
참고로 useQuery를 사용할 때 retryDelay 옵션을 수정할 수 있다. 다만 권장하진 않는다.
const result = useQuery("todos", fetchTodos, {
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
});
사실 페이지네이션을 바탕으로 UI 를 작성하는 것은 굉장히 흔한 일이다. 따라서 Vue Query 는 이를 편리하게 동작할 수 있도록 지원한다.
const result = useQuery(["projects", page], fetchProjects);
그런데 UI 를 보면 success 이후에 다시 loading status 가 뜨는 것을 볼 수 있는데 각 페이지들이 새로운 useQuery 로 동작하기 때문이다.
<script setup lang="ts">
...
const pagePayload = ref<Pagination>(new Pagination(0, 3))
const { isLoading, isError, data, error, isPreviousData } = useQuery(
["posts", pagePayload.value],
() => fetchTodoList(pagePayload.value),
{ keepPreviousData: true }
);
const prevPage = () => {
pagePayload.value.page = Math.max(pagePayload.value.page - 1, 1);
};
const nextPage = () => {
if (!isPreviousData.value) {
pagePayload.value.page += 1
}
};
</script>
여기서 주의해야할 점은 payload 로 들어가는 값과 queryKey 값의 2번째 인자가 같아야 동작한다.
결과적으로 아래 사진을 보면 같은 페이지이지만 이전에 로드를 한 번 했냐 안 했냐에 따라 결과가 다르게 표출된다.
첫번째 사진은 최초 로드시 아직 1 페이지를 보여주는 모습 두번째 사진은 이미 로드 이후 다시 2 페이지를 클릭 했을 시 즉각적으로 데이터를 보여주는 모습이다.
참고로 previous data 라는 명칭이 "이전에 불러온 데이터" 라는 번역으로 헷갈릴 수 있는데
이전에 불러온 적이 있는 즉, cache data 냐 아니냐가 아니라
페이지를 기준으로 이전 데이터냐 아니냐를 판단하는 것이다.
keepPreviousData
옵션은 useInfiniteQuery hook
과 함께 작동하여, 무한 쿼리 키가 시간이 지남에 따라 변경되더라도 사용자가 캐시된 데이터를 계속 볼 수 있도록 해주는 옵션을 제공한다.
버튼이라든지 아니면 무한 스크롤을 구현할 때 사용하는 쿼리이다.
fetchNextPage
와 fetchPreviousPage
함수를 사용할 수 있다.
getNextPageParam
및 getPreviousPageParam
옵션을 사용할 수 있다.
hasNextPage
불리언이 사용 가능하다.
getNextPageParam
이 undefined
가 아닌 값을 반환하면 true
이다.
hasPreviousPage
불리언이 사용 가능하다.
getPreviousPageParam
이 undefined
가 아닌 값을 반환하면 true
이다.
백그라운드 새로 고침 상태와 더 많은 상태를 로딩하는 것 사이를 구별하기 위해 isFetchingNextPage
및 isFetchingPreviousPage
불리언이 사용 가능하다.
pages, pageParams 속성값이 있어야한다.
주의: getNextPageParam 함수에서 반환된 pageParam 데이터를 덮어쓰고 싶지 않다면,
인자를 담아 fetchNextPage를 호출하는 일을 피해야 한다.
예를들어 <button @click={fetchNextPage} > 이렇게 하면 onClick 이벤트가 fetchNextPage 함수로 전송되기 때문에 이런식으로 사용하면 안 된다.
<script setup>
import { defineComponent } from "vue";
import { useInfiniteQuery } from "vue-query";
const fetchProjects = ({ pageParam = 0 }) =>
fetch("/api/projects?cursor=" + pageParam);
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isLoading,
isError,
} = useProjectsInfiniteQuery();
</script>
<template>
<span v-if="isLoading">Loading...</span>
<span v-else-if="isError">Error: {{ error.message }}</span>
<div v-else>
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
<ul v-for="(group, index) in data.pages" :key="index">
<li v-for="project in group.projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<button
@click="() => fetchNextPage()"
:disabled="!hasNextPage || isFetchingNextPage"
>
<span v-if="isFetchingNextPage">Loading more...</span>
<span v-else-if="hasNextPage">Load More</span>
<span v-else>Nothing more to load</span>
</button>
</div>
</template>
그래서 그냥 @click="() => fetchNextPage()"
이렇게 작성해주면 되고 fetchNextPage
라는 예약된 메서드를 사용해주면 된다.
그럼 각 페이지 별로 순서대로 refetch 된다.
이때 cache 에서 제거된다면 페이지가 초기 상태에서 다시 시작된다.
useInfiniteQuery
에서 반환된 refetch
에 refetchPage
함수를 전달할 수 있다.
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const { refetch } = useProjectsInfiniteQuery();
// 첫 페이지만 다시 가져오기
refetch({ refetchPage: (page, index) => index === 0 });
queryClient.refetchQueries, queryClient.invalidateQueries 또는 queryClient.resetQueries의 두 번째 인자(queryFilters)로도 전달할 수 있다.
refetchPage: (page: TData, index: number, allPages: TData[]) => boolean
이때 refetch 가 완료되면 bool 값을 반환한다.
const fetchProjects = ({ pageParam = 0 }) =>
fetch("/api/projects?cursor=" + pageParam);
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const { fetchNextPage } = useProjectsInfiniteQuery();
// 사용자 지정 페이지 파라미터 전달
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 });
기본적으로 getNextPageParam
에서 반환된 변수를 쿼리 함수에 쓰지만 , fetchNextPage
함수에 사용자 지정 변수를 전달하여 기본 변수를 덮어쓸 수 있다.
양방향 목록은 getPreviousPageParam
, fetchPreviousPage
, hasPreviousPage
및 isFetchingPreviousPage
속성과 함수를 사용하여 구현할 수 있다.
useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
});
select
옵션을 사용하면 된다.
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
});
}
첫 페이지 빼기
queryClient.setQueryData("projects", (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}));
개별 페이지에서 특정 값을 제거
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId)
) ?? [];
queryClient.setQueryData("projects", (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}));
마치 데이터가 있는 것 처럼 표시해주는 옵션이다.
다만, initialData 옵션과 다른점은 Placeholder Data 는 캐시되지 않는다는 점이다.
예를들어 블로그 개시물을 가져오는데 useQuery가 전체 데이터를 가져오는 동안 콘텐츠 레이아웃을 가능한 빨리 보여주는 데 유용하다.
placeholder 데이터 작성법
useQueyr 에 placeholderData 를 제공한다.
function useTodosQuery() {
const placeholderTodos = [...];
return useQuery("todos", () => fetch("/todos"), {
placeholderData: placeholderTodos,
});
}
const { data, isLoading } = useTodosQuery();
Prefetch
혹은 queryClient
를 사용하여 데이터를 패치하여 제공한다.
function useBlogPostQuery(blogPostId) {
return useQuery(
["blogPost", blogPostId],
() => fetch(`/blogPosts/${blogPostId.value}`),
{
placeholderData: () => {
// Use the smaller/preview version of the blogPost from the 'blogPosts' query as the placeholder data for this blogPost query
return queryClient
.getQueryData("blogPosts")
?.find((d) => d.id === blogPostId.value);
},
}
);
}
const { data, isLoading } = useBlogPostQuery(blogPostId);
이 Imperatively 한 방법은 뭔가 query 를 또 날리는 것 같지만 사실 캐시에서 가져오는 것이다.
이니셜 쿼리에 데이터를 넣어주는 방법
참고로 이니셜 데이터는 placeholderData 와 다르게 캐싱되기 때문에 불완전한 데이터를 사용하면 안 된다.
캐시를 채우기 위해 선언적으로 이니셜 데이터 제공.
만약 변하지 않는 최초 쿼리 결과를 갖고 있다면 이 방법을 사용하면 좋다.
그래서 값을 세팅해주고 로딩 status 를 건너뛸 수 있다.
const initialTodosUpdatedTimestamp = new Date().getTime();
function useTodosQuery() {
const initialTodos = [...];
return useQuery("todos", () => fetch("/todos"), {
initialData: initialTodos,
staleTime: 1000,
initialDataUpdatedAt: initialTodosUpdatedTimestamp // eg. 1608412420052
});
}
const { data, isLoading } = useTodosQuery();
이니셜 데이터역시 staleTime이 존재한다.
그래서 staleTime 옵션을 따로 주지 않으면 즉시 refect 된다. => 그럼 placeholder 와 크게 다른 점이 없을 것이다.
따라서 staleTime 을 주면 cache 와 유사하게 동작한다. 다만, interaction 이 있다면 그때 다시 refetch 를 한다.
하지만 만약 interaction 이 없다면 어떻게 될까? (staleTime 이 지난 상태로 메모리만 잡아먹고 있는 상태면 어떻게 될까?)
그래서 우리는 initialDataUpdatedAt
옵션을 고려해볼 수 있다.
그래서 이로 하여금 initialDataUpdatedAt
옵션값이 staleTime 을 지났으면 다음 interaction 때 cache 가 아니라 바로 query 를 해오는 것이다.
queryClient.prefetchQuery
를 사용하여 데이터 prefetch
queryClient.setQueryData
를 사용하여 데이터를 캐시에 저장
function useTodoQuery(todoId) {
return useQuery(["todo", todoId], () => fetch(`/todos/${todoId.value}`), {
// Use a todo from the 'todos' query as the initial data for this todo query
initialData: queryClient.getQueryData("todos")?.find((d) => d.id === todoId.value);
});
}
캐시된 데이터에서 값을 꺼내올 수 있다.
function useTodoQuery(todoId) {
return useQuery(["todo", todoId], () => fetch(`/todos/${todoId.value}`), {
initialData: queryClient
.getQueryData("todos")
?.find((d) => d.id === todoId.value),
initialDataUpdatedAt: queryClient.getQueryState("todos")?.dataUpdatedAt,
});
}
그리고 당연하게도 여기서 initialDataUpdatedAt
옵션은 값을 새로 넣는 것이 아닌 기존의 값을 넣여야된다.
만약 이니셜 데이터를 캐시로부터 가져오는데 이게 오랜 시간동안 interactive 를 하지 않다 유효하지 않는 캐시인지 판단하기 위해 queryClient.getQueryState
를 사용하여 간단하게 로직을 구현할 수 있다.
아래 예제 코드는 state.dataUpdateAt 타임스탬프
에서 값을 가져와서 유효한지 확인 후 이니셜 데이터를 지정한 코드이다.
function useTodoQuery(todoId) {
const getInitialData = () => {
// Get the query state
const state = queryClient.getQueryState("todos");
// If the query exists and has data that is no older than 10 seconds...
if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) {
// return the individual todo
return state.data.find((d) => d.id === todoId);
}
// Otherwise, return undefined and let it fetch from a hard loading state!
};
return useQuery(["todo", todoId], () => fetch(`/todos/${todoId.value}`), {
initialData: getInitialData(),
});
}
const prefetchTodos = async () => {
// 이 쿼리의 결과는 평소와 같이 캐시 됨.
await queryClient.prefetchQuery("todos", fetchTodos, { staleTime: 5000 });
};
만약 이미 하당 query 가 캐시된 상태이고 또 유효하다면 따로 query 되진 않는다.
예를 들어 위 예제 코드처럼 staleTime 이 5 초를 넘었다면 유요하지 않기 때문에 qeury 를 해온다.
그리고 이 "todos" 라는 쿼리에 대항 useQuery 가 없다면 지정된 caheTime 이후 가비지 컬렉션 된다.
만약 쿼리에 대한 데이터를 이미 같고 있다면 prefetching 을 사용할 필요가 없다.
그냥 queryClient
객체의 setQueryData
함수를 사용하여 직접 쿼리 캐시 결과를 키로 추가하거나 업데이트 할 수 있다.
queryClient.setQueryData("todos", todos);
쿼리 값들이 stale 되기 전에 refetch 되도록 계속 기다리는 것은 바보같은 짓이다.
따라서 invalidateQueries
함수는 지정된 쿼리의 데이터가 더 이상 최신이 아니라고 판단될 때 사용한다.
그래서 특정 쿼리 또는 쿼리 그룹의 캐시된 데이터를 무효화함으로써, Vue Query가 해당 데이터를 자동으로 다시 가져오도록 강제할 수 있다.
// Invalidate every query in the cache
queryClient.invalidateQueries();
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries(["todos"]);
그래서 쿼리가 invalidateQueries
에 들어가면 2가지가 실행된다.
stale
마크르르 남김, 그리고 staleTime 을 그냥 덮어씌움.위 코드 블럭에서 보면 알 수 있듯, 정확히 "todos" 와 일치한 쿼리를 중단시키는 것이 아닌, 그냥 "todo" 가 들어간 것도 아닌,
"todos" 로 시작하는 쿼리들을 모조리 중단시킨다.
import { useQuery, useQueryClient } from "vue-query";
// Get QueryClient from the context
const queryClient = useQueryClient();
queryClient.invalidateQueries(["todos"]);
// Both queries below will be invalidated
const todoListQuery1 = useQuery(["todos"], fetchTodoList);
const todoListQuery2 = useQuery(["todos", { page: 1 }], fetchTodoList);
따라서 아래 todoListQuery1 과 todoListQuery2 는 모두 중단된다.
하지만 만약 특정 쿼리만 중단시키고 싶을 땐 2가지 방법이 있다.
queryClient.invalidateQueries(["todos", { type: "done" }]);
// The query below will be invalidated
const todoListQuery = useQuery(["todos", { type: "done" }], fetchTodoList);
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery(["todos"], fetchTodoList);
queryClient.invalidateQueries(["todos"], { exact: true });
// The query below will be invalidated
const todoListQuery = useQuery(["todos"], fetchTodoList);
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery(["todos", { type: "done" }], fetchTodoList);
만약 조건을 더 까다롭게 세분화 하고 싶다면 아래와 같이 query 예약어를 사용하면 Query
인스턴스가 넘어가는데 이를 활용하여 bool 값을 반환하는 함수를 만들어주면 된다.
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "todos" && query.queryKey[1]?.version >= 10,
});
// The query below will be invalidated
const todoListQuery = useQuery(["todos", { version: 20 }], fetchTodoList);
// The query below will be invalidated
const todoListQuery = useQuery(["todos", { version: 10 }], fetchTodoList);
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery(["todos", { version: 5 }], fetchTodoList);
15 Query Invalidation
에 쿼리 무효화는 사실 절반만 아는 것이고 이 무효화 "시기"를 아는 것이 더 중하다.
바로 값이 바뀌고(mutation
) 나서 해당 값을 업데이트를 해줘야한다.
예를들어 "todo" 가 들어간 쿼리를 post(delete, update) 하여 DB 에 값이 바뀌었으면 캐시값도 이제 유효하지 않기 때문에 모두 바꿔줘야하는데 이때 아래 예제 코드처럼 작성할 수 있다.
import { useMutation, useQueryClient } from "vue-query";
const queryClient = useQueryClient();
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
queryClient.invalidateQueries(["reminders"]);
},
});
사실 vue query 를 번역, 공부하며 느낀 점이 너무 많은 한 사용자에게 너무 많은 자원을 할당하는 듯 보였다.
뭐만하면 http req 를 날리니 분명 서버 개발자분이 안 좋아할게 뻔히 보였다.(물론 캐시로 최적화를 잘 하면 되지만) 그래서 이를 그나마 최소화 하는 것이 setQueryData
이다.
어차피 우리는 추가, 삭제 한 값을 알고있기 때문에 invalidateQueries
를 사용하지 않고 해당 상태값에서 추가 혹은 삭제를 해주면 된다.
const useMutateTodo = () => {
const queryClient = useQueryClient();
return useMutation(editTodo, {
// Notice the second argument is the variables object that the `mutate` function receives
onSuccess: (data, variables) => {
queryClient.setQueryData(["todo", { id: variables.id }], data);
},
});
};
앞서 낙관적 업데이트 대해 언급한 적이 있다. fetch 가 성골할 것이라고 판단하고 그냥 UI 업데이트를 진행한다는 건데 문제는 이게 실패할 수 있다는 것이다. 그랬을 땐 UI 를 rollback 해줘야하는데 이를 하기 위해선 muation
의 onMutate
를 사용해주면 된다.
그리고 onError 와 onSettled 에 이전 값(context
)를 전달할 수 있는데 여기서 이전의 값을 꺼내서 작성해주면 된다.
const queryClient = useQueryClient();
useMutation(updateTodo, {
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(["todos"]);
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(["todos"]);
// Optimistically update to the new value
queryClient.setQueryData(["todos"], (old) => [...old, newTodo]);
// Return a context object with the snapshotted value
return { previousTodos };
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries(["todos"]);
},
});
그리고 하나만 업데이트 하려거나 onSettled 에 묶어서 하려면
useMutation(updateTodo, {
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(["todos", newTodo.id]);
// Snapshot the previous value
const previousTodo = queryClient.getQueryData(["todos", newTodo.id]);
// Optimistically update to the new value
queryClient.setQueryData(["todos", newTodo.id], newTodo);
// Return a context with the previous and new todo
return { previousTodo, newTodo };
},
// If the mutation fails, use the context we returned above
onError: (err, newTodo, context) => {
queryClient.setQueryData(
["todos", context.newTodo.id],
context.previousTodo
);
},
// Always refetch after error or success:
onSettled: (newTodo) => {
queryClient.invalidateQueries(["todos", newTodo.id]);
},
});
useMutation(updateTodo, {
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
});