๐Ÿ’ฅ 03. ์—๋Ÿฌ ํ•ธ๋“ค๋ง๊ณผ ์ „์—ญ ์ฝœ๋ฐฑ(Global Callbacks) ์•„ํ‚คํ…์ฒ˜

2026๋…„ 3์›” 4์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

๋กœ์ปฌ try-catch์˜ ํ•œ๊ณ„๋ฅผ ๋„˜์–ด์„œ, QueryCache ๋‹จ์œ„์˜ ์ „์—ญ ์—๋Ÿฌ ํ•ธ๋“ค๋ง๊ณผ React Error Boundary ๊ฒฐํ•ฉ์„ ํ†ตํ•œ ์ตœ์ƒ์˜ ์—๋Ÿฌ ๋ฐฉ์–ด๋ง(Defensive UX) ๊ตฌ์ถ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ

"์˜์ฒ  ๋‹˜, ๋งŒ์•ฝ ์œ ์ € ํ† ํฐ์ด ๊ฐ“ ๋งŒ๋ฃŒ๋˜์–ด์„œ ํ„ฐ์ง„ '401 Unauthorized API' ํ˜ธ์ถœ์ด 1์ดˆ ์‚ฌ์ด์— 5๊ฐœ๊ฐ€ ๋™์‹œ๋‹ค๋ฐœ์ ์œผ๋กœ ๋Œ์•„๊ฐ”๋‹ค๋ฉด, ๋ธŒ๋ผ์šฐ์ €์— '๋กœ๊ทธ์ธ ํ•ด์ฃผ์„ธ์š”' ํ† ์ŠคํŠธ ์•Œ๋ฆผ์„ 5๊ฐœ ๋™์‹œ์— ๋„์šฐ๊ณ  ์œ ์ €ํ•œํ…Œ ์š•๋จน์„ ๊ฑด๊ฐ€์š”?"

โ˜•๏ธ ์˜์ฒ ์ด์˜ ๊ณ ๋ฏผ: "์—๋Ÿฌ ํ† ์ŠคํŠธ ์ฐฝ์ด ํ™”๋ฉด์„ ๋‹ค ๋ฎ์–ด๋ฒ„๋ ค์š”!"

(๋ชฉ์š”์ผ ์˜คํ›„, ํ™”๋ฉด์„ ๋นผ๊ณกํžˆ ๋ฎ์€ ๋นจ๊ฐ„์ƒ‰ ์‹คํŒจ ์•Œ๋žŒ ์ฐฝ๋“ค์— ์‹์€๋•€์„ ํ˜๋ฆฌ๋Š” ์˜์ฒ )

๐Ÿฃ ์˜์ฒ : ์ €๊ธฐ... ๐Ÿฆ ๋ฆฌ๋“œ ๋‹˜. ์ œ๊ฐ€ ๊ฐ useQuery ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐฉ์–ด ๋กœ์ง์„ ์ฐธ ์ž˜ ์งœ๋†จ๊ฑฐ๋“ ์š”?

const { data, isError, error } = useQuery(['user'], fetchUser);
 
useEffect(() => {
  // ๐Ÿ’ฅ ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ๋…๋‹จ์ ์ธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
  if (isError) {
    if (error.response.status === 401) {
      toast.error("ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธ ํ•ด ์ฃผ์„ธ์š”.");
      router.push('/login');
    }
  }
}, [isError, error]);

๊ทผ๋ฐ ๊ฐ‘์ž๊ธฐ ๋ฐฑ์—”๋“œ(์˜์ˆ˜) ๋‹˜์ด ์ž ์‹œ ๋ฐฐํฌํ•œ๋‹ค๊ณ  ์„œ๋ฒ„๋ฅผ 10์ดˆ ๋‚ด๋ ธ๊ฑฐ๋“ ์š”? ๊ทธ ์‚ฌ์ด์— ์ œ ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์— ์žˆ๋˜ 10๊ฐœ์˜ ์œ„์ ฏ(๊ฐ๊ฐ ๋‹ค๋ฅธ API ํ˜ธ์ถœ ์ค‘)์ด ์ผ์ œํžˆ ์‘๋‹ต์„ ๋ชป ๋ฐ›๋”๋‹ˆ, ์—๋Ÿฌ ํ† ์ŠคํŠธ(์•Œ๋ฆผ์ฐฝ)๊ฐ€ ์šฐ๋‹ค๋‹ค๋‹ค ์—„์ฒญ๋‚˜๊ฒŒ ํŒŒ๋ฐ”๋ฐ• ๋œจ๋ฉด์„œ ํ™”๋ฉด์„ ๋ฒŒ๊ฒ‹๊ฒŒ ๋ฎ์–ด๋ฒ„๋ ธ์–ด์š” ใ… ใ… .

