Vue Query Guides & Concepts (작성중..)

강정우·2024년 5월 1일
2

vue.js

목록 보기
72/72
post-thumbnail
post-custom-banner

1. Query Keys

vue query 는 일반적으로 query keys 를 바탕으로 쿼리 캐싱을 관리한다.
query keys 는 그냥 문자열 도 가능하고 배열도 가능하다.
일단 serializable 하고 고유하다면 사용가능하다.

// 할 일 목록
useQuery(['todos'], ...)

// 그 밖의 다른 것, 무엇이든!
useQuery(['something', 'special'], ...)

convention

보통은 이런식으로 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 한 값을 넣어주면 된다.

Query Keys 는 결정적으로 해쉬된다.

키 순서에 상관이 없다는 것이다. 다만, array 는 순서에 상관이 있기 때문에

 useQuery(['todos', status, page], ...)
 useQuery(['todos', page, status], ...)
 useQuery(['todos', undefined, page, status], ...)

위 query key 는 각각 다른 키가 된다는 것이다.

2. Query Functions

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

QueryFunctionContext 로는

queryKey

string, number, array, etc...

pageParam

unknown | undefined

  • Infinite Queries 에서만 쓰인다.
    page parameter 는 현재 페이지를 fetching 하기 위해 쓰인다.

signal?

옵셔널로 사용된다. query 를 중간에 취소할 때 사용한다.

meta?

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 };
  },
};

Query function 변수

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 Object

물론 Query function 에서 파라미터로 넘겨도 되지만 Obj 로도 사용가능하다는 것을 알려주고 있다.

import { useQuery } from "vue-query";

useQuery({
  queryKey: ["todo", 7],
  queryFn: fetchTodo,
  ...config,
});

3. Network Mode

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

가장 기본 모드이며 이 online 모드일 때만 query, mutation 이 동작한다.
그리고 status 는 항상 loading, error, success 중 하나로 존재하고
fetstatus 는 fetching, paused, idle 중 하나로 존재한다.

  • 참고로 로딩 스피너를 표시하기 위해 로딩 상태만 확인하는 것으로 충분하지 않을 수 있다.
    치초 마운트할 때 네트워크 연결이 없어 status가 '로딩' 상태이지만 fetchStatus가 '일시 중지'인 경우가 있을 수 있기 때문이다.

이 네트워크 모드가 Online 일 때 쿼리가 시행되지만 fetching 가 진행되는 중에 offline 이 되면 retry 메커니즘을 비롯한 qeury 가 pause 된다.
그리고 다시 네트워크가 연결되면 다시 재실행 되는데 이는 refetchOnReconnect 와는 개념이 조금 다르다.
취소(cancel)가 아니라 중지(pause)이기 때문이다.

always

이 상태는 뭐 offline 이든 online 이든 가리지 않고 항상 fetch 를 진행한다.

이 모드는 쿼리가 작동할 때 네트워크 연결이 필요하지 않은 환경에서 사용한다.
예를들어 AsyncStorage 에서 값을 읽기만 한다거나 하니면 그냥 Promise 객체를 사용하고 싶을 때 사용한다.

네트워크 연결이 없다고 해서 query 가 중지(paused) 되진 않는다.
재시도(retry) 도 중지(pause)되지 않고 계속 조지고 실패하면 error status 가 된다.
refetchOnReconnect 는 false 로 설정된다. 네트워크에 다시 연결됐다고 해서 다시 refetch 를 하는 것이 좋은 것은 아니기 때문이다. 다만, 원한다면 true 로 옵션을 바꿀 순 있다.

offlineFirst

이 모드는 첫 번째 두 옵션 사이의 중간 선택지 이다.
여기서 Vue Query는 queryFn을 한 번 실행하지만, 그 후에 재시도를 일시 중지한다.

이는 오프라인 우선 PWA와 같이 서비스 워커가 요청을 캐싱하기 위해 가로채거나, Cache-Control 헤더를 통한 HTTP 캐싱을 사용하는 경우에 매우 유용하다.

이러한 상황에서는 처음 패칭은 오프라인 저장소/캐시에서 오기 때문에 성공할 가능성이 있다. 하지만 만약 캐시가 누락되었다면 바로 fail 로 간주하고 온라인 쿼리처럼 동작한다. 이때 retry 는 중지된다.

서비스 워커
서비스 워커(Service Worker)는 웹 애플리케이션, 특히 프로그레시브 웹 앱(PWA, Progressive Web App)에서 중요한 역할을 하는 웹 기술이다. 서비스 워커는 브라우저서버 사이에서 프록시 서버 역할을 하며, 웹 애플리케이션을 보다 빠르고 신뢰성 있게 만드는 데 도움을 준다.

