{"id":781,"date":"2026-05-18T23:02:40","date_gmt":"2026-05-18T15:02:40","guid":{"rendered":"https:\/\/junai.ai\/blog\/react-testing-21\/"},"modified":"2026-05-19T20:26:29","modified_gmt":"2026-05-19T12:26:29","slug":"react-testing-21","status":"publish","type":"post","link":"https:\/\/junai.ai\/blog\/react-testing-21\/","title":{"rendered":"React \ud14c\uc2a4\ud2b8 Vitest \uac00\uc774\ub4dc (Ch.21)"},"content":{"rendered":"\n<!-- WordPress REST API \ubc1c\ud589\uc6a9 HTML (\uc790\ub3d9 \uc0dd\uc131) -->\n<!-- WP-FEATURED-MEDIA-ID: 755 -->\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 21\ud3b8<\/span>\n  <h1>React \ud14c\uc2a4\ud2b8 \u2014 Vitest + Testing Library<\/h1>\n  <p>\uc0ac\uc6a9\uc790\uac00 \ubcf4\ub294 \ub300\ub85c \ud14c\uc2a4\ud2b8. \ud074\ub798\uc2a4\uba85 \uac80\uc0ac \ub9d0\uace0 &#8220;\ubc84\ud2bc\uc744 \ud074\ub9ad\ud558\uba74 \uba54\uc2dc\uc9c0\uac00 \ubcf4\uc778\ub2e4&#8221;.<\/p>\n  <img decoding=\"async\" src=\"https:\/\/junai.ai\/blog\/wp-content\/uploads\/2026\/05\/hero-5-38.jpg\" alt=\"\ud14c\uc2a4\ud2b8 \ud29c\ube0c \uc548 UI \ucef4\ud3ec\ub10c\ud2b8\uc640 \ub179\uc0c9 \uccb4\ud06c \uc77c\ub7ec\uc2a4\ud2b8 \u2014 \ucef4\ud3ec\ub10c\ud2b8 \ud14c\uc2a4\ud2b8 \ucee8\uc149\">\n<\/section>\n\n<div class=\"container\">\n<article>\n\n<p>React \ucef4\ud3ec\ub10c\ud2b8 \ud14c\uc2a4\ud2b8\uc758 \ub450 \ub3c4\uad6c\uac00 \uc0ac\uc2e4\uc0c1 \ud45c\uc900 \u2014 <strong>Vitest<\/strong> (\ud14c\uc2a4\ud2b8 \ub7ec\ub108, Vite \uc640 \uc9dd) + <strong>React Testing Library<\/strong> (DOM \uc870\uc791\u00b7\uac80\uc99d). \ub458 \ub2e4 \uac00\ubcbc\uc6b0\uba74\uc11c \ubaa8\ub358 React \ud328\ud134\uc744 \uc798 \ub2e4\ub8ec\ub2e4.<\/p>\n\n<p>\uc774\ubc88 21\ud3b8\uc740 \uccab \ud14c\uc2a4\ud2b8 5\ubd84 \uc14b\uc5c5 + Testing Library \uc758 \ud575\uc2ec \ucca0\ud559 (queryByRole \uc6b0\uc120) + \uc774\ubca4\ud2b8 \uc2dc\ubbac\ub808\uc774\uc158 + async \uac80\uc99d + TanStack Query \uac19\uc740 fetch \ubaa8\ud0b9.<\/p>\n\n<h2>1. \uc14b\uc5c5 \u2014 Vite \ud504\ub85c\uc81d\ud2b8\uba74 3\ubd84<\/h2>\n\n<div class=\"code-block\">npm install -D vitest @testing-library\/react @testing-library\/jest-dom @testing-library\/user-event jsdom\n\n\/\/ vite.config.ts \uc5d0 test \uc635\uc158 \ucd94\uac00\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: &#8216;jsdom&#8217;,\n    globals: true,\n    setupFiles: &#8216;.\/src\/test-setup.ts&#8217;,\n  },\n});\n\n\/\/ src\/test-setup.ts\nimport &#8216;@testing-library\/jest-dom\/vitest&#8217;;\n\n\/\/ package.json scripts\n&#8220;test&#8221;: &#8220;vitest&#8221;,\n&#8220;test:ui&#8221;: &#8220;vitest &#8211;ui&#8221;<\/div>\n\n<p><code>npm test<\/code> \uba74 watch \ubaa8\ub4dc, <code>npm run test:ui<\/code> \uba74 \ube0c\ub77c\uc6b0\uc800 UI \uac00 \ub738 (\ud14c\uc2a4\ud2b8 \uc2e4\ud328\ud55c \ubd80\ubd84 \uc2dc\uac01\ud654).<\/p>\n\n<h2>2. \uccab \ud14c\uc2a4\ud2b8 \u2014 render + queryByRole<\/h2>\n\n<div class=\"code-block\">\/\/ Button.test.tsx\nimport { render, screen } from &#8216;@testing-library\/react&#8217;;\nimport { Button } from &#8216;.\/Button&#8217;;\n\ntest(&#8216;\ubc84\ud2bc\uc774 \ub77c\ubca8\uc744 \ud45c\uc2dc\ud55c\ub2e4&#8217;, () =&gt; {\n  render(&lt;Button label=&#8221;\uc800\uc7a5&#8221; onClick={() =&gt; {}} \/&gt;);\n\n  const button = screen.getByRole(&#8216;button&#8217;, { name: &#8216;\uc800\uc7a5&#8217; });\n  expect(button).toBeInTheDocument();\n});<\/div>\n\n<p>Testing Library \ucca0\ud559 \u2014 <strong>&#8220;\uc0ac\uc6a9\uc790\uac00 \ubcf4\ub294 \ubc29\uc2dd\uc73c\ub85c \uc694\uc18c\ub97c \ucc3e\ub294\ub2e4&#8221;<\/strong>. \ud074\ub798\uc2a4\uba85\u00b7\ud14c\uc2a4\ud2b8 ID \uac00 \uc544\ub2c8\ub77c <em>role<\/em> (button\u00b7link\u00b7heading) \uacfc <em>\uc811\uadfc \uac00\ub2a5\ud55c \uc774\ub984<\/em> (label\u00b7text) \uc73c\ub85c.<\/p>\n\n<div class=\"databox\">\n<strong>\uc65c role \uc6b0\uc120\uc778\uac00<\/strong> \u2014 \u2460 \uc2a4\ud06c\ub9b0\ub9ac\ub354 \uc0ac\uc6a9\uc790\uac00 \uc778\uc9c0\ud558\ub294 \ubc29\uc2dd\uacfc \uc77c\uce58 \u2192 \uc811\uadfc\uc131 \uc790\ub3d9 \uac80\uc99d. \u2461 class \uc774\ub984 \ubc14\uafd4\ub3c4 \ud14c\uc2a4\ud2b8 \uc548 \uae68\uc9d0. \u2462 \ub514\uc790\uc778 \ub9ac\ud329\ud1a0\ub9c1\uc5d0 \uac15\ud568. testid \ub294 &#8220;\ub2e4\ub978 \ubc29\ubc95\uc774 \uc5c6\uc744 \ub54c&#8221; \ucd5c\ud6c4 \uc218\ub2e8.\n<\/div>\n\n<h2>3. \uc774\ubca4\ud2b8 \uc2dc\ubbac\ub808\uc774\uc158 \u2014 userEvent<\/h2>\n\n<div class=\"code-block\">import { userEvent } from &#8216;@testing-library\/user-event&#8217;;\n\ntest(&#8216;\ubc84\ud2bc \ud074\ub9ad \uc2dc onClick \ud638\ucd9c&#8217;, async () =&gt; {\n  const handleClick = vi.fn();   \/\/ Vitest mock\n  render(&lt;Button label=&#8221;\uc800\uc7a5&#8221; onClick={handleClick} \/&gt;);\n\n  const user = userEvent.setup();\n  await user.click(screen.getByRole(&#8216;button&#8217;, { name: &#8216;\uc800\uc7a5&#8217; }));\n\n  expect(handleClick).toHaveBeenCalledTimes(1);\n});\n\ntest(&#8216;input \uc5d0 \ud14d\uc2a4\ud2b8 \uc785\ub825&#8217;, async () =&gt; {\n  render(&lt;LoginForm \/&gt;);\n  const user = userEvent.setup();\n\n  await user.type(screen.getByLabelText(&#8216;\uc774\uba54\uc77c&#8217;), &#8216;test@example.com&#8217;);\n  await user.type(screen.getByLabelText(&#8216;\ube44\ubc00\ubc88\ud638&#8217;), &#8216;pwd1234&#8217;);\n  await user.click(screen.getByRole(&#8216;button&#8217;, { name: &#8216;\ub85c\uadf8\uc778&#8217; }));\n\n  \/\/ \uac80\uc99d\n  expect(screen.getByText(&#8216;\ub85c\uadf8\uc778 \uc911&#8230;&#8217;)).toBeInTheDocument();\n});<\/div>\n\n<p><code>userEvent<\/code> \ub294 \uc2e4\uc81c \uc0ac\uc6a9\uc790\ucc98\ub7fc \u2014 \ud074\ub9ad \uc804 hover, \ud0a4 \uc785\ub825 \uc2dc keydown\/keyup \ubaa8\ub450 \ubc1c\uc0dd. <code>fireEvent.click<\/code> \uac19\uc740 \uc800\uc218\uc900 API \ubcf4\ub2e4 \ud56d\uc0c1 \uc6b0\uc704.<\/p>\n\n<h2>4. async \uac80\uc99d \u2014 findBy \u00b7 waitFor<\/h2>\n\n<p>fetch \uac19\uc740 \ube44\ub3d9\uae30 \uacb0\uacfc\ub294 \uc989\uc2dc \uc548 \ub098\ud0c0\ub0a8. <strong>findBy<\/strong> (\uc788\uc744 \ub54c\uae4c\uc9c0 \uae30\ub2e4\ub9bc) \ub610\ub294 <strong>waitFor<\/strong>.<\/p>\n\n<div class=\"code-block\">test(&#8216;\ub85c\uadf8\uc778 \uc131\uacf5 \uc2dc \ud658\uc601 \uba54\uc2dc\uc9c0&#8217;, async () =&gt; {\n  render(&lt;App \/&gt;);\n  const user = userEvent.setup();\n\n  await user.type(screen.getByLabelText(&#8216;\uc774\uba54\uc77c&#8217;), &#8216;admin@x.com&#8217;);\n  await user.type(screen.getByLabelText(&#8216;\ube44\ubc00\ubc88\ud638&#8217;), &#8216;pwd&#8217;);\n  await user.click(screen.getByRole(&#8216;button&#8217;, { name: &#8216;\ub85c\uadf8\uc778&#8217; }));\n\n  \/\/ findBy = \ucd5c\ub300 1\ucd08 \uae30\ub2e4\ub9ac\uba70 \ud3f4\ub9c1\n  const welcome = await screen.findByText(\/\ud658\uc601\ud569\ub2c8\ub2e4, admin\/);\n  expect(welcome).toBeInTheDocument();\n});<\/div>\n\n<p>3 family \u2014 <code>getBy<\/code> (\uc989\uc2dc, \uc5c6\uc73c\uba74 throw), <code>queryBy<\/code> (\uc989\uc2dc, \uc5c6\uc73c\uba74 null), <code>findBy<\/code> (\ub300\uae30, \uc788\uc744 \ub54c\uae4c\uc9c0). async \uacb0\uacfc\ub294 \ubb34\uc870\uac74 findBy.<\/p>\n\n<h2>5. fetch \ubaa8\ud0b9 \u2014 MSW (Mock Service Worker)<\/h2>\n\n<p>\uc2e4\uc81c API \uc548 \ubd80\ub974\uace0 \uac00\uc9dc \uc751\ub2f5\uc73c\ub85c \ud14c\uc2a4\ud2b8. <strong>MSW<\/strong> \uac00 React \ud14c\uc2a4\ud2b8\uc758 \uc0ac\uc2e4\uc0c1 \ud45c\uc900.<\/p>\n\n<div class=\"code-block\">npm install -D msw\n\n\/\/ src\/test-setup.ts\nimport { setupServer } from &#8216;msw\/node&#8217;;\nimport { http, HttpResponse } from &#8216;msw&#8217;;\n\nconst server = setupServer(\n  http.get(&#8216;\/api\/users\/:id&#8217;, () =&gt; HttpResponse.json({ id: 1, name: &#8216;\ubc15\uc900\uc131&#8217; }))\n);\nbeforeAll(() =&gt; server.listen());\nafterEach(() =&gt; server.resetHandlers());\nafterAll(() =&gt; server.close());\n\n\/\/ \ud14c\uc2a4\ud2b8 \uc548\uc5d0\uc11c \uc751\ub2f5 \ubcc0\uacbd\ntest(&#8216;\uc11c\ubc84 \uc5d0\ub7ec \ud45c\uc2dc&#8217;, async () =&gt; {\n  server.use(\n    http.get(&#8216;\/api\/users\/:id&#8217;, () =&gt; HttpResponse.error())\n  );\n  render(&lt;UserProfile id={1} \/&gt;);\n  expect(await screen.findByText(\/\uc5d0\ub7ec\/)).toBeInTheDocument();\n});<\/div>\n\n<p>MSW \uac00 \uc9c4\uc9dc \uac00\uce58 \u2014 <strong>\ub124\ud2b8\uc6cc\ud06c \ub808\uc774\uc5b4\uc5d0\uc11c \uac00\ub85c\ucc44\uae30<\/strong>. fetch\u00b7axios\u00b7React Query \uc5b4\ub290 \ub3c4\uad6c\ub85c \ud638\ucd9c\ud574\ub3c4 \ub3d9\uc77c\ud558\uac8c \uac00\uc9dc \uc751\ub2f5. \uac1c\ubc1c \uc911\uc5d0\ub3c4 \uc0ac\uc6a9 \uac00\ub2a5 (\uc11c\ubc84 \uc5c6\uc774 \ud504\ub860\ud2b8 \uac1c\ubc1c).<\/p>\n\n<div class=\"warnbox\">\n<strong>\ud14c\uc2a4\ud2b8\ub294 \uc5bc\ub9c8\ub098 \uc368\uc57c \ud558\ub098<\/strong> \u2014 100% \ucee4\ubc84\ub9ac\uc9c0\ub294 \ud568\uc815. \ud575\uc2ec \ud750\ub984 (\ub85c\uadf8\uc778\u00b7\uacb0\uc81c\u00b7\ud575\uc2ec \uae30\ub2a5) \ub9cc \uc798 \ud14c\uc2a4\ud2b8\ud558\ub294 \uac8c 80\/20. \ucef4\ud3ec\ub10c\ud2b8 \ub2e8\uc704\ubcf4\ub2e4 <strong>\uc0ac\uc6a9\uc790 \uc2dc\ub098\ub9ac\uc624 \ub2e8\uc704<\/strong> \uac00 ROI \ub192\ub2e4. e2e \ud14c\uc2a4\ud2b8 (Playwright) \uc640 \uacb0\ud569\ud558\uba74 \ub354 \uc644\uc131.\n<\/div>\n\n<p>21\ud3b8\uc73c\ub85c React \uc758 &#8220;\uc2e0\ub8b0 \uac00\ub2a5\ud55c \ucf54\ub4dc \uc791\uc131&#8221; \uae4c\uc9c0 \ub05d. 22\ud3b8\ubd80\ud130\ub294 \uc2dc\uac01\u00b7\uad6c\uc870 \uc815\ub9ac \u2014 TypeScript \uc640 \uacb0\ud569\ud55c \ud3fc \uac80\uc99d (Zod + React Hook Form) \uc73c\ub85c production \ud3fc\uc758 \ud45c\uc900.<\/p>\n\n<div class=\"cta\">\n<h3>\ub2e4\uc74c \uae00<\/h3>\n<p>React \uad50\uc7ac 22\ud3b8 \u2014 Zod + React Hook Form. \ud68c\uc0ac stack \uc758 \ud3fc \uac80\uc99d \ud45c\uc900. \ud0c0\uc785 \uc548\uc804 + \ub7f0\ud0c0\uc784 \uac80\uc99d.<\/p>\n<\/div>\n\n<div class=\"footer-nav\">\nReact \uad50\uc7ac \uc2dc\ub9ac\uc988 \u00b7\n<a href=\"https:\/\/junai.ai\/blog\/react-css-strategies-19\/\">19\ud3b8 CSS<\/a> \u00b7\n<a href=\"https:\/\/junai.ai\/blog\/react-typescript-20\/\">20\ud3b8 TypeScript<\/a> \u00b7\n<strong>21\ud3b8 \ud14c\uc2a4\ud2b8<\/strong>\n<\/div>\n\n<\/article>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>React \ud14c\uc2a4\ud2b8 \u2014 Vitest + Testing Library. \uc0ac\uc6a9\uc790 \uad00\uc810\u00b7\uc774\ubca4\ud2b8\u00b7async\u00b7queryByRole\u00b7MSW. \uad50\uc7ac 21\ud3b8.<\/p>\n","protected":false},"author":1,"featured_media":755,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22],"tags":[],"class_list":["post-781","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\/781","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=781"}],"version-history":[{"count":1,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/781\/revisions"}],"predecessor-version":[{"id":808,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/781\/revisions\/808"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media\/755"}],"wp:attachment":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media?parent=781"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/categories?post=781"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/tags?post=781"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}