Post

React Query Key Factory로 QueryKey 관리하기

현재 개인 프로젝트는 외부 api 요청을 tanstack-query를 이용하여 요청하고 있었습니다. 이에 따라 tanstack-query 공식 문서를 보면서 어떤 새로운 기능을 찾아서 리팩토링할까 고민하던 중 공식 문서의 COMMUNITY RESOURCES 탭에 있는 Query Key factory 라는 라이브러리를 찾게 되었는데 이 도구로 query key를 효율적으로 관리할 수 있을 것 같아 오늘은 Query Key Factory에 대한 간단한 소개와 사용법, 그리고 개인 프로젝트에 어떻게 적용했는지를 소개해 보겠습니다.

이 글은 2024-06-11 에 업데이트 되었습니다.

이 글은 HTML, CSS , JavaScript, TypeScript, React, Tanstack-query에 대한 기본적인 지식을 알고 있어야 합니다.

Query Key factory 란

Query Key factorytanstack-query와 함께 사용되는 라이브러리로, tanstack-query 사용하는 query key를 효율적으로 생성하고 관리할 수 있도록 도와줍니다.

주요 기능

  • 자동 Query Key 생성: 객체와 메소드 체인을 사용하여 일관되고 직관적인 방식으로 - query key를 생성할 수 있습니다.
  • 가독성 향상: 명확하고 구조화된 방식으로 query key를 정의하여 코드의 가독성을 높입니다.
  • 유지보수 용이: query key의 생성을 중앙 집중식으로 관리하여, 변경 사항을 쉽게 적용할 수 있습니다. 설치

기본 사용법

우선 터미널에서 tanstack-queryquery-key-factory를 설치합니다.

1
2
3
4
5
# tanstack query
npm i @tanstack/react-query

# query-key-factory
npm install @lukemorales/query-key-factory

쿼리 작성하기

그리고 쿼리 키를 작성 할 파일에서 query-key-factory를 이용하여 쿼리를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
import { createQueryKeyStore } from "@lukemorales/query-key-factory";

export const queries = createQueryKeyStore({
  users: {
    all: null,
    detail: (userId: string) => ({
      queryKey: [userId],
      queryFn: () => api.getUser(userId),
    }),
  },
});

한 곳에 머지하기

또한 각각 따로 작성한 파일을 한 곳에서 merge 하는 것도 가능합니다.

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
32
33
34
35
36
import {
  createQueryKeys,
  mergeQueryKeys,
} from "@lukemorales/query-key-factory";

// queries/users.ts
export const users = createQueryKeys("users", {
  all: null,
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
  }),
});

// queries/todos.ts
export const todos = createQueryKeys("todos", {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
    queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
    contextQueries: {
      search: (query: string, limit = 15) => ({
        queryKey: [query, limit],
        queryFn: (ctx) =>
          api.getSearchTodos({
            page: ctx.pageParam,
            filters,
            limit,
            query,
          }),
      }),
    },
  }),
});
// queries/index.ts
export const queries = mergeQueryKeys(users, todos);

사용하기

이제 위의 query-factory에서 hook을 사용하여 간단하게 꺼내 쓰면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { queries } from "../queries";
import { useQuery } from "@tanstack/react-query";

export function useUsers() {
  return useQuery({
    ...queries.users.all,
    queryFn: () => api.getUsers(),
  });
}

export function useUserDetail(id: string) {
  return useQuery(queries.users.detail(id));
}
1
2
3
4
5
6
7
8
// components/UserList.tsx
import { useQuery } from "@tanstack/react-query";
import { useUsers } from "../useUser";

export function UserList() {
  const { data, error, isLoading } = useUsers();
  // 컴포넌트 코드...
}

응용 사용법

컨텍스트 기반 쿼리 사용

컨텍스트 기반 쿼리를 사용하면 동시에 2가지의 쿼리를 가져올 수 있습니다. 예를 들어 특정 사용자의 상세 정보와 그 사용자가 좋아하는 항목을 동시에 가져오는 경우는 아래와 같이 작성이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// queries/users.ts
export const users = createQueryKeys("users", {
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
    contextQueries: {
      likes: {
        queryKey: null,
        queryFn: () => api.getUserLikes(userId),
      },
    },
  }),
});

// components/UserLikes.tsx
import { useQuery } from "@tanstack/react-query";
import { queries } from "../queries";