1. 오프라인 경험 개선: 서비스 워커는 네트워크 요청을 가로채고 캐싱을 통해 오프라인에서도 콘텐츠를 제공할 수 있게 한다.
이를 통해 네트워크 연결이 불안정하거나 없는 환경에서도 사용자에게 일관된 경험을 제공할 수 있다.

2. 백그라운드 동기화: 네트워크 연결이 다시 확립되었을 때, 서비스 워커를 이용해 백그라운드에서 데이터를 동기화할 수 있다.
이를 통해 애플리케이션은 최신 상태를 유지할 수 있다.

3. 푸시 알림: 서비스 워커는 웹 애플리케이션에서 푸시 알림을 구현할 수 있게 해준다.
이를 통해 사용자가 애플리케이션을 사용하지 않을 때도 중요한 정보를 전달할 수 있다.

4. 성능 향상: 캐싱 전략을 통해 자주 사용되는 리소스를 로컬에 저장하고 빠르게 로드할 수 있어 애플리케이션의 로딩 시간을 단축시킬 수 있다.

서비스 워커의 작동 방식은 비교적 복잡하며, 사용하기 위해서는 HTTPS 환경이 필요하다.
웹 애플리케이션의 root 디렉토리에 서비스 워커 파일을 등록하고, 이를 통해 네트워크 요청을 가로채고 관리한다.
서비스 워커는 웹 애플리케이션의 생명주기와 별개로 동작하며, 웹 페이지가 닫혀 있더라도 백그라운드에서 활동할 수 있다.

4. Parallel Queries (병렬 쿼리)

동시에 쿼리를 날리는 것이다.

만약 동시에 날릴 쿼리의 수가 정해져 있는 경우엔 굉장히 간단하다.
그냥 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})

5. 종속 쿼리

직렬 쿼리로도 변역할 수 있겠다. 즉, 쿼리가 실행되기 이전 선행되어야하는 쿼리가 있어야한다.

// 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";

6. 로딩 indicator

앞서 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 로 표시된다.

7. Window Focus Refetching

앞서 일정 시간이 지난 데이터는 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 });

Custom 하기

만약 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 을 사용해야할 때도 있다. 이때 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 가 될 수 있다는 것을 유의해야한다.

8. 쿼리 비활성화(Disabling Queries)

자동으로 실행되는 쿼리를 비활성화하고 싶을 때는, enabled = false 옵션을 사용할 수 있다.

  • 쿼리에 캐시된 데이터가 있는 경우: 쿼리는 status === success 또는 isSuccess 상태로 초기화.
    쿼리에 캐시된 데이터가 없는 경우: 쿼리는 status === idle 또는 isIdle 상태로 시작.

  • 또 false 일 때,
    쿼리는 마운트 시 자동으로 데이터를 가져오지 않음.
    새 인스턴스가 마운트되거나 나타날 때 쿼리가 백그라운드에서 자동으로 다시 가져오지 않음.
    쿼리는 일반적으로 쿼리를 다시 가져오게 만들 query client의 invalidateQueries 및 refetchQueries 호출을 무시함.
    refetch를 사용하여 수동으로 쿼리가 데이터를 가져오도록 트리거할 수 있음.

lazy queries

<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 옵션으로 최초 로드시에만 실행할 수 있도록 할 수 있다.

9. Query Retries

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
});

10. Paginated Queries

사실 페이지네이션을 바탕으로 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 옵션

keepPreviousData 옵션은 useInfiniteQuery hook 과 함께 작동하여, 무한 쿼리 키가 시간이 지남에 따라 변경되더라도 사용자가 캐시된 데이터를 계속 볼 수 있도록 해주는 옵션을 제공한다.

11. Infinite Queries

버튼이라든지 아니면 무한 스크롤을 구현할 때 사용하는 쿼리이다.

fetchNextPagefetchPreviousPage 함수를 사용할 수 있다.
getNextPageParamgetPreviousPageParam 옵션을 사용할 수 있다.

hasNextPage 불리언이 사용 가능하다.
getNextPageParamundefined가 아닌 값을 반환하면 true이다.

hasPreviousPage 불리언이 사용 가능하다.
getPreviousPageParamundefined가 아닌 값을 반환하면 true이다.

백그라운드 새로 고침 상태와 더 많은 상태를 로딩하는 것 사이를 구별하기 위해 isFetchingNextPageisFetchingPreviousPage 불리언이 사용 가능하다.

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 라는 예약된 메서드를 사용해주면 된다.

만약 stale 된다면?

그럼 각 페이지 별로 순서대로 refetch 된다.
이때 cache 에서 제거된다면 페이지가 초기 상태에서 다시 시작된다.

일부 페이지만 refetch 하고 싶다면?

useInfiniteQuery에서 반환된 refetchrefetchPage 함수를 전달할 수 있다.

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, hasPreviousPageisFetchingPreviousPage 속성과 함수를 사용하여 구현할 수 있다.