๐Ÿฆ ์˜ํ˜ธ: ์˜์ฒ  ๋‹˜... API ํ•˜๋‚˜ํ•˜๋‚˜์˜ ์•„์ฃผ ๋„ํŠธ๋จธ๋ฆฌ์ธ ๋กœ์ปฌ(Local) ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ์—์„œ ์ €๋ ‡๊ฒŒ "์ธ์ฆ ์—๋Ÿฌ"๋‚˜ "500 ์„œ๋ฒ„ ๋ถ•๊ดด ์—๋Ÿฌ" ๊ฐ™์€ ์น˜๋ช…์ ์ธ ์ƒํƒœ๋ฅผ ๋‹ค๋ฃจ๋ ค๊ณ  ํ•˜๋‹ˆ๊นŒ 10์ค‘ ํญ๋ฐœ์ด ์น˜๋Š” ๊ฒ๋‹ˆ๋‹ค.
์ด๋Ÿฐ ์ค‘๋Œ€ํ•œ ๊ณตํ†ต ์—๋Ÿฌ๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€(๋กœ์ปฌ)์—์„œ ์žก๋Š” ๊ฒŒ ์•„๋‹™๋‹ˆ๋‹ค. ์–ด์ฐจํ”ผ ๋ชจ๋“  ๋„คํŠธ์›Œํฌ ๋น„๋™๊ธฐ ์—๋Ÿฌ๊ฐ€ ๊ท€๊ฒฐ๋˜์–ด ์Ÿ์•„์ง€๋Š” ์ตœ์ƒ๋‹จ ์ค‘์•™ ๋Œ, ๋ฐ”๋กœ QueryClient์˜ ์บ์‹œ(Cache) ์ „์—ญ ์ฝœ๋ฐฑ ๋‹จ์—์„œ ํ†ตํ•ฉ ํƒ€๊ฒฉ์„ ๊ฐ€ํ•ด์•ผ์ฃ .


๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: ๋Œ์„ ์–ด๋””์— ๊ฑด์„คํ•  ๊ฒƒ์ธ๊ฐ€?

