{"id":783,"date":"2026-05-18T23:02:08","date_gmt":"2026-05-18T15:02:08","guid":{"rendered":"https:\/\/junai.ai\/blog\/react-suspense-tanstack-17\/"},"modified":"2026-05-19T20:26:25","modified_gmt":"2026-05-19T12:26:25","slug":"react-suspense-tanstack-17","status":"publish","type":"post","link":"https:\/\/junai.ai\/blog\/react-suspense-tanstack-17\/","title":{"rendered":"React Suspense + TanStack Query (Ch.17)"},"content":{"rendered":"\n<!-- WordPress REST API \ubc1c\ud589\uc6a9 HTML (\uc790\ub3d9 \uc0dd\uc131) -->\n<!-- WP-FEATURED-MEDIA-ID: 757 -->\n<div style=\"max-width:800px;margin:0 auto;\">\n<style>\n:root {--color-primary:#0891b2;--color-accent:#06b6d4;--color-bg:#f8fafc;--color-bg-card:#fff;--color-text:#0f172a;--color-text-muted:#64748b;--hero-start:#0f172a;--hero-end:#0891b2;}\n*{box-sizing:border-box;}\n.container{max-width:760px;margin:0 auto;padding:0 24px 80px;}\n.hero{background:linear-gradient(135deg,var(--hero-start) 0%,var(--hero-end) 100%);color:#fff;padding:80px 24px 60px;text-align:center;}\n.hero .eyebrow{display:inline-block;font-size:14px;color:#67e8f9;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:14px;}\n.hero h1{font-size:36px;margin:0 0 16px;line-height:1.3;font-weight:800;}\n.hero p{color:#cffafe;font-size:18px;max-width:640px;margin:0 auto;line-height:1.6;}\n.hero img{width:100%;max-width:640px;height:auto;margin:32px auto 0;border-radius:10px;display:block;}\narticle{padding-top:48px;}\narticle h2{font-size:26px;margin:56px 0 20px;padding-left:14px;border-left:5px solid var(--color-accent);line-height:1.4;}\narticle h3{font-size:19px;margin:32px 0 12px;color:var(--color-primary);}\narticle p{margin:16px 0;}\narticle strong{color:var(--color-primary);font-weight:700;}\narticle code{background:#ecfeff;padding:2px 8px;border-radius:4px;font-family:'SF Mono',Menlo,Consolas,monospace;font-size:14px;color:#0e7490;}\n.databox{background:#ecfeff;border-left:4px solid var(--color-accent);padding:16px 20px;margin:24px 0;border-radius:0 8px 8px 0;font-size:15.5px;}\n.databox strong{color:var(--color-primary);}\n.warnbox{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);padding:16px 20px;margin:24px 0;border-radius:8px;font-size:15.5px;}\n.code-block{background:#0f172a;color:#e2e8f0;padding:16px 20px;border-radius:8px;font-family:'SF Mono',Menlo,Consolas,monospace;font-size:14px;line-height:1.6;margin:20px 0;overflow-x:auto;white-space:pre;}\n.cta{background:linear-gradient(135deg,#0891b2 0%,#06b6d4 100%);color:#fff;padding:28px 24px;border-radius:12px;margin:48px 0 0;text-align:center;}\n.cta h3{color:#fff;margin:0 0 8px;font-size:20px;}\n.cta p{color:#cffafe;margin:0;font-size:15.5px;}\n.footer-nav{margin-top:32px;padding-top:20px;border-top:1px solid #e2e8f0;font-size:14px;color:var(--color-text-muted);}\n.footer-nav a{color:var(--color-primary);text-decoration:none;}\n@media (max-width:480px){.hero h1{font-size:26px;}.hero p{font-size:16px;}article h2{font-size:21px;}article h3{font-size:17px;}body{font-size:16px;}}\n<\/style>\n<section class=\"hero\">\n  <span class=\"eyebrow\">React \uad50\uc7ac \u00b7 \uace0\uae09 17\ud3b8<\/span>\n  <h1>React Suspense + TanStack Query<\/h1>\n  <p>useEffect \uc548\uc758 fetch + loading + error 5\uc904\uc744 \ud55c \uc904\ub85c. \ubaa8\ub358 \ub370\uc774\ud130 fetch \uc758 \ud45c\uc900.<\/p>\n  <img decoding=\"async\" src=\"https:\/\/junai.ai\/blog\/wp-content\/uploads\/2026\/05\/hero-5-40.jpg\" alt=\"\uc2a4\ud53c\ub108\uac00 \ucf58\ud150\uce20\ub85c \ubcc0\ud558\ub294 \ud398\uc774\ub4dc \ud2b8\ub79c\uc9c0\uc158 \uc77c\ub7ec\uc2a4\ud2b8 \u2014 Suspense \ube44\ub3d9\uae30 \ucee8\uc149\">\n<\/section>\n\n<div class=\"container\">\n<article>\n\n<p>11\ud3b8\uc758 <code>useFetch<\/code> \ud328\ud134\uc740 \ud559\uc2b5\uc6a9\uc73c\ub860 \uc88b\uc9c0\ub9cc \uc2e4\uc804\uc5d4 \ubd80\uc871. \uce90\uc2f1 \uc5c6\uc74c \u2192 \uac19\uc740 \ud398\uc774\uc9c0 \ub450 \ubc88 \uc5f4\uba74 \ub450 \ubc88 fetch. \uc911\ubcf5 \ud638\ucd9c \uc5c6\uc74c \u2192 \ucef4\ud3ec\ub10c\ud2b8 3\uac1c\uac00 \uac19\uc740 API \ubd80\ub974\uba74 3\ubc88 \ud638\ucd9c. \uc790\ub3d9 \uc7ac\uc2dc\ub3c4\u00b7refetch\u00b7\ub099\uad00\uc801 \uc5c5\ub370\uc774\ud2b8\ub3c4 \uc5c6\uc74c. \uc774\uac78 \ub2e4 \ud574\uacb0\ud55c \uac8c <strong>TanStack Query<\/strong> (\uc61b React Query) + React 18 \uc758 <strong>Suspense<\/strong>.<\/p>\n\n<p>\uc774\ubc88 17\ud3b8\uc740 TanStack Query \uac00 \uc65c useEffect fetch \uc758 \uc9c4\uc9dc \uc878\uc5c5\uc778\uc9c0 + Suspense \uc640 \uacb0\ud569 + \ud68c\uc0ac stack \ud45c\uc900\uc73c\ub85c \uc790\ub9ac \uc7a1\ub294 \uc774\uc720.<\/p>\n\n<h2>1. TanStack Query \u2014 useFetch \uac00 \ubabb \ud558\ub294 6\uac00\uc9c0<\/h2>\n\n<div class=\"code-block\">npm install @tanstack\/react-query\n\n\/\/ main.tsx \u2014 QueryClientProvider \ud55c \ubc88 \uac10\uc308\nimport { QueryClient, QueryClientProvider } from &#8216;@tanstack\/react-query&#8217;;\nconst queryClient = new QueryClient();\ncreateRoot(&#8230;).render(\n  &lt;QueryClientProvider client={queryClient}&gt;\n    &lt;App \/&gt;\n  &lt;\/QueryClientProvider&gt;\n);\n\n\/\/ \uc0ac\uc6a9 \u2014 useQuery \ud55c \uc904\nimport { useQuery } from &#8216;@tanstack\/react-query&#8217;;\n\nfunction UserProfile({ id }) {\n  const { data, isLoading, error } = useQuery({\n    queryKey: [&#8216;user&#8217;, id],\n    queryFn: () =&gt; fetch(`\/api\/users\/${id}`).then(r =&gt; r.json()),\n  });\n  if (isLoading) return &lt;Spinner \/&gt;;\n  if (error) return &lt;ErrorMsg \/&gt;;\n  return &lt;h1&gt;{data.name}&lt;\/h1&gt;;\n}<\/div>\n\n<p>\ud45c\uba74\uc801\uc73c\ub860 12\ud3b8 <code>useFetch<\/code> \uc640 \ube44\uc2b7. \uc548\uc5d0 \ub4e4\uc5b4\uc788\ub294 6\uac00\uc9c0\uac00 \ub2e4\ub974\ub2e4.<\/p>\n\n<div class=\"databox\">\n<strong>useQuery \uc790\ub3d9 \uc81c\uacf5<\/strong> \u2014 \u2460 \uce90\uc2dc (\uac19\uc740 queryKey \uba74 \uba54\ubaa8\ub9ac\uc5d0\uc11c \uc989\uc2dc \ubc18\ud658). \u2461 \uc911\ubcf5 \uc81c\uac70 (\uac19\uc740 \uc2dc\uc810\uc5d0 \uac19\uc740 key \uc694\uccad 3\uac74 \u2192 1\ubc88\ub9cc \ud638\ucd9c). \u2462 background refetch (\uc0ac\uc6a9\uc790\uac00 \uc708\ub3c4\uc6b0 \ub2e4\uc2dc \ud3ec\ucee4\uc2a4\ud558\uba74 \uc790\ub3d9 \ucd5c\uc2e0\ud654). \u2463 \uc790\ub3d9 \uc7ac\uc2dc\ub3c4 (\uc2e4\ud328 \uc2dc exponential backoff 3\ubc88). \u2464 \ub099\uad00\uc801 \uc5c5\ub370\uc774\ud2b8 (mutate \uc9c1\ud6c4 UI \uba3c\uc800 \ubc18\uc601). \u2465 devtools (\uce90\uc2dc \uc0c1\ud0dc \uc2dc\uac01\ud654). \uc9c1\uc811 \uad6c\ud604\ud558\ub824\uba74 1000+ \ub77c\uc778.\n<\/div>\n\n<h2>2. queryKey \uac00 \ud575\uc2ec \u2014 \uce90\uc2dc \uc2dd\ubcc4\uc790<\/h2>\n\n<p>useQuery \uc758 \uccab \uc778\uc790 <code>queryKey<\/code> \uac00 \ubaa8\ub4e0 \ub9c8\ubc95\uc758 \uae30\ubc18. <strong>\uac19\uc740 key \uba74 \uce90\uc2dc \uacf5\uc720<\/strong>, \ub2e4\ub978 key \uba74 \ub2e4\ub978 \ucffc\ub9ac.<\/p>\n\n<div class=\"code-block\">\/\/ \ub450 \ucef4\ud3ec\ub10c\ud2b8\uac00 \uac19\uc740 \ub370\uc774\ud130 \u2192 API 1\ubc88\ub9cc \ud638\ucd9c\ub428\nfunction HeaderUser({ id }) {\n  const { data } = useQuery({ queryKey: [&#8216;user&#8217;, id], queryFn: fetchUser });\n  return &lt;span&gt;{data?.name}&lt;\/span&gt;;\n}\nfunction SidebarAvatar({ id }) {\n  const { data } = useQuery({ queryKey: [&#8216;user&#8217;, id], queryFn: fetchUser });\n  \/\/ \u2190 Header \uac00 \uba3c\uc800 \ud638\ucd9c\ud588\uc73c\uba74 \uce90\uc2dc\uc5d0\uc11c \uc989\uc2dc \ubc18\ud658\n  return &lt;img src={data?.avatar} \/&gt;;\n}\n\n\/\/ id \uac00 \ubc14\ub00c\uba74 \ub2e4\ub978 key \u2192 \ub2e4\ub978 \uce90\uc2dc \u2192 \uc0c8 fetch\n\/\/ queryKey \ubc30\uc5f4\uc5d0 \ubaa8\ub4e0 \uc758\uc874 \uac12 \ud3ec\ud568 (useEffect dependency \uc640 \ub3d9\uc77c \uc6d0\uce59)<\/div>\n\n<h2>3. mutation \u2014 \ub370\uc774\ud130 \ubcc0\uacbd<\/h2>\n\n<p>fetch GET \uc740 useQuery, POST\/PUT\/DELETE \ub294 <code>useMutation<\/code>. \ubcc0\uacbd \ud6c4 \uad00\ub828 query \ub97c \uc790\ub3d9 \ubb34\ud6a8\ud654 \u2192 \uc790\ub3d9 refetch.<\/p>\n\n<div class=\"code-block\">import { useMutation, useQueryClient } from &#8216;@tanstack\/react-query&#8217;;\n\nfunction AddTodo() {\n  const queryClient = useQueryClient();\n  const mutation = useMutation({\n    mutationFn: (newTodo) =&gt; fetch(&#8216;\/api\/todos&#8217;, {\n      method: &#8216;POST&#8217;, body: JSON.stringify(newTodo)\n    }).then(r =&gt; r.json()),\n    onSuccess: () =&gt; {\n      \/\/ [&#8216;todos&#8217;] \uce90\uc2dc \ubb34\ud6a8\ud654 \u2192 \ub9ac\uc2a4\ud2b8 \ud654\uba74\uc774 \uc790\ub3d9 refetch\n      queryClient.invalidateQueries({ queryKey: [&#8216;todos&#8217;] });\n    },\n  });\n  return &lt;button onClick={() =&gt; mutation.mutate({ text: &#8216;\uc6b0\uc720&#8217; })}&gt;\n    {mutation.isPending ? &#8216;\ucd94\uac00 \uc911&#8230;&#8217; : &#8216;\ucd94\uac00&#8217;}\n  &lt;\/button&gt;;\n}<\/div>\n\n<p>\uc774 \ud328\ud134\uc774 \uc9c4\uac00 \u2014 todo \ucd94\uac00 \u2192 \uc790\ub3d9\uc73c\ub85c \ubaa9\ub85d \ud654\uba74 \uac31\uc2e0. \ub450 \ucef4\ud3ec\ub10c\ud2b8 \uc0ac\uc774 \uc9c1\uc811 \uc0c1\ud0dc \uacf5\uc720 \uc5c6\uc774\ub3c4 \ub3d9\uae30\ud654.<\/p>\n\n<h2>4. Suspense \ubaa8\ub4dc \u2014 if isLoading \uc5c6\uc774<\/h2>\n\n<p>\uc704 \uc608\uc81c\ub294 \ub9e4\ubc88 <code>if (isLoading) return &lt;Spinner \/&gt;<\/code> \ubd84\uae30. React 18 \uc758 Suspense \uc640 useQuery \uc758 <code>suspense: true<\/code> \uc635\uc158\uc73c\ub85c \ud55c \ub2e8\uacc4 \ub354 \uc6b0\uc544\ud558\uac8c:<\/p>\n\n<div class=\"code-block\">\/\/ useQuery \uac00 promise throw \u2192 \ubd80\ubaa8 Suspense \uac00 fallback \ud45c\uc2dc\nfunction UserProfile({ id }) {\n  const { data } = useSuspenseQuery({\n    queryKey: [&#8216;user&#8217;, id],\n    queryFn: () =&gt; fetchUser(id),\n  });\n  \/\/ \u2705 data \uac00 \ubb34\uc870\uac74 \uc788\uc74c (TS \ub3c4 \uc54c\uc544\ucc54)\n  return &lt;h1&gt;{data.name}&lt;\/h1&gt;;\n}\n\nfunction App() {\n  return (\n    &lt;Suspense fallback={&lt;Spinner \/&gt;}&gt;\n      &lt;UserProfile id={1} \/&gt;\n    &lt;\/Suspense&gt;\n    {\/* \uc5d0\ub7ec\ub294 18\ud3b8 Error Boundary \uac00 \uc7a1\uc74c *\/}\n  );\n}<\/div>\n\n<p>\uc7a5\uc810 \u2014 \u2460 \ub370\uc774\ud130 \ub85c\ub529 \ubd84\uae30\uac00 \ubcf8\ubb38\uc5d0\uc11c \uc0ac\ub77c\uc9d0 \u2192 JSX \uac04\uacb0. \u2461 TypeScript \uac00 data \uc758 nullable \uc744 \uc548 \uc54c\uc544\ub3c4 \ub428 (\ud56d\uc0c1 \uc788\uc74c \ubcf4\uc7a5). \u2462 \ubd80\ubaa8 \ud55c \uacf3\uc5d0 fallback \ubaa8\uc74c \u2192 \ub514\uc790\uc778 \uc77c\uad00\uc131.<\/p>\n\n<div class=\"warnbox\">\n<strong>\uc8fc\uc758<\/strong> \u2014 useSuspenseQuery \ub294 \ub370\uc774\ud130 \uc5c6\uc73c\uba74 \ubb34\uc870\uac74 promise throw. \uc989 \ucef4\ud3ec\ub10c\ud2b8 \ubcf8\ubb38\uc740 \ub370\uc774\ud130 \uc788\uc744 \ub54c\ub9cc \uc2e4\ud589. SSR\u00b7\uc810\uc9c4\uc801 hydration \uacfc \uacb0\ud569 \uc2dc \uc2e0\uc911\ud574\uc57c (22\ud3b8 RSC \uc5d0\uc11c \ub2e4\uc2dc). \uc791\uc740 SPA \ub294 \uadf8\ub0e5 <code>useQuery<\/code> \ub3c4 \ucda9\ubd84\ud788 \uae68\ub057.\n<\/div>\n\n<p>TanStack Query \uac00 \ud68c\uc0ac stack \uc758 \uc0ac\uc2e4\uc0c1 \ud45c\uc900\uc774 \ub41c \uc774\uc720\ub294 \uba85\ubc31 \u2014 useEffect \ub85c \uc9e0 fetch \ucf54\ub4dc\uac00 200\uc904\uc774\uba74, useQuery \ub85c \uc9dc\uba74 30\uc904. \uadf8\uac83\ub3c4 \ub354 \ube60\ub974\uace0 \ub354 \uc548\uc815\uc801. 18\ud3b8 Error Boundary \uc640 \uc9dd\uc9c0\uc73c\uba74 production-grade \ub370\uc774\ud130 \ub808\uc774\uc5b4 \uc644\uc131.<\/p>\n\n<div class=\"cta\">\n<h3>\ub2e4\uc74c \uae00<\/h3>\n<p>React \uad50\uc7ac 18\ud3b8 \u2014 Error Boundary. \ucef4\ud3ec\ub10c\ud2b8 \ud2b8\ub9ac \uc548 \uc5d0\ub7ec\ub97c \uaca9\ub9ac\ud574 \ud654\uba74 \uc804\uccb4\uac00 \uc8fd\ub294 \uc0ac\uace0 \ubc29\uc9c0.<\/p>\n<\/div>\n\n<div class=\"footer-nav\">\nReact \uad50\uc7ac \uc2dc\ub9ac\uc988 \u00b7\n<a href=\"https:\/\/junai.ai\/blog\/react-router-15\/\">15\ud3b8 Router<\/a> \u00b7\n<a href=\"https:\/\/junai.ai\/blog\/react-performance-16\/\">16\ud3b8 \uc131\ub2a5<\/a> \u00b7\n<strong>17\ud3b8 Suspense+Query<\/strong>\n<\/div>\n\n<\/article>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>React Suspense + TanStack Query \u2014 \uc120\uc5b8\uc801 \ube44\ub3d9\uae30 \ub370\uc774\ud130, \uce90\uc2f1, \uc911\ubcf5 \uc81c\uac70, refetch. \uad50\uc7ac 17\ud3b8.<\/p>\n","protected":false},"author":1,"featured_media":757,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22],"tags":[],"class_list":["post-783","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/783","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/comments?post=783"}],"version-history":[{"count":1,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/783\/revisions"}],"predecessor-version":[{"id":804,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/783\/revisions\/804"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media\/757"}],"wp:attachment":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media?parent=783"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/categories?post=783"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/tags?post=783"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}