๋ฌธ์ œ

Tanstack Query๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ชจ๋“  ์‚ฌ๋žŒ๋“ค์ด ํ•œ ๋ฒˆ์”ฉ์€ ๊ฒช๋Š” ๋ถˆํŽธํ•จ์ด๋‹ค.

์ด ์ฟผ๋ฆฌํ‚ค๋ฅผ ๋‹ค ๊ธฐ์–ตํ•ด๋†”์•ผ ํ•ด?

  // ๋ฐ์ดํ„ฐ ์กฐํšŒ (useQuery)
  const {
    data: users,
    isLoading,
    error,
    isError,
    refetch
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 5 * 60 * 1000, // 5๋ถ„๊ฐ„ fresh ์ƒํƒœ ์œ ์ง€
    cacheTime: 10 * 60 * 1000, // 10๋ถ„๊ฐ„ ์บ์‹œ ์œ ์ง€
    retry: 3, // ์‹คํŒจ์‹œ 3๋ฒˆ ์žฌ์‹œ๋„
    refetchOnWindowFocus: false, // ์œˆ๋„์šฐ ํฌ์ปค์Šค์‹œ refetch ๋น„ํ™œ์„ฑํ™”
  });

์‹ค์ œ๋กœ ์šฐ๋ฆฌ๋Š” Tanstack Query๋ฅผ ํ†ตํ•ด์„œ ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ๊ด€๋ฆฌํ•  ๋•Œ, ๋ฌธ์ž์—ด์„ ๊ทธ๋Œ€๋กœ ๋„ฃ๊ธฐ๋„ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Š” ์ ์  ๊ด€๋ฆฌํ•˜๋Š” ์ฟผ๋ฆฌ ํ‚ค๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๋งŽ์•„์งˆ์ˆ˜๋ก ๋‚ด๊ฐ€ ์–ด๋–ค ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ์ผ๋Š”์ง€๋„ ๊นŒ๋จน๊ณ  ๋‹ค๋ฅธ ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์œผ๋ฉฐ, ์‹ฌํ•œ ๊ฒฝ์šฐ ๋ฉ”๋ชจ์žฅ์— ์‚ฌ์ดํŠธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ๋กํ•ด ๋†“๋“ฏ์ด ๋”ฐ๋กœ ๊ธฐ๋กํ•ด๋†“๊ธฐ๋„ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์˜ ๊ฒฝ์šฐ ์ ์  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ๊ฐ€ ๋ถˆํŽธํ•ด์ง„๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์šฐ์•„ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์„๊นŒ?

https://tkdodo.eu/blog/effective-react-query-keys Tanstack-query์˜ ๋ฉ”์ธํ…Œ์ด๋„ˆ์ธ Dominik๋ผ๋Š” ๋ถ„์€ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ์— ๋Œ€ํ•ด์„œ Query Key๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๋Š” Query Key Factory๋ฅผ ์ œ์‹œํ•œ๋‹ค.

const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
}

์ด๋ ‡๊ฒŒ ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ์ž‘์„ฑํ•  ๊ฒฝ์šฐ, ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ํ•œ๋ฒˆ์— ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, fuzzy matching์„ ํ•˜๋Š” tanstack query ์˜ ์„ฑ๊ฒฉ์— ๋งž๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

TanStack Query์—์„œ์˜ Fuzzy Matching๋ž€

TanStack Query์—์„œ fuzzy Matching์€ ์˜๋ฏธ ๊ทธ๋Œ€๋กœ โ€˜ํ๋ฆฟํ•œ ๋งค์นญโ€™์ด๋‹ค. ๋ฌด์กฐ๊ฑด ๋˜‘๊ฐ™์€ ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด์„œ ๋งค์นญํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ํ๋ฆฟํ•˜๊ฒŒ ๋ณด๊ณ  ๊ฐ™์•„๋ณด์ด๋Š” ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด์„œ๋Š” ๋งค์นญ์‹œํ‚ค๋Š” ๊ตฌ์กฐ์ด๋‹ค. ๋ฐ˜๋Œ€ ๊ฐœ๋…์œผ๋กœ exact matching(์ •ํ™•ํ•œ ๋งค์นญ) ์ด ์žˆ๋‹ค.

ํฌ๋ฏธํ•˜๋‹ค๋Š” ์˜๋ฏธ๊ฐ€ ๋ฌด์Šจ ์˜๋ฏธ์ธ์ง€ ๊ฐ์ด ์ œ๋Œ€๋กœ ์˜ค์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ๋‹ค. ์ด๋Ÿด ๋•Œ๋Š” ๋ถ€๋ถ„์ ์ธ ๊ณตํ†ต์  ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ๋งค์นญ์ •๋„๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ดํ•ด๊ฐ€ ํŽธํ•˜๋‹ค.

์•ž์— ์˜ค๋Š˜ ๊ณตํ†ต์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ์žˆ๋‹ค๋ฉด, ๊ทธ ๋’ค์˜ ์ฟผ๋ฆฌ๋“ค ๋˜ํ•œ ๋ชจ๋‘ ๋งค์นญ์— ๊ฑธ๋ฆฌ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋œ๋‹ค.

// ๋ถ€๋ถ„ ๋งค์นญ (TanStack Query ๊ธฐ๋ณธ ๋ฐฉ์‹)
['user'] โ†’ ['user', 1], ['user', 1, 'profile'] ๋งค์น˜๋จ
 