์šฐ๋ฆฌ๋Š” ๊ทธ๋™์•ˆ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ๋งˆ๋‹ค useQuery ํ˜น์€ useMutation ์„ค์ • ๊ฐ์ฒด์˜ onError ์ฝœ๋ฐฑ์ด๋‚˜ isError ์ƒํƒœ ๋ณ€์ˆ˜๋งŒ ๋ฐ”๋ผ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ด๊ฑด "์ž์‹ ์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ์ง€ํ‚ค๋Š” ์ž‘์€ ์šฐ์‚ฐ"์ž…๋‹ˆ๋‹ค.

  • ๋ฌธ์ œ์  1: ์˜์ฒ ์ด์˜ ์‚ฌ๋ก€์ฒ˜๋Ÿผ A ์ปดํฌ๋„ŒํŠธ, B ์ปดํฌ๋„ŒํŠธ, C ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™์‹œ์— 401 ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚ฌ์„ ๋•Œ, ์ฝ”๋“œ ์ค‘๋ณต์ด ๋”์ฐํ•˜๊ฒŒ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.
  • ๋ฌธ์ œ์  2: useMutation({ onError: ... }) ์•ˆ์ชฝ์— ๋กœ๊ทธ์ธ์„ ํŠ•๊ตฌ๋Š” ๋กœ์ง์„ ๋„ฃ์–ด๋‘๋ฉด? ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋‚˜์ค‘์— ๋˜‘๊ฐ™์€ ๋กœ๊ทธ์ธ ์—ฐ๋™ Mutation์„ ๋งŒ๋“ค ๋•Œ๋งˆ๋‹ค ๋ฐฉ์–ด ์ฝ”๋“œ๋ฅผ "๋ณต๋ถ™"ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ๊นŒ๋จน์œผ๋ฉด? ๊ณง์žฅ ๋ณด์•ˆ ๋ฒ„๊ทธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. (TkDodo ์•„ํ‹ฐํด #11 ์ฐธ์กฐ)

์ง„์งœ ์„ฑ์ˆ™ํ•œ 5๋…„ ์ฐจ์˜ ํ”„๋ก ํŠธ์—”๋“œ ๋ผ๋ฉด, ๊ฐœ๋ณ„ ์ปดํฌ๋„ŒํŠธ์˜ ์šฐ์‚ฐ์„ ๋‹ค ๋ฒ„๋ฆฌ๊ณ  QueryCache ์™€ MutationCache ๋ผ๋Š” ๊ฐ€์žฅ ๋†’์€ ํ•˜๋Š˜์— ์ „์ฒœํ›„ ๋ฐฉ์–ด ๋”(์ „์—ญ ์ฝœ๋ฐฑ) ์„ ์น˜๋Š” ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ธํŒ…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


1. ์ตœ์ƒ๋‹จ ์ˆ˜๋ฌธ์žฅ: Global Cache Callbacks ์„ธํŒ…

React Query v5(App Router) ํ™˜๊ฒฝ์˜ QueryClient ์ƒ์„ฑ ํŒฉํ† ๋ฆฌ ์ฝ”๋“œ๋กœ ๋Œ์•„๊ฐ€ ๋ด…์‹œ๋‹ค(Basic 09๊ฐ• ์ฐธ์กฐ). ์—ฌ๊ธฐ์— ์ง„์ •ํ•œ ์˜๋ฏธ์˜ ๋ฐฉ์–ด ์‹œ์Šคํ…œ์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

// ๐Ÿ“ app/providers/get-query-client.ts
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';
import { toast } from 'react-hot-toast'; // ๊ธ€๋กœ๋ฒŒ ํ† ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜ˆ์‹œ
 
function makeQueryClient() {
  return new QueryClient({
    // ๐Ÿš€ [1] ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” Query(GET) ์ „์—ญ ์—๋Ÿฌ ์ œ์–ด
    queryCache: new QueryCache({
      // ์ง€๊ตฌ์ƒ์˜ ์–ด๋–ค useQuery ํ›…์—์„œ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง€๋“  ๊ฒฐ๊ตญ ์ด๊ณณ์œผ๋กœ ํ•œ ๋ฒˆ ๋ถˆ๋ ค์˜ต๋‹ˆ๋‹ค.
      onError: (error, query) => {
        // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ๊ท€(Refetch) ์ค‘์— ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” ์งœ์ฆ ๋‚˜๋‹ˆ๊นŒ ์‚ด์ง ๋ฌด์‹œํ•˜๊ณ ...
        if (query.state.data !== undefined) {
          toast.error(`์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`);
          return;
        }
        
        // ์น˜๋ช…์ ์ธ 401, 500 ์ „์—ญ ๋ผ์šฐํŒ… ํ•ธ๋“ค๋ง ๋กœ์ง์€ ์˜ค๋กœ์ง€ ์ด ํ•œ ๊ณณ์— ์‘์ง‘์‹œํ‚ด!
        if (error.status === 401) {
          toast.error('์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
          window.location.href = '/login'; // ๊ธ€๋กœ๋ฒŒ ๋กœ๊ทธ์ธ ๊ฐ•์ œ ์ด๋™ (๋‹จ 1ํšŒ ํญ๋ฐœ)
        }
      },
    }),
 
    // ๐Ÿš€ [2] ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๊พธ๋Š” Mutation(POST/DELETE) ์ „์—ญ ์—๋Ÿฌ ์ œ์–ด
    mutationCache: new MutationCache({
      onError: (error, _variables, _context, mutation) => {
        // ํ•ด๋‹น Mutation์„ ํ˜ธ์ถœํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ "๋‚˜ ์Šค์Šค๋กœ onError ์ฒ˜๋ฆฌ ์ž˜ ํ•ด๋†จ์–ด์š”!" 
        // ๋ผ๊ณ  ์„ ์–ธํ•œ ๊ฒฝ์šฐ(meta ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด)์—๋Š” ์ „์—ญ ํ† ์ŠคํŠธ๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ  ๋กœ์ปฌ์— ๋งก๊น€ 
        if (mutation.meta?.bypassGlobalError) return;
 
        toast.error(`์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`);
      }
    }),
  });
}

์ด์ œ ์ „ ์„ธ๊ณ„ ์ปดํฌ๋„ŒํŠธ 100๊ตฐ๋ฐ์—์„œ ํผ์ ธ์žˆ๋˜ 401 ์ธ์ฆ ํŠ•๊น€ ๋กœ์ง์ด๋‚˜ isError ์žก๋™์‚ฌ๋‹ˆ ์ฝ”๋“œ๋“ค์ด ์ˆœ์‚ญ ์ฆ๋ฐœํ•ฉ๋‹ˆ๋‹ค. UI ๊ฐœ๋ฐœ์ž๋Š” ๊ทธ์ € "ํ™”๋ฉด์— ๋ฐ์ดํ„ฐ ์˜ˆ์˜๊ฒŒ ๊ทธ๋ฆฌ๊ธฐ" ์—๋งŒ ์ง‘์ค‘ํ•˜๊ฒŒ ๋˜์ฃ !


2. Error Boundary์˜ ์•„๋ฆ„๋‹ค์šด ๊ฒฐํ•ฉ (Declarative UX)

๐Ÿฃ ์˜์ฒ : ์•„ ์ „์—ญ ์ฒ˜๋ฆฌ ์ฉŒ๋„ค์š”! ๊ทผ๋ฐ ๋กœ์ปฌ ์ชฝ์—์„  "๋ฐ์ดํ„ฐ ์ž์ฒด๊ฐ€ ๋ฐ•์‚ด๋‚˜์„œ isError === true ์ผ ๋•Œ, ์•„์˜ˆ ํ™”๋ฉด ๋Œ€์‹  '๋นจ๊ฐ„์ƒ‰ ์—๋Ÿฌ ๋‚ฌ์–ด ํž๊ตฌ' ์ปดํฌ๋„ŒํŠธ(Fallback UI)๋ฅผ ๊ทธ๋ ค์ค˜์•ผ" ํ•  ๋•Œ๋„ ์žˆ์ž–์•„์š”. ๊ทธ๊ฑด ๊ฒฐ๊ตญ ๊ฐœ๋ณ„ ์ปดํฌ๋„ŒํŠธ์—์„œ if (isError) return <div>์—๋Ÿฌ๋‚จ</div> ์จ์•ผ ํ•˜๋Š” ๊ฑฐ ์•„๋‹Œ๊ฐ€์š”?

๐Ÿฆ ์˜ํ˜ธ: ๋„ค, ๋งž์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ปดํฌ๋„ŒํŠธ ์•ˆ์ชฝ์— if (isError) ๋„๋ฐฐ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ์ง“๋„ ๋”์ฐํ•˜๊ธด ๋งคํ•œ๊ฐ€์ง€์ฃ . ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„ ๋•Œ "UI๋ฅผ ๋ฌด์—‡์œผ๋กœ (์„ ์–ธ์ ์œผ๋กœ) ๋ Œ๋”๋ง ํ•  ๊ฒƒ์ธ๊ฐ€"๋Š” React ๋ณธ์—ฐ์˜ ๊ฐ“-๊ธฐ๋Šฅ์ธ Error Boundary ์—๊ฒŒ ์œ„์ž„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

React Query ์˜ต์…˜ ์ค‘ throwOnError: true (v5 ๋ณ€๊ฒฝ์ , ์˜ˆ์ „์—” useErrorBoundary) ๋ฅผ ์ผœ๋ฉด, ํ›… ๋‚ด๋ถ€์—์„œ ์žก๊ณ  ์žˆ๋˜ ๊ผฌ๋ฆฌ๊ฐ€ React์˜ ๊ธฐ๋ณธ ์—๋Ÿฌ ์‹œ์Šคํ…œ์œผ๋กœ ํƒ! ๋˜์ ธ์ง‘๋‹ˆ๋‹ค(throw).

export const todoOptions = {
  detail: (id: number) => queryOptions({
    queryKey: ['todos', id],
    queryFn: fetchTodoDetail,
    // ๐Ÿ”ฅ ๋งˆ๋ฒ•์˜ ์ง€์‹œ์–ด: "์—๋Ÿฌ ๋‚˜๋ฉด ๋‚ด๊ฐ€(isError) ์ฅ๊ณ  ์žˆ์ง€ ์•Š๊ณ  ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ๋กœ ํญํƒ„ ๋˜์งˆ๊ฒŒ!"
    throwOnError: true,
  }),
};

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, ์ด์ œ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์†Œ๋น„ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๋Š” ๋ถˆ์ˆœ๋ฌผ์ด 1g๋„ ์•ˆ ์„ž์ธ "์ˆœ์ˆ˜ ๋ฐ์ดํ„ฐ ์ •์ƒ ๋ Œ๋”๋ง ์ฝ”๋“œ" ๋กœ ๊ทนํ•œ์˜ ์บก์Аํ™”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
 
// ํ™”๋ฉด ์ตœ์ƒ๋‹จ ๋˜๋Š” ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ ๋‹จ
function Page() {
  return (
    // ํ„ฐ์ง„ ํญํƒ„์„ ์บ์น˜ํ•ด์„œ ์˜ˆ์œ "๋‹ค์‹œ ์‹œ๋„" ์—๋Ÿฌ ์นด๋“œ ๊ทธ๋ ค์ฃผ๋Š” ์šฐ์‚ฐ โ˜‚๏ธ
    <ErrorBoundary fallback={<ErrorFallbackCard />}>
      <Suspense fallback={<Spinner />}>
        {/* ๐ŸŽ‰ ์ด ๋…€์„ ๋‚ด๋ถ€์—” ์˜จ๊ฐ– ์˜ˆ์™ธ ์ œ์–ด๋ฌผ(if isLoading, if isError)์ด ์•„์˜ˆ ์—†์Šต๋‹ˆ๋‹ค! */}
        <PureTodoDetail id={1} />
      </Suspense>
    </ErrorBoundary>
  )
}

์ด ํŒจํ„ด์ด ๋ฐ”๋กœ ์„ ์–ธ์  ๋น„๋™๊ธฐ UX์˜ ์ •์ , ์ด๋ฅธ๋ฐ” "Suspense + Error Boundary + React Query" ๋Œ€ํ†ตํ•ฉ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์™€์”จ... ์•„๊นŒ ๋ฐฑ์—”๋“œ 401 ํ„ฐ์กŒ์„ ๋•Œ ๋‚ด ํ™”๋ฉด์— ๋ป˜๊ฑด ํ† ์ŠคํŠธ ์•Œ๋ฆผ์ฐฝ 12๊ฐœ ์šฐ๋ฅด๋ฅด ์Ÿ์•„์ง„ ๊ฑฐ ์ƒ๊ฐํ•˜๋ฉด ์–ด์ง€๋Ÿฌ์šด๋ฐ, ์ด๊ฑธ ์ตœ์ƒ๋‹จ QueryCache ๋กœ ๋„˜๊ธฐ๋‹ˆ๊นŒ 12๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ผ ๊น”๋”ํ•˜๊ฒŒ ํ•œ ๋ฒˆ ๋”ฑ ๊ฑธ๋Ÿฌ์„œ ๋กœ๊ทธ์ธ ์ฐฝ ํŠ•๊ฒจ์ฃผ๋Š” ๊ฑฐ ๋ฌด๋น™ ์ง€๋ ธ๋‹ค...

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "๋กœ์ปฌ ํ›…(onError, isError)์—์„œ ๋ชจ๋“  ๋˜ฅ์„ ๋‹ค ์น˜์šฐ๋ ค๊ณ  ํ•˜์ง€ ๋งˆ๋ผ. ์น˜๋ช…์ ์ธ ์ธ์ฆ ์—๋Ÿฌ๋‚˜ 500 ํ„ฐ์ง์€ ๊ฐ€์žฅ ์œ„์ชฝ ์ „์—ญ ์บ์‹œ(QueryCache, MutationCache) ๋Œ์œผ๋กœ ๋ชฐ์•„๋ฒ„๋ฆฌ๊ณ  (๋กœ์ง ๊ฒฉ๋ฆฌ), ๋กœ์ปฌ UI ์—๋Ÿฌ ํ™”๋ฉด์€ ํญ๋ฐœ๋ฌผ ๋˜์ง€๊ธฐ(throwOnError: true) ๋กœ Error Boundaryํ•œํ…Œ ์งฌ์ฒ˜๋ฆฌํ•˜์ž!"

๊ฒฐ๊ตญ ์‹œ๋‹ˆ์–ด ์˜ํ˜ธ ๋‹˜์ด ํ•ญ์ƒ ์งœ๋ฅด์น˜๋˜ "๋„ˆ์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ๊ฑธ ์•Œ๊ณ  ์žˆ๊ฒŒ(๊ฒฐํ•ฉ๋„ ๋†’๊ฒŒ) ํ•˜์ง€ ๋งˆ๋ผ"๋Š” ์ฒ ํ•™์ด ์ƒํƒœ ๊ด€๋ฆฌ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์—๋Ÿฌ ๋ฐฉ์–ด๋ง(Defensive UX) ์„ค๊ณ„์—๋„ ๋˜‘๊ฐ™์ด ํ†ตํ•˜๋Š” ๊ฑฐ์˜€๋‹ค. ์†Œ๋ฆ„ ๋‹๋Š”๋‹ค ์ฆŒ์ฏ”... ์˜ค๋Š˜์€ ๋งฅ์ฃผ ๋ง๊ณ  ์—์Šคํ”„๋ ˆ์†Œ ๋งˆ์‹œ๊ณ  ์ด ์ฒ ํ•™์„ ๋‡Œ์— ๊ฐ์ธ ์‹œ์ผœ์•ผ์ง€. โ˜•๏ธ


๐Ÿ“ ๋ฐฐ์šด ๋‚ด์šฉ ์ ๊ฒ€ํ•˜๊ธฐ (Quiz)

Q. ์ „์—ญ MutationCache ์•ˆ์— ํ† ์ŠคํŠธ ์‹คํŒจ ์•Œ๋ฆผ(onError)์„ ๋‹ฌ์•„๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ฐœ๋ฐœ ์ค‘, ์–ด๋А ํ•œ ํŠน์ • ๋ฒ„ํŠผ(๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š” ์ทจ์†Œ) ๋งŒํผ์€ ์ „์—ญ ์—๋Ÿฌ ํŒ์—…์„ ๋„์šฐ์ง€ ์•Š๊ณ  ์กฐ์šฉํžˆ ๋’ท๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์Šคํƒ€์ผ๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์‹ถ์–ด์กŒ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ํŒฉํ† ๋ฆฌ ์ „์—ญ ์ฝ”๋“œ๋ฅผ ๋”๋Ÿฝํžˆ์ง€(์ˆ˜์ •ํ•˜์ง€) ์•Š๊ณ , ํ•ด๋‹น ๋กœ์ปฌ ์ปดํฌ๋„ŒํŠธ ์ชฝ์˜ Mutation์—์„œ ์ „์—ญ ์ฝœ๋ฐฑ์„ "์šฐํšŒ(Bypass)" ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์•Œ๋ ค์ฃผ๋Š” ๊ฐ€์žฅ ๊ถŒ์žฅ๋˜๋Š” React Query ๋ฌธ๋ฒ•/์˜ต์…˜์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

  • A) ํ›… ์˜ต์…˜์— ignoreGlobalError: true ๊ฐ™์€ ์ž์ฒด ๋‚ด์žฅ ์˜ต์…˜์„ ๊ฑด๋‹ค.
  • B) ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ์ „์—ญ ์บ์‹œ์— ๋ฐ•ํžŒ ๋กœ์ง์€ ๋ฌด์กฐ๊ฑด ์ง€๊ตฌ์ƒ ๋ชจ๋“  ํ›…์— ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋˜๋ฏ€๋กœ ์šฐํšŒํ•  ์ˆ˜ ์—†๋‹ค.
  • C) useMutation ({ meta: { bypassGlobalToast: true } }) ์ฒ˜๋Ÿผ ์ปค์Šคํ…€ ์ธ๊ณ„ ๋ฉ”ํƒ€๊ฐ์ฒด(meta) ์•ˆ์— ํ‚ค ๊ฐ’์„ ๋‹ฌ์•„, ์ „์—ญ ์บ์‹œ๊ฐ€ ๊ทธ ๊ฐ’์„ ์ฝ๊ณ  ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

