๊ธฐ๋ณธ ์ค์
ํ๊ฒฝ ๋ณ์ ์ค์
AUTH_SECRET
ํ๋ก์ ํธ ๋ฃจํธ์ .env ํ์ผ์ ์์ฑํ๊ณ ํ์ํ ํ๊ฒฝ ๋ณ์๋ฅผ ์ถ๊ฐํฉ๋๋ค.
AUTH_SECRET=your-secret-key AUTH_GITHUB_ID=your-github-client-id AUTH_GITHUB_SECRET=your-github-client-secret
AUTH_SECRET์ JWT ์ํธํ๋ฅผ ์ํด ํ์์
๋๋ค. ext-auth์์ AUTH_SECRET์ ์ธ์
๊ด๋ฆฌ์ JWT(JSON Web Token) ์๋ช
์ ์ํด ์ฌ์ฉ๋๋ ๋น๋ฐ ํค(secret key)์
๋๋ค. ์ด ํ๊ฒฝ ๋ณ์๋ NextAuth.js๊ฐ ์ฌ์ฉ์ ์ธ์
์ ์์ ํ๊ฒ ์ํธํํ๊ณ ์ธ์ฆ ํ ํฐ์ ์์ฑํ๊ฑฐ๋ ๊ฒ์ฆํ ๋ ์ฌ์ฉ๋ฉ๋๋ค. openssl rand -base64 32 ๋ช
๋ น์ด๋ก ์์ฑํ ์ ์์ต๋๋ค.
- AUTH_SECRET์ ์ ์ ๋ณ๋ก ์์ฑ๋๊ฑฐ๋ ์ ๋ฌ๋์ง ์์ต๋๋ค.
- AUTH_SECRET์ ์ ํ๋ฆฌ์ผ์ด์ ์์ค์์ ์ค์ ๋๋ ๋จ์ผ ๋น๋ฐ ํค๋ก, ๋ชจ๋ ์ฌ์ฉ์์ ๋ํด ๋์ผํ๊ฒ ์ฌ์ฉ๋ฉ๋๋ค. ์ฆ, ๊ฐ๋ณ ์ ์ ๋ง๋ค ๋ค๋ฅธ AUTH_SECRET์ ์ฌ์ฉํ ํ์๋ ์์ต๋๋ค. ์ด ํค๋ NextAuth.js๊ฐ ์ธ์ ๊ด๋ฆฌ์ JWT ์๋ช ์ ์ํด ์๋ฒ ์ธก์์ ์ฌ์ฉํ๋ ๊ณ ์ ๋ ๊ฐ์ ๋๋ค. ์ด ๋ชจ๋ ์์ ์ ์๋ฒ์์ ์ผ๊ด๋๊ฒ ์ด๋ฃจ์ด์ง๋ฉฐ, ์ ์ ๋ณ๋ก ๋ค๋ฅธ ํค๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์์ต๋๋ค.
- JWT ์๋ช : next-auth๋ ๊ธฐ๋ณธ์ ์ผ๋ก JWT๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ธ์ ์ ๊ด๋ฆฌํฉ๋๋ค. AUTH_SECRET์ JWT๋ฅผ ์๋ช ํ๊ณ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๋น๋ฐ ํค๋ก, ์ด ํค๊ฐ ์๊ฑฐ๋ ์๋ชป๋๋ฉด ํ ํฐ์ด ์ ํจํ์ง ์๊ฒ ๋ฉ๋๋ค.
- ์ธ์ ์ํธํ: ์ธ์ ๋ฐ์ดํฐ(์: ์ฌ์ฉ์ ์ ๋ณด)๋ฅผ ์ํธํํ์ฌ ๋ณด์์ ๊ฐํํฉ๋๋ค.
- CSRF ํ ํฐ ๋ณดํธ: next-auth๊ฐ CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์์ฑํ๋ ํ ํฐ์๋ ์ด ํค๊ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- ์ ์ ๋ณ ํ ํฐ๊ณผ ํผ๋ ๊ฐ๋ฅ์ฑ: ๋ก๊ทธ์ธ ์ ์๋ฒ๋ ์ ์ ๋ณ๋ก ๊ณ ์ ํ JWT๋ฅผ ์์ฑํฉ๋๋ค. ์ด ํ ํฐ์๋ ์ ์ ์ ์ ๋ณด(์: userId, email, role)๊ฐ ํฌํจ๋๋ฉฐ, AUTH_SECRET์ ์ฌ์ฉํด ์๋ช ๋ฉ๋๋ค. ํ์ง๋ง AUTH_SECRET ์์ฒด๋ ํ ํฐ์ ์์ฑ/๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๊ณ ์ ๋ ํค์ผ ๋ฟ์ ๋๋ค.
- ํด๋ผ์ด์ธํธ๋ก ์ ๋ฌ๋์ง ์์: AUTH_SECRET์ ์ ๋ ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )๋ก ์ ๋ฌ๋์ง ์์ต๋๋ค. ์ด๋ ์๋ฒ ๋ด๋ถ์์๋ง ์ฌ์ฉ๋๋ ๋น๋ฐ ๊ฐ์ผ๋ก, ์ ์ ์ ์ง์ ์ ์ธ ์ํธ์์ฉ์ด ์์ต๋๋ค.
- ์ธ์ ๋ฐ์ดํฐ: NextAuth.js๋ ์ธ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค(์ต์ ) ๋๋ JWT ์์ฒด์ ์ ์ฅํ ์ ์์ต๋๋ค. ์ ์ ๋ณ ์ ๋ณด๋ ์ด ์ธ์ ๋ฐ์ดํฐ์ ์ ์ฅ๋๋ฉฐ, AUTH_SECRET์ ์ด ๋ฐ์ดํฐ๋ฅผ ์ํธํํ๊ฑฐ๋ ๋ณดํธํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- ์๋ฒ๋ ์์ฒญ๋ง๋ค AUTH_SECRET์ผ๋ก JWT๋ฅผ ๊ฒ์ฆํด ์ ์ ๋ฅผ ์ธ์ฆ.
๊ฐ์ ์์ ํ๊ณ ์์ธกํ๊ธฐ ์ด๋ ค์ด ๋ฌธ์์ด์ด์ด์ผ ํ๋ฉฐ, ์ต์ 32์ ์ด์์ ๋ฌด์์ ๋ฌธ์์ด์ ์ฌ์ฉํ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ค์ ๋ช
๋ น์ด๋ก ์์ฑํ ์ ์์ต๋๋ค.
openssl rand -base64 32
App Router๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, /app/api/auth/[...nextauth]/route.ts ํ์ผ์์ ์ค์ ํฉ๋๋ค. ์ด ํ์ผ์ NextAuth๊ฐ ์ธ์ฆ ๊ด๋ จ ์์ฒญ(๋ก๊ทธ์ธ, ๋ก๊ทธ์์, ์ธ์
๊ด๋ฆฌ ๋ฑ)์ ์ฒ๋ฆฌํ๋ ์๋ํฌ์ธํธ ์ญํ ์ ํฉ๋๋ค.
๋ง์ฝ Pages Router(/pages)๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, /pages/api/auth/[...nextauth].ts ํ์ผ์์ ๋น์ทํ ๋ฐฉ์์ผ๋ก ์ค์ ํ์ง๋ง, App Router์์๋ route.ts ํ์ผ์ ์ฌ์ฉํฉ๋๋ค. ๋ํ, App Router์์๋ GET, POST ๋ฑ์ HTTP ๋ฉ์๋๋ฅผ ๋ช ์์ ์ผ๋ก ๋ด๋ณด๋ด์ผ ํฉ๋๋ค.
๋ง์ฝ Pages Router(/pages)๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, /pages/api/auth/[...nextauth].ts ํ์ผ์์ ๋น์ทํ ๋ฐฉ์์ผ๋ก ์ค์ ํ์ง๋ง, App Router์์๋ route.ts ํ์ผ์ ์ฌ์ฉํฉ๋๋ค. ๋ํ, App Router์์๋ GET, POST ๋ฑ์ HTTP ๋ฉ์๋๋ฅผ ๋ช ์์ ์ผ๋ก ๋ด๋ณด๋ด์ผ ํฉ๋๋ค.
- App Router: App Router์์๋ next-auth์ ์ต์ ๋ฒ์ (4.x)์ ์ฌ์ฉํ๊ณ , next-auth/middleware๋ฅผ ํ์ฉํด ๋ณดํธ๋ ๋ผ์ฐํธ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
- ๋ฏธ๋ค์จ์ด: ์ธ์ฆ์ด ํ์ํ ํ์ด์ง๋ฅผ ๋ณดํธํ๋ ค๋ฉด /app/middleware.ts์์ NextAuth ๋ฏธ๋ค์จ์ด๋ฅผ ์ค์ ํฉ๋๋ค.
- ํ๊ฒฝ ๋ณ์: NEXTAUTH_SECRET๊ณผ ์ ๊ณต์(์: Google, GitHub)์ ํด๋ผ์ด์ธํธ ID/์ํฌ๋ฆฟ์ .env ํ์ผ์ ์ค์ ํด์ผ ํฉ๋๋ค.
ํ๋ก์ ํธ ๊ตฌ์กฐ ์์
my-nextjs-app/ โโโ app/ โ โโโ api/ โ โ โโโ auth/ โ โ โ โโโ [...nextauth]/ โ โ โ โโโ route.ts # NextAuth ์ค์ (์ธ์ฆ ์๋ํฌ์ธํธ) โ โโโ (auth)/ โ โ โโโ login/ โ โ โ โโโ page.tsx # ์ปค์คํ ๋ก๊ทธ์ธ ํ์ด์ง โ โ โโโ signup/ โ โ โ โโโ page.tsx # ์ปค์คํ ํ์๊ฐ์ ํ์ด์ง (์ ํ) โ โโโ dashboard/ โ โ โโโ page.tsx # ๋ณดํธ๋ ํ์ด์ง (์ธ์ฆ ํ์) โ โโโ layout.tsx # ๋ฃจํธ ๋ ์ด์์ โ โโโ page.tsx # ํ ํ์ด์ง โ โโโ globals.css # ์ ์ญ ์คํ์ผ โโโ lib/ โ โโโ auth.ts # NextAuth ์ค์ (authOptions) โ โโโ db.ts # ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ (์: Prisma) โโโ components/ โ โโโ Header.tsx # ํค๋ ์ปดํฌ๋ํธ โ โโโ Footer.tsx # ํธํฐ ์ปดํฌ๋ํธ โ โโโ AuthButton.tsx # ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๋ฒํผ ์ปดํฌ๋ํธ โโโ public/ โ โโโ images/ # ์ ์ ์ด๋ฏธ์ง ํ์ผ โ โโโ favicon.ico # ํ๋น์ฝ โโโ middleware.ts # NextAuth ๋ฏธ๋ค์จ์ด (๋ณดํธ๋ ๋ผ์ฐํธ ์ค์ ) โโโ .env # ํ๊ฒฝ ๋ณ์ (NEXTAUTH_SECRET, Google ID ๋ฑ) โโโ next.config.mjs # Next.js ์ค์ โโโ tsconfig.json # TypeScript ์ค์ (TypeScript ์ฌ์ฉ ์) โโโ package.json # ํ๋ก์ ํธ ์์กด์ฑ ๋ฐ ์คํฌ๋ฆฝํธ โโโ README.md # ํ๋ก์ ํธ ์ค๋ช
- /app/(auth)/login/page.tsx : ์ปค์คํ ๋ก๊ทธ์ธ ํ์ด์ง๋ก, signIn ํจ์๋ฅผ ํธ์ถํ๊ฑฐ๋ UI๋ฅผ ์ ๊ณตํฉ๋๋ค. ํ์์ ๋ฐ๋ผ ํ์๊ฐ์ ํ์ด์ง(/signup) ๋ฑ์ ์ถ๊ฐ.
- /app/dashboard/page.tsx : ์ธ์ฆ์ด ํ์ํ ๋ณดํธ๋ ํ์ด์ง. NextAuth ๋ฏธ๋ค์จ์ด๋ก ์ ๊ทผ์ ์ ์ดํ ์ ์์.
๐ app/api/auth/[...nextauth]/route.ts
NextAuth์ ์ธ์ฆ ์๋ํฌ์ธํธ๋ฅผ ์ฒ๋ฆฌํ๋ ํ์ผ์
๋๋ค. GET, POST ๋ฉ์๋๋ก NextAuth๋ฅผ ์ค์ ํฉ๋๋ค.
import NextAuth from "next-auth"; import GitHubProvider from "next-auth/providers/github"; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHubProvider({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET, }), ], }); export { handlers as GET, handlers as POST };
NextAuth(authOptions)๋ฅผ ํธ์ถํ์ฌ Google, GitHub ๋ฑ์ ์ธ์ฆ ์ ๊ณต์๋ฅผ ์ฒ๋ฆฌํ๋ฉฐ handlers๋ ์ธ์ฆ ๊ด๋ จ API ์๋ํฌ์ธํธ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
๐ /lib/auth.js
NextAuth ์ค์ (authOptions)์ ์ ์ํฉ๋๋ค. GoogleProvider, ์ธ์
์ฝ๋ฐฑ, JWT ์ค์ ๋ฑ์ ์ค์ ํฉ๋๋ค.
import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; export const { handlers: { GET, POST }, auth, signIn, signOut, } = NextAuth({ providers: [ CredentialsProvider({ name: "Credentials", credentials: { username: { label: "Username", type: "text", placeholder: "jsmith" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { const authResponse = await fetch("/your/endpoint", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credentials), }); if (!authResponse.ok) { return null; } const user = await authResponse.json(); return user; }, }), ], });
ํด๋ผ์ด์ธํธ ์ฌ์ฉ๋ฒ
SessionProvider ์ฌ์ฉ
ํด๋ผ์ด์ธํธ์์๋ next-auth/react์ ํ (useSession, signIn, signOut ๋ฑ)์ ์ฌ์ฉํด ์ธ์ฆ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ฑฐ๋ ๋ก๊ทธ์ธ/๋ก๊ทธ์์์ ์ฒ๋ฆฌํฉ๋๋ค.
๐ app/layout.tsx
import { SessionProvider } from "next-auth/react"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body> <SessionProvider>{children}</SessionProvider> </body> </html> ); }
App Router์์๋ SessionProvider๊ฐ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ด๋ฏ๋ก, ๋ฃจํธ ๋ ์ด์์์ ์ง์ ์ถ๊ฐํ ์ ์์ต๋๋ค. ๋ณ๋์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
๐ pages/_app.tsx
import { SessionProvider } from "next-auth/react"; export default function App({ Component, pageProps: { session, ...pageProps } }) { return ( <SessionProvider session={session}> <Component {...pageProps} /> </SessionProvider> ); }
ํด๋ผ์ด์ธํธ ์ฌ์ฉ๋ฒ
useSession ์ฌ์ฉ
ํด๋ผ์ด์ธํธ์์๋ next-auth/react์ ํ (useSession, signIn, signOut ๋ฑ)์ ์ฌ์ฉํด ์ธ์ฆ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ฑฐ๋ ๋ก๊ทธ์ธ/๋ก๊ทธ์์์ ์ฒ๋ฆฌํฉ๋๋ค.
ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ useSession ํ
์ ์ฌ์ฉํ์ฌ ์ธ์
์ํ๋ฅผ ํ์ธํฉ๋๋ค.
// app/client/page.tsx "use client"; import { useSession, signIn, signOut } from "next-auth/react"; export default function ClientPage() { const { data: session, status } = useSession(); if (status === "loading") return <p>๋ก๋ฉ ์ค...</p>; if (status === "authenticated") { return ( <> <p>{session.user?.name}๋์ผ๋ก ๋ก๊ทธ์ธ๋จ</p> <button onClick={() => signOut()}>๋ก๊ทธ์์</button> </> ); } return ( <> <p>๋ก๊ทธ์ธํ์ง ์์</p> <button onClick={() => signIn("github")}>GitHub๋ก ๋ก๊ทธ์ธ</button> </> ); }
- useSession์ data (์ธ์ ๊ฐ์ฒด)์ status (loading, authenticated, unauthenticated)๋ฅผ ๋ฐํํฉ๋๋ค.
- signIn๊ณผ signOut์ ๊ฐ๊ฐ ๋ก๊ทธ์ธ๊ณผ ๋ก๊ทธ์์์ ์ฒ๋ฆฌํฉ๋๋ค.
์ธ์ ์ ๋ฐ์ดํธ
useSession์ update ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ ์ธก์์ ์ธ์
์ ์
๋ฐ์ดํธํ ์ ์์ต๋๋ค.
// app/client/page.tsx "use client"; import { useSession } from "next-auth/react"; export default function UpdateSessionPage() { const { data: session, update } = useSession(); const handleUpdate = async () => { await update({ name: "์๋ก์ด ์ด๋ฆ" }); // ์ธ์ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ }; return ( <> <p>ํ์ฌ ์ฌ์ฉ์: {session?.user?.name}</p> <button onClick={handleUpdate}>์ด๋ฆ ์ ๋ฐ์ดํธ</button> </> ); }
update ๋ฉ์๋๋ ์๋ฒ์ jwt ์ฝ๋ฐฑ์ ํธ๋ฆฌ๊ฑฐํ์ฌ ์ธ์
๋ฐ์ดํฐ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.
<์๋ฒ ์ธก ์ฌ์ฉ๋ฒ> App Router์์ ์๋ฒ ์ธ์ ํ์ธ
๊ด๋ จ ํค์๋
auth ํจ์
/app/api/auth/[...nextauth]/route.ts์์ ๋ด๋ณด๋ธ auth ํจ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๐ app/server/page.tsx
import { auth , GET, POST } from @/app/auth"; export default async function ServerPage() { const session = await auth(); if (!session) { return <p>๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.</p>; } return <p>์๋ฒ์์ ํ์ธ๋ ์ฌ์ฉ์: {session.user?.name}</p>; }
auth ํจ์๋ ์๋ฒ ์ปดํฌ๋ํธ, ๋ฏธ๋ค์จ์ด, API ๋ผ์ฐํธ ๋ฑ์์ ์ฌ์ฉํ ์ ์๋ ํตํฉ ๋ฉ์๋์
๋๋ค.
<์๋ฒ ์ธก ์ฌ์ฉ๋ฒ> Pages Router์์ ์๋ฒ ์ธ์ ํ์ธ
Pages Router์์๋ getServerSession์ ์ฌ์ฉํฉ๋๋ค.
๐ pages/server.js
import { getServerSession } from "next-auth/next"; import { authOptions } from "./api/auth/[...nextauth]"; export default function ServerPage({ session }) { if (!session) { return <p>๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.</p>; } return <p>์๋ฒ์์ ํ์ธ๋ ์ฌ์ฉ์: {session.user?.email}</p>; } export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); return { props: { session, }, }; }
getServerSession์ ์ธ์
ํ์ธ ์๋๊ฐ ๋น ๋ฅด๋ฉฐ, ์ถ๊ฐ ๋คํธ์ํฌ ์์ฒญ์ ์ค์
๋๋ค.
๋ฏธ๋ค์จ์ด๋ก ํ์ด์ง ๋ณดํธ
๐ /middleware.ts
NextAuth ๋ฏธ๋ค์จ์ด๋ฅผ ์ค์ ํ์ฌ ํน์ ๊ฒฝ๋ก(์: /dashboard)์ ์ธ์ฆ์ด ํ์ํ๋๋ก ์ ํ. ์: export { default } from 'next-auth/middleware';
import { withAuth } from "next-auth/middleware"; export default withAuth({ callbacks: { authorized: ({ token }) => !!token, // ํ ํฐ์ด ์์ผ๋ฉด ์ธ์ฆ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผ }, }); export const config = { matcher: ["/dashboard/:path*"], // /dashboard ๊ฒฝ๋ก์ ํ์ ๊ฒฝ๋ก ๋ณดํธ };
/dashboard๋ก ์์ํ๋ ๋ชจ๋ ๊ฒฝ๋ก์ ๋ํด ์ธ์ฆ์ ์๊ตฌ
์ปค์คํ ๋ก๊ทธ์ธ ํ์ด์ง
๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ปค์คํฐ๋ง์ด์งํ๋ ค๋ฉด pages ์ต์
์ ์ฌ์ฉํฉ๋๋ค.
๐ app/api/auth/[...nextauth]/route.ts
export const { handlers, auth } = NextAuth({ providers: [GitHubProvider({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET })], pages: { signIn: "/auth/signin", }, });
๐ app/auth/signin/page.tsx
import { getProviders, signIn } from "next-auth/react"; export default async function SignIn() { const providers = await getProviders(); return ( <div> <h1>๋ก๊ทธ์ธ</h1> {providers && Object.values(providers).map((provider) => ( <div key={provider.name}> <button onClick={() => signIn(provider.id)}> {provider.name}์ผ๋ก ๋ก๊ทธ์ธ </button> </div> ))} </div> ); }