export function UserLikes({ userId }) {
  const { data, error, isLoading } = useQuery(
    queries.users.detail(userId)._ctx.likes
  );
  // 컴포넌트 코드...
}

쿼리키 무효화

추가로 쿼리 키를 무효화 하려면 아래와 같이 하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { queries } from "../queries";

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation(updateUser, {
    onSuccess(updatedUser) {
      // 업데이트된 사용자의 상세 정보 무효화
      queryClient.invalidateQueries(
        queries.users.detail(updatedUser.id).queryKey
      );

      // 사용자 목록 무효화
      queryClient.invalidateQueries(queries.users.all.queryKey);
    },
  });
}

이 밖에도 다양한 사용법이 있으니 공식 문서에 가서 확인해 보시길 바랍니다.

query-key-factory 공식 문서 바로가기

간단한 프로젝트 만들기

이제 간단한 기본 사용법을 알았으니 진짜 실제로 되는지 한번 테스트 해 보겠습니다. 간단하게 json 파일을 만들어 랜덤 유저 데이터를 넣고 실제로 api를 작성해 보았습니다.

code sandbox 로 api 요청을 하면 cors 에러가 발생하기 때문에 (예전에는 잘 됐는데..) 간단한 json파일 데이터를 생성하였습니다.

api 요청 함수 만들기

간단하게 불러오는 api 파일을 작성해 줍니다.

1
2
3
4
5
6
7
8
9
export const api = {
  getUsers: async () => {
    // 사용자 목록을 가져오는 API 호출
    const response = await fetch("/users.json");
    console.log(response);
    const data = await response.json();
    return data;
  },
};

query factory 만들기

이제 query factory 에 api를 등록합니다.

1
2
3
4
5
6
7
8
9
10
11
import { createQueryKeyStore } from "@lukemorales/query-key-factory";
import { api } from "./api";

export const queries = createQueryKeyStore({
  users: {
    all: {
      queryKey: ["users"],
      queryFn: () => api.getUsers(),
    },
  },
});

hook 만들기

마지막으로 useUser hook을 만든후 app.tsx에서 데이터를 불러오면 아주 쉽게 쿼리 키를 등록하실 수 있습니다.

1
2
3
4
5
6
7
8
9
// useUser hook
import { useQuery } from "@tanstack/react-query";
import { queries } from "./query";

export function useUsers() {
  return useQuery({
    ...queries.users.all,
  });
}
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
32
33
34
35
36
37
38
39
40
41
import "./styles.css";
import { useUsers } from "./useUser";

export default function App() {
  const { data, error, isLoading, isError } = useUsers();

  if (isLoading) {
    return <h1>Loading...</h1>;
  }

  if (isError) {
    return <h1>Something Error!...</h1>;
  }

  console.log(data);

  return (
    <div className="App">
      <h1>Hello Wrold</h1>
      <h2>get random user using query key factory</h2>

      <ul
        style=
      >
        {data.results.map((el) => (
          <li
            style=
            key={el.cell}
          >
            <img
              style=
              src={el.picture.thumbnail}
              alt="avatar"
            />
            {`${el.name.title} ${el.name.first} ${el.name.last}`}
          </li>
        ))}
      </ul>
    </div>
  );
}

CodeSandbox 에서 확인하기

모든 코드는 CodeSandbox에 작성되어 있습니다. 직접 확인하면서 변경해 보세요

개인 프로젝트에 적용

저의 개인 프로젝트에서는 아래의 코드처럼 query-key 들을 관련된 query key를 생성할 때 바로 위에서 객체를 만들어 적용 했었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const liveQueryKey = {
  useLiveMathesQuery: "footballHomeLiveMathesQuery",
};

export const useLiveMathesQuery = (leagueId: number | null) => {
  return useQuery({
    queryKey: [liveQueryKey.useLiveMathesQuery, leagueId],
    queryFn: ({ queryKey }) => getLiveMatches(queryKey[1] as number),
    enabled: !!leagueId,
    staleTime: 60000,
    gcTime: 60000,
  });
};

export const useLiveLineUpQuery = (fixtureId?: number) => {
  return useQuery({
    queryKey: [liveQueryKey.useLiveMathesQuery, fixtureId],
    queryFn: ({ queryKey }) => getLineUp(queryKey[1] as number),
    enabled: !!fixtureId,
    staleTime: 60000,
    gcTime: 60000,
  });
};