โœ… ์ •๋‹ต: C

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: React Query์˜ useQuery ์™€ useMutation ์—๋Š” ์•„์ฃผ์•„์ฃผ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ์œ ์ €๋“ค์ด ์ž˜ ๋ชจ๋ฅด๋Š” ๋งˆ๋ฒ•์˜ ๋’ท๊ตฌ๋ฉ์ธ meta ๊ฐ์ฒด๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค(TkDodo ์•„ํ‹ฐํด #11 ์—ญ์„ค). ์ด meta ๊ฐ์ฒด์˜ ํ‚ค์™€ ๊ฐ’๋“ค์€ ์˜ค์ง ์ •๋ณด ์ „๋‹ฌ ์šฉ๋„๋กœ๋งŒ ์“ฐ์ด๋ฉฐ, ์ตœ์ƒ๋‹จ ๊ธ€๋กœ๋ฒŒ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ์ธ (QueryCache/MutationCache ์˜ ์ฝœ๋ฐฑ)์˜ 4๋ฒˆ์งธ ์ธ์ž๋กœ ๊ณ ์Šค๋ž€ํžˆ ๋ฐฐ๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ๊ธ€๋กœ๋ฒŒ ํ•ธ๋“ค๋Ÿฌ๋Š” ์ด meta ๊ผฌ๋ฆฌํ‘œ๋ฅผ ์ฝ์–ด๋ณด๊ณ  "์•„! ์ด Mutation์€ ๊ธ€๋กœ๋ฒŒ ํ† ์ŠคํŠธ๋ฅผ ๋„์šฐ์ง€ ๋ง๋ผ๊ณ  ๋ถ€ํƒํ–ˆ๊ตฌ๋‚˜! ๋ฌด์‹œํ•˜์ž~" ๋ผ๊ณ  ์Šค๋งˆํŠธํ•˜๊ฒŒ ์šฐํšŒ ๋ถ„๊ธฐ๋ฅผ ํƒœ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: "์˜์ฒ  ๋‹˜, A๋ฒˆ์ฒ˜๋Ÿผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์—†๋Š” ๋ง˜๋Œ€๋กœ ์ง€์–ด๋‚ธ ์ด๋ฆ„ ๋„ฃ์œผ๋ฉด TS ์—๋Ÿฌ ๋‚˜๊ณ  ํ„ฐ์ ธ์š”! ์˜ค๋กœ์ง€ ๊ฐœ๋ฐœ์ž ์ „์šฉ ํ†ต์‹  ๊ทœ์•ฝ์ธ meta ์˜ต์…˜์„ ํ†ตํ•ด์„œ๋งŒ ํ›… ๊นŠ์ˆ™ํ•œ ์•ˆ์ชฝ์—์„œ๋ถ€ํ„ฐ ์ตœ์ƒ๋‹จ ์ „์—ญ ์บ์‹œ ๋Œ€๊ธฐ๊ถŒ ๋ฐ”๊นฅ์ชฝ์œผ๋กœ ๋ฌด์ „(signal)์„ ๋•Œ๋ฆด ์ˆ˜ ์žˆ๋Š” ๊ฒ๋‹ˆ๋‹ค."
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ๋กœ์ปฌ ํ›…์ด ์ „์—ญ ๋Œ ํ†ต์ œ์‹ค์— ๋ชฐ๋ž˜ ๊ท“์†๋ง์„ ๋ณด๋‚ด๋Š” ์œ ์ผํ•œ ๋’ท๊ตฌ๋ฉ, meta ์˜ต์…˜!