useInfiniteQuery("projects", fetchProjects, {
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
});

reverse 옵션은?

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,
}));

12. Placeholder Query Data

마치 데이터가 있는 것 처럼 표시해주는 옵션이다.
다만, initialData 옵션과 다른점은 Placeholder Data 는 캐시되지 않는다는 점이다.

예를들어 블로그 개시물을 가져오는데 useQuery가 전체 데이터를 가져오는 동안 콘텐츠 레이아웃을 가능한 빨리 보여주는 데 유용하다.

placeholder 데이터 작성법

1. 선언적

useQueyr 에 placeholderData 를 제공한다.

function useTodosQuery() {
  const placeholderTodos = [...];

  return useQuery("todos", () => fetch("/todos"), {
    placeholderData: placeholderTodos,
  });
}

const { data, isLoading } = useTodosQuery();

2. 명령적

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 를 또 날리는 것 같지만 사실 캐시에서 가져오는 것이다.

13. Initial Query Data

이니셜 쿼리에 데이터를 넣어주는 방법

참고로 이니셜 데이터는 placeholderData 와 다르게 캐싱되기 때문에 불완전한 데이터를 사용하면 안 된다.

1. 선언적

캐시를 채우기 위해 선언적으로 이니셜 데이터 제공.

만약 변하지 않는 최초 쿼리 결과를 갖고 있다면 이 방법을 사용하면 좋다.

그래서 값을 세팅해주고 로딩 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 and initialDataUpdatedAt

이니셜 데이터역시 staleTime이 존재한다.
그래서 staleTime 옵션을 따로 주지 않으면 즉시 refect 된다. => 그럼 placeholder 와 크게 다른 점이 없을 것이다.
따라서 staleTime 을 주면 cache 와 유사하게 동작한다. 다만, interaction 이 있다면 그때 다시 refetch 를 한다.

하지만 만약 interaction 이 없다면 어떻게 될까? (staleTime 이 지난 상태로 메모리만 잡아먹고 있는 상태면 어떻게 될까?)
그래서 우리는 initialDataUpdatedAt 옵션을 고려해볼 수 있다.

그래서 이로 하여금 initialDataUpdatedAt 옵션값이 staleTime 을 지났으면 다음 interaction 때 cache 가 아니라 바로 query 를 해오는 것이다.

2. 명령적

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);
  });
}

캐시된 데이터에서 값을 꺼내올 수 있다.

initialDataUpdatedAt

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(),
  });
}

14. Prefetching

const prefetchTodos = async () => {
  // 이 쿼리의 결과는 평소와 같이 캐시 됨.
  await queryClient.prefetchQuery("todos", fetchTodos, { staleTime: 5000 });
};

만약 이미 하당 query 가 캐시된 상태이고 또 유효하다면 따로 query 되진 않는다.

예를 들어 위 예제 코드처럼 staleTime 이 5 초를 넘었다면 유요하지 않기 때문에 qeury 를 해온다.

그리고 이 "todos" 라는 쿼리에 대항 useQuery 가 없다면 지정된 caheTime 이후 가비지 컬렉션 된다.

Manually priming a query (쿼리 수동 초기화)

만약 쿼리에 대한 데이터를 이미 같고 있다면 prefetching 을 사용할 필요가 없다.
그냥 queryClient 객체의 setQueryData 함수를 사용하여 직접 쿼리 캐시 결과를 키로 추가하거나 업데이트 할 수 있다.

queryClient.setQueryData("todos", todos);

15. Query Invalidation

쿼리 값들이 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가지가 실행된다.

  1. stale 마크르르 남김, 그리고 staleTime 을 그냥 덮어씌움.
  2. useQuery 를 사용하여 UI 에 렌더링 되고 있거나 아님 hook 과 연관이 있을 때 백그라운드에서 refetch 된다.

삭제 조건

위 코드 블럭에서 보면 알 수 있듯, 정확히 "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가지 방법이 있다.

1. 조건을 더 자세히 주기

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);

2. exact 옵션 사용하기

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);

granularity

만약 조건을 더 까다롭게 세분화 하고 싶다면 아래와 같이 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);

16. Mutation 으로 인한 무효화

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"]);
  },
});

17. Updates from mutation responses

사실 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);
    },
  });
};

18. Optimistic updates

앞서 낙관적 업데이트 대해 언급한 적이 있다. fetch 가 성골할 것이라고 판단하고 그냥 UI 업데이트를 진행한다는 건데 문제는 이게 실패할 수 있다는 것이다. 그랬을 땐 UI 를 rollback 해줘야하는데 이를 하기 위해선 muationonMutate 를 사용해주면 된다.

그리고 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
    }
  },
});

19. Query cancellation

profile
智(지)! 德(덕)! 體(체)!
post-custom-banner

0개의 댓글