{"id":904,"date":"2026-05-20T09:55:21","date_gmt":"2026-05-20T01:55:21","guid":{"rendered":"https:\/\/junai.ai\/blog\/nextjs-auth-patterns-19\/"},"modified":"2026-05-20T09:55:21","modified_gmt":"2026-05-20T01:55:21","slug":"nextjs-auth-patterns-19","status":"publish","type":"post","link":"https:\/\/junai.ai\/blog\/nextjs-auth-patterns-19\/","title":{"rendered":"\uc778\uc99d \ud328\ud134 \u2014 Auth.js (NextAuth) v5"},"content":{"rendered":"\n<!-- WordPress REST API \ubc1c\ud589\uc6a9 HTML (\uc790\ub3d9 \uc0dd\uc131) -->\n<!-- WP-FEATURED-MEDIA-ID: 851 -->\n<div style=\"max-width:800px;margin:0 auto;\">\n<style>\n:root {--color-primary:#4f46e5;--color-accent:#6366f1;--color-bg:#fafbfc;--color-bg-card:#fff;--color-text:#1a202c;--color-text-muted:#64748b;--hero-start:#1e1b4b;--hero-end:#4338ca;}\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:#a78bfa;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:#c7d2fe;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:#eef2ff;padding:2px 8px;border-radius:4px;font-family:'SF Mono',Menlo,Consolas,monospace;font-size:14px;color:#4338ca;}\n.databox{background:#eef2ff;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.tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:22px 0;}\ntable{width:100%;border-collapse:collapse;font-size:15px;background:var(--color-bg-card);}\nth,td{padding:11px 12px;text-align:left;border-bottom:1px solid #e2e8f0;vertical-align:top;}\nth{background:#f1f5f9;font-weight:700;color:#0f172a;}\ntd:first-child,th:first-child{font-weight:700;}\n@media (max-width:560px){.tablewrap table,.tablewrap thead,.tablewrap tbody,.tablewrap tr,.tablewrap th,.tablewrap td{display:block;width:auto;}.tablewrap thead{display:none;}.tablewrap tr{margin:0 0 14px;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;}.tablewrap td{border:none;border-bottom:1px solid #f1f5f9;padding:9px 14px;}.tablewrap td:first-child{background:#f1f5f9;font-weight:800;font-size:15.5px;}.tablewrap td:last-child{border-bottom:none;}.tablewrap td[data-label]::before{content:attr(data-label) \" \u2014 \";font-weight:700;color:var(--color-primary);}}\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,#4f46e5 0%,#6366f1 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:#c7d2fe;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\">Next.js \uad50\uc7ac \u00b7 19\ud3b8 \u00b7 \uc778\uc99d<\/span>\n  <h1>\uc778\uc99d \ud328\ud134 \u2014 Auth.js (NextAuth) v5<\/h1>\n  <p>\uc9c1\uc811 \ub9cc\ub4e4\uc9c0 \ub9d0 \uac83. \ubcf4\uc548 \uc0ac\uace0 1\uc704\uac00 \uc790\uc791 \uc778\uc99d. \uac80\uc99d\ub41c \ub77c\uc774\ube0c\ub7ec\ub9ac\ub85c.<\/p>\n  <img decoding=\"async\" src=\"https:\/\/junai.ai\/blog\/wp-content\/uploads\/2026\/05\/hero-5-82.jpg\" aalt=\"Auth.js OAuth \ub85c\uadf8\uc778 \ud750\ub984 \ucee8\uc149 \uc77c\ub7ec\uc2a4\ud2b8\">\n<\/section>\n\n<div class=\"container\">\n<article>\n\n<p>&#8220;\ub85c\uadf8\uc778 \uc9c1\uc811 \ub9cc\ub4e4\uba74 30\ubd84\uc774\uba74 \ub418\uc9c0 \uc54a\ub098?&#8221; \u2014 \ud45c\uba74\uc740 \uadf8\ub807\ub2e4. \ube44\ubc00\ubc88\ud638 \ud574\uc2dc, \uc138\uc158 \ud1a0\ud070 \ubc1c\uae09, \ub9cc\ub8cc \uac31\uc2e0, OAuth \uc81c\uacf5\uc790 \ud1a0\ud070 \uac80\uc99d, CSRF \ubc29\uc5b4, \uc548\uc804\ud55c \ucfe0\ud0a4, \ube44\ubc00\ubc88\ud638 \uc7ac\uc124\uc815 \uc774\uba54\uc77c, \uacc4\uc815 \uc7a0\uae08\u2026 \uace7 30\uc2dc\uac04\uc774 \ub41c\ub2e4. \uadf8\ub9ac\uace0 \ubcf4\uc548 \uc0ac\uace0\ub294 \uadf8 \uc0ac\uc774 \uc5b4\ub518\uac00\uc5d0\uc11c \ud130\uc9c4\ub2e4.<\/p>\n\n<p>Next.js \uc9c4\uc601\uc758 \ub2f5\uc740 <strong>Auth.js<\/strong> (\uc61b \uc774\ub984 NextAuth.js). v5 \ubd80\ud130 App Router \uc640 \uc644\ubcbd \ud1b5\ud569. Google\u00b7GitHub\u00b7\uc774\uba54\uc77c\u00b7\uc790\uccb4 DB \uc5b4\ub5a4 \ubc29\uc2dd\uc774\ub4e0 \ud55c \uc124\uc815 \ud30c\uc77c\ub85c \ub05d. \uc774\ubc88 \ud3b8\uc774 \uadf8 \uccab \uc138\ud305.<\/p>\n\n<h2>1. \uc124\uce58\uc640 \ud658\uacbd\ubcc0\uc218<\/h2>\n\n<div class=\"code-block\">$ npm install next-auth@beta   # v5 (2026 \uae30\uc900 beta\u00b7rc \uac00 \ud45c\uc900)<\/div>\n\n<p><code>.env.local<\/code>:<\/p>\n\n<div class=\"code-block\">AUTH_SECRET=long-random-string-at-least-32-chars\nAUTH_GOOGLE_ID=&#8230;\nAUTH_GOOGLE_SECRET=&#8230;\nAUTH_GITHUB_ID=&#8230;\nAUTH_GITHUB_SECRET=&#8230;\nAUTH_URL=http:\/\/localhost:3000   # production \uc740 \uc2e4\uc81c \ub3c4\uba54\uc778<\/div>\n\n<p><code>AUTH_SECRET<\/code> \uc0dd\uc131 \u2014 <code>openssl rand -base64 32<\/code> \ub610\ub294 <code>npx auth secret<\/code>. \uc808\ub300 \uae43\uc5d0 \ub123\uc9c0 \ub9d0 \uac83.<\/p>\n\n<p>OAuth \ud0a4 \ubc1c\uae09 \u2014 Google: Cloud Console > Credentials > OAuth 2.0 client. GitHub: Settings > Developer settings > OAuth Apps. \ucf5c\ubc31 URL \uc740 <code>{AUTH_URL}\/api\/auth\/callback\/{provider}<\/code> \ud615\uc2dd.<\/p>\n\n<h2>2. auth.ts \uc124\uc815 \u2014 \ud55c \ud30c\uc77c\ub85c \ub05d<\/h2>\n\n<div class=\"code-block\">\/\/ auth.ts (\ud504\ub85c\uc81d\ud2b8 \ub8e8\ud2b8)\nimport NextAuth from &#8216;next-auth&#8217;;\nimport Google from &#8216;next-auth\/providers\/google&#8217;;\nimport GitHub from &#8216;next-auth\/providers\/github&#8217;;\n\nexport const { handlers, auth, signIn, signOut } = NextAuth({\n  providers: [Google, GitHub],\n  pages: {\n    signIn: &#8216;\/login&#8217;,           \/\/ \ucee4\uc2a4\ud140 \ub85c\uadf8\uc778 \ud398\uc774\uc9c0\n  },\n  callbacks: {\n    async session({ session, token }) {\n      \/\/ \uc138\uc158\uc5d0 \ucd94\uac00 \uc815\ubcf4 \ubc15\uae30\n      if (session.user) session.user.id = token.sub;\n      return session;\n    },\n  },\n});<\/div>\n\n<p>\uc774 \ud55c \ud30c\uc77c\uc774 export \ud558\ub294 4\uac00\uc9c0\ub97c \uc5b4\ub514\uc11c\ub098 import \ud574\uc11c \uc4f4\ub2e4.<\/p>\n\n<div class=\"tablewrap\">\n<table>\n<thead><tr><th>export<\/th><th>\uc6a9\ub3c4<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>handlers<\/code><\/td><td data-label=\"\uc6a9\ub3c4\">API Route \uc5d0 \uadf8\ub300\ub85c \uc5f0\uacb0<\/td><\/tr>\n<tr><td><code>auth()<\/code><\/td><td data-label=\"\uc6a9\ub3c4\">\uc11c\ubc84 \uc5b4\ub514\uc11c\ub098 \ud604\uc7ac \uc138\uc158 \uc77d\uae30<\/td><\/tr>\n<tr><td><code>signIn(provider)<\/code><\/td><td data-label=\"\uc6a9\ub3c4\">Server Action \uc5d0\uc11c \ub85c\uadf8\uc778 \ud2b8\ub9ac\uac70<\/td><\/tr>\n<tr><td><code>signOut()<\/code><\/td><td data-label=\"\uc6a9\ub3c4\">Server Action \uc5d0\uc11c \ub85c\uadf8\uc544\uc6c3<\/td><\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n\n<h2>3. API Route \uc5f0\uacb0<\/h2>\n\n<div class=\"code-block\">\/\/ app\/api\/auth\/[&#8230;nextauth]\/route.ts\nimport { handlers } from &#8216;@\/auth&#8217;;\nexport const { GET, POST } = handlers;<\/div>\n\n<p>\uc774 \ud55c \uc904\uc774 <code>\/api\/auth\/signin<\/code>\u00b7<code>\/api\/auth\/callback\/google<\/code>\u00b7<code>\/api\/auth\/session<\/code> \ub4f1 \uc2ed\uc218 \uac1c \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \uc790\ub3d9 \uc0dd\uc131. Auth.js \uac00 OAuth flow\u00b7\uc138\uc158 \ubc1c\uae09\u00b7\ucf5c\ubc31 \ucc98\ub9ac\ub97c \ub2e4 \uc54c\uc544\uc11c.<\/p>\n\n<h2>4. \uc138\uc158 \uc77d\uae30 \u2014 \uc11c\ubc84\u00b7\ud074\ub77c\uc774\uc5b8\ud2b8<\/h2>\n\n<h3>\uc11c\ubc84 \ucef4\ud3ec\ub10c\ud2b8\u00b7Server Action\u00b7Route Handler<\/h3>\n\n<div class=\"code-block\">\/\/ app\/dashboard\/page.tsx\nimport { auth } from &#8216;@\/auth&#8217;;\nimport { redirect } from &#8216;next\/navigation&#8217;;\n\nexport default async function DashboardPage() {\n  const session = await auth();\n  if (!session) redirect(&#8216;\/login&#8217;);\n  return &lt;h1&gt;\uc548\ub155, {session.user.name}&lt;\/h1&gt;;\n}<\/div>\n\n<h3>\ud074\ub77c\uc774\uc5b8\ud2b8 \ucef4\ud3ec\ub10c\ud2b8<\/h3>\n\n<div class=\"code-block\">&#8216;use client&#8217;;\nimport { useSession, signIn, signOut } from &#8216;next-auth\/react&#8217;;\n\nexport function AuthButtons() {\n  const { data: session, status } = useSession();\n  if (status === &#8216;loading&#8217;) return &lt;p&gt;\u2026&lt;\/p&gt;;\n  if (session) return (\n    &lt;&gt;\n      &lt;span&gt;{session.user.email}&lt;\/span&gt;\n      &lt;button onClick={() =&gt; signOut()}&gt;\ub85c\uadf8\uc544\uc6c3&lt;\/button&gt;\n    &lt;\/&gt;\n  );\n  return &lt;button onClick={() =&gt; signIn(&#8216;google&#8217;)}&gt;\uad6c\uae00 \ub85c\uadf8\uc778&lt;\/button&gt;;\n}<\/div>\n\n<p>\ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c <code>useSession()<\/code> \uc744 \uc4f0\ub824\uba74 <code>app\/layout.tsx<\/code> \uc758 children \uc744 <code>&lt;SessionProvider&gt;<\/code> \ub85c \uac10\uc2f8\uc57c. v5 \ub294 \ub354 \ub9e4\ub044\ub7ec\uc6b4 \ud328\ud134\uc774 \uc788\uc9c0\ub9cc \ud074\ub77c\uc774\uc5b8\ud2b8 hook \uc790\uccb4\ub294 \uac19\ub2e4.<\/p>\n\n<h2>5. middleware \ub85c \ud398\uc774\uc9c0 \uac00\ub4dc<\/h2>\n\n<p>13\ud3b8 \ubbf8\ub4e4\uc6e8\uc5b4 \uc751\uc6a9 \u2014 <code>\/dashboard\/*<\/code> \uc804\uccb4\ub97c \uc778\uc99d \uac00\ub4dc.<\/p>\n\n<div class=\"code-block\">\/\/ middleware.ts\nimport { auth } from &#8216;@\/auth&#8217;;\n\nexport default auth((req) =&gt; {\n  const isProtected = req.nextUrl.pathname.startsWith(&#8216;\/dashboard&#8217;);\n  if (isProtected &#038;&#038; !req.auth) {\n    const url = req.nextUrl.clone();\n    url.pathname = &#8216;\/login&#8217;;\n    url.searchParams.set(&#8216;callbackUrl&#8217;, req.nextUrl.pathname);\n    return Response.redirect(url);\n  }\n});\n\nexport const config = {\n  matcher: [&#8216;\/dashboard\/:path*&#8217;, &#8216;\/api\/admin\/:path*&#8217;],\n};<\/div>\n\n<p>Auth.js \uc758 <code>auth()<\/code> \ud568\uc218\uac00 \ubbf8\ub4e4\uc6e8\uc5b4 \ud615\ud0dc\ub85c\ub3c4 \ub3d9\uc791. <code>req.auth<\/code> \uac00 \uc138\uc158 \uac1d\uccb4 \u2014 \uc5c6\uc73c\uba74 \ub85c\uadf8\uc778\uc73c\ub85c \ubcf4\ub0c4.<\/p>\n\n<div class=\"warnbox\">\n<strong>\ub9e4\ubc88 \uae5c\ube61\ud558\ub294 \uac83<\/strong> \u2014 <code>AUTH_SECRET<\/code> \uc744 production \uc5d0 \ub204\ub77d. \uadf8 \uc21c\uac04 \ubaa8\ub4e0 \uc138\uc158 \ud1a0\ud070\uc774 \uc798\ubabb\ub41c \uc11c\uba85\uc774 \ub418\uc5b4 \ub85c\uadf8\uc778\uc774 \uae68\uc9c4\ub2e4. Vercel\u00b7\ud638\uc2a4\ud305 UI \uc5d0\uc11c \ud658\uacbd\ubcc0\uc218 \ub4f1\ub85d \ud6c4 \uc7ac\ubc30\ud3ec \ud544\uc218. 18\ud3b8 \ud658\uacbd\ubcc0\uc218\uc758 Zod \uac80\uc99d\uc73c\ub85c \uc2dc\uc791 \uc2dc \uc7a1\ub294 \uac8c \uac00\uc7a5 \ube60\ub978 \uc608\ubc29.\n<\/div>\n\n<h3>\uc694\uc57d \u2014 19\ud3b8 \uc88c\ud45c<\/h3>\n\n<p>\uc5ec\uae30\uae4c\uc9c0 \uc815\ub9ac. <strong>Auth.js v5<\/strong> \u2014 <code>auth.ts<\/code> \ud55c \ud30c\uc77c\uc5d0 provider \uc124\uc815 \u2192 <code>app\/api\/auth\/[...nextauth]\/route.ts<\/code> \ud55c \uc904\ub85c \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc790\ub3d9 \uc0dd\uc131. \uc11c\ubc84 \uc5b4\ub514\uc11c\ub098 <code>auth()<\/code>, \ud074\ub77c\uc774\uc5b8\ud2b8\ub294 <code>useSession()<\/code>. \ubbf8\ub4e4\uc6e8\uc5b4\ub85c <code>\/dashboard\/*<\/code> \uac00\ub4dc. \uc9c1\uc811 \ub9cc\ub4e4\uc9c0 \ub9d0 \uac83 \u2014 \ubcf4\uc548 \uc0ac\uace0 1\uc704 \uacbd\ub85c. \ub2e4\uc74c \ud3b8\uc5d0\uc11c <strong>Vercel \ubc30\ud3ec<\/strong>\ub85c \ud55c \ud074\ub9ad \ub77c\uc774\ube0c.<\/p>\n\n<div class=\"cta\">\n<h3>\ub2e4\uc74c \ud3b8 \uc608\uace0 \u2014 Vercel \ubc30\ud3ec<\/h3>\n<p>GitHub \uc5f0\ub3d9\u00b7\ub3c4\uba54\uc778\u00b7\ud658\uacbd\ubcc0\uc218\u00b7preview. 20\ud3b8.<\/p>\n<\/div>\n\n<div class=\"footer-nav\">\n\uc2dc\ub9ac\uc988 \u00b7 <a href=\"https:\/\/junai.ai\/blog\/category\/nextjs\/\">\uc27d\uac8c \ubc30\uc6b0\ub294 Next.js<\/a> \u00b7 \uc774\uc804: <a href=\"https:\/\/junai.ai\/blog\/nextjs-env-variables-18\/\">Ch.18 \ud658\uacbd\ubcc0\uc218<\/a>\n<\/div>\n\n<\/article>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Next.js \uc778\uc99d \u2014 Auth.js v5 \uc138\ud305\u00b7OAuth\u00b7\uc138\uc158\u00b7middleware \uac00\ub4dc. \uc9c1\uc811 \ub9cc\ub4e4\uc9c0 \ub9d0 \uac83. \uad50\uc7ac 19\ud3b8.<\/p>\n","protected":false},"author":1,"featured_media":851,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-904","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-nextjs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/904","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=904"}],"version-history":[{"count":0,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/904\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media\/851"}],"wp:attachment":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media?parent=904"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/categories?post=904"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/tags?post=904"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}