하지만 리팩토링 이후에는 아래의 코드처럼 query key와 function을 따로 관리할 수 있고 코드 네이밍도 훨씬 읽기 쉽게 만들 수 있어서 가독성과 유지보수가 더 좋아진 것 같았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// live.ts
import { createQueryKeys } from "@lukemorales/query-key-factory";
import { getLineUp, getLiveMatches } from "../apis/live";

export const lives = createQueryKeys("lives", {
  live: (leagueId: number) => ({
    queryKey: [leagueId],
    queryFn: () => getLiveMatches(leagueId),
  }),

  lineup: (fixtureId: number) => ({
    queryKey: [fixtureId],
    queryFn: () => getLineUp(fixtureId),
  }),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// use-live-query.ts
import { queries } from "./../services/quries/index";
import { useQuery } from "@tanstack/react-query";

export const useLiveMathesQuery = (leagueId: number | null) => {
  return useQuery({
    ...queries.lives.lineup(leagueId!),
    enabled: !!leagueId,
    staleTime: 60000,
    gcTime: 60000,
  });
};

export const useLiveLineUpQuery = (fixtureId?: number) => {
  return useQuery({
    ...queries.lives.lineup(fixtureId!),
    enabled: !!fixtureId,
    staleTime: 60000,
    gcTime: 60000,
  });
};

또한 news 페이지 에서는 무한 스크롤을 쓰고 있었기 때문에 ctx를 통해 pageParam을 가져와 적용해 주었습니다.

1
2
3
4
5
6
7
8
9
10
11
// news.ts
export const news = createQueryKeys("news", {
  global: (query: string, filter?: string) => ({
    queryKey: [query, filter],
    queryFn: (ctx) => getGlobalNews(query, ctx.pageParam as number, filter),
  }),
  local: (query: string) => ({
    queryKey: [query],
    queryFn: (ctx) => getNaverNews(query, ctx.pageParam as number),
  }),
});
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
32
33
34
35
36
37
// use-news-query

export const useGlobalNewsQuery = (
  query: string,
  isUse: boolean,
  filter?: string
) => {
  return useInfiniteQuery({
    ...queries.news.global(query, filter),
    initialPageParam: 1,
    enabled: !!query && !!isUse,
    getNextPageParam: (lastPage, pages) => {
      if (lastPage.status === "ok" && lastPage.articles.length > 0) {
        return pages?.length + 1;
      }
      return undefined;
    },
    select(data) {
      return data.pages.flatMap((data) => data.articles);
    },
  });
};

export const useLocalNewsQuery = (query: string, isUse: boolean) => {
  return useInfiniteQuery({
    ...queries.news.local(query),
    initialPageParam: 1,
    enabled: !!query && !!isUse,
    getNextPageParam: (lastPage) => {
      const nextPage = lastPage.start + 1;
      return lastPage.items.length === 0 ? undefined : nextPage;
    },
    select(data) {
      return data.pages.flatMap((data) => data.items);
    },
  });
};

마지막으로 나머지 코드들도 index.ts 파일을 생성하여 모두 머지시켜주고 사용할 때 queries를 따로 불러와서 사용했기 때문에 매우 편리하였습니다.

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
// index.ts
import { mergeQueryKeys } from "@lukemorales/query-key-factory";
import { banners } from "./banner";
import { leagues } from "./league";
import { lives } from "./live";
import { matchResults } from "./match-result";
import { news } from "./news";
import { predicts } from "./predict";
import { players } from "./player";
import { ranks } from "./rank";
import { schedules } from "./schedules";
import { searches } from "./search";
import { teams } from "./team";

export const queries = mergeQueryKeys(
  banners,
  leagues,
  lives,
  matchResults,
  news,
  predicts,
  players,
  ranks,
  schedules,
  searches,
  teams
);

결론

이번 포스트에서는 @lukemorales/query-key-factory 라이브러리를 활용하여 React Query에서 쿼리 키를 효율적으로 관리하는 방법을 알아보았습니다. 이를 통해 저는 코드의 가독성을 높이고, 쿼리 키의 재사용성을 극대화할 수 있었습니다.

이 라이브러리는 특히 대규모 프로젝트에서 쿼리 키를 체계적으로 관리하고, 중복 코드를 줄이며, 유지보수성을 높이는 데 큰 도움이 될 것 같습니다. 앞으로 프로젝트에서 React Query를 사용할 때, 이 라이브러리를 적극 활용하여 더욱 효율적인 쿼리 키 관리를 해 보겠습니다.

This post is licensed under CC BY 4.0 by the author.