// ์ •ํ™•ํ•œ ๋งค์นญ
['user', 1] โ†’ ์ •ํ™•ํžˆ ['user', 1]๋งŒ ๋งค์น˜๋จ
 
// ์กฐ๊ฑด๋ถ€ ๋งค์นญ
predicate: (query) => query.state.isStale

๋‹ค๋ฅธ ๋ฐฉ์‹๋„ ์žˆ์ง€๋งŒ, ์ด๋ฒˆ์—” ํŠนํžˆ fuzzy Matching์— ๋Œ€ํ•ด์„œ query key factory๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

Query Key Factory๋กœ ๋ณด์™„ํ•˜๊ธฐ

๋‚˜ ๋˜ํ•œ Query Key Factory๊ฐ€ ํ•œ ๊ณณ์—์„œ ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ›จ์”ฌ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ์ข‹์œผ๋ฉฐ, ๋งŒ์•ฝ์— ๋ชจ๋“  ์ฟผ๋ฆฌ๋“ค์„ ์ดˆ๊ธฐํ™”์‹œํ‚ค๊ณ  ์‹ถ์„ ๋•Œ ์œ„ ์˜ˆ์‹œ์˜ all() ์ด๋‚˜ list(filter) ์™€ ๊ฐ™์ด ํŠน์ • ์ฟผ๋ฆฌ ํ‚ค ๋ฒ”์œ„์— ๋”ฐ๋ผ์„œ invalidate ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋กœ์ง ๋˜ํ•œ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฐฉ์‹์„ ์ฑ„ํƒํ–ˆ๋‹ค.

const interviewKeys = {
  all: ["interview"],
  byInterviewId: (id: number): QueryKey => [...interviewKeys.all, id],
  byInterviewIdAndQuestionId: (id: number, questionId: number) =>
    [...interviewKeys.byInterviewId(id), questionId],
};

๊ทธ๋Ÿผ ๋‚ด๊ฐ€ ํ˜„์žฌ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š” Interview๋ผ๋Š” ๋„๋ฉ”์ธ์— ๋Œ€ํ•ด์„œ ํ‚ค๋Š” ์ด๋ ‡๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์ฟผ๋ฆฌ ํ‚ค๋Š” ํƒ€์ž…์— ๋Œ€ํ•ด์„œ ์ •ํ™•ํžˆ ๋ช…์‹œ๋˜์–ด์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฐํŠธ ์—๋Ÿฌ๊ฐ€ ๋–ด๋‹ค. ๋”ฐ๋ผ์„œ ๊ธฐ์กด์˜ ์ฟผ๋ฆฌ ํ‚ค ํŒฉํ† ๋ฆฌ๋ฅผ ์กฐ๊ธˆ ๊ณ ์น˜๋ฉด์„œ ํƒ€์ž…์„ ๋ณด์™„ํ–ˆ๋‹ค.

์ฟผ๋ฆฌ ํ‚ค ๋งค์นญ๊ณผ ํƒ€์ž… ์•ˆ์ •์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ

type QueryKey = readonly (string | number)[];
 
type QueryKeyFactory<T> = {
  readonly all: QueryKey;
} & {
  [K in keyof T]: T[K] extends (...args: any[]) => QueryKey
    ? (...args: Parameters<T[K]>) => QueryKey
    : QueryKey;
};
 
type InterviewMethods = {
  byInterviewId: (id: number) => QueryKey;
  byInterviewIdAndQuestionId: (id: number, questionId: number) => QueryKey;
};
const interviewKeys: QueryKeyFactory<InterviewMethods> = {
  all: ["interview"] as const,
  byInterviewId: (id: number): QueryKey => [...interviewKeys.all, id] as const,
  byInterviewIdAndQuestionId: (id: number, questionId: number): QueryKey =>
    [...interviewKeys.all, id, questionId] as const,
};

์ฟผ๋ฆฌ ํ‚ค์˜ ๊ฒฝ์šฐ๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ all์€ readonly๋กœ ํ•˜์˜€๊ณ , stringํ˜น์€ number ํƒ€์ž…๋งŒ ์“ฐ๋‹ˆ Query Key์˜ ๊ฒฝ์šฐ๋Š” union ํƒ€์ž…์œผ๋กœ ํ•ด์ฃผ๊ณ , QueryKeyFactory ์˜ ๊ฒฝ์šฐ์—๋Š” ์ œ๋„ค๋ฆญ์„ ๋ฐ›๋„๋ก ํ•˜์˜€๋‹ค. ์ œ๋„ค๋ฆญ์€ ํ•œ ๊ฐ์ฒด๋กœ ๋˜์–ด ์žˆ์œผ๋ฉฐ, T[K] ๊ฐ€ ํ•จ์ˆ˜๋กœ ๋˜์–ด ์žˆ์œผ๋ฉด ๊ทธ๋ƒฅ ๊ทธ๋Œ€๋กœ ๋‚ด๋ณด๋‚ด๊ณ , ํ•ด๋‹น ๊ฐ์ฒด์˜ ํ‚ค๊ฐ’๊ณผ ํ•ด๋‹น ํ‚ค๊ฐ’์— ๋Œ€์‘๋˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„์„œ QueryKey๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํƒ€์ž…์„ ๋ณด์™„ํ–ˆ๋‹ค.