iBetter Books
수정

Ch 02. NextAuth로 로그인 구현하기

로그인 시스템을 처음부터 직접 만드는 것은 생각보다 훨씬 복잡합니다. 비밀번호 해싱, 세션 관리, CSRF 방어, 토큰 갱신... 보안과 관련된 모든 것을 올바르게 구현해야 합니다. 하나라도 잘못되면 보안 취약점이 됩니다.

NextAuth.js(현재는 Auth.js라는 이름으로 발전 중)는 이 모든 복잡한 작업을 대신 처리해줍니다. 우리는 설정만 하면 됩니다.

설치

npm install next-auth@beta```text`@beta` 붙이는 이유가 있습니다. Auth.js v5는 Next.js App Router를 위해 완전히 재작성된 버전입니다. v4는 Pages Router 시절에 만들어진 것으로, App Router 환경에서는 v5를 사용하는 것이 자연스럽습니다.비밀 키도 필요합니다. JWT 서명에 사용되므로 절대 노출되면 안 됩니다.```bash# 랜덤 비밀 키 생성openssl rand -base64 32```text생성한 값을 `.env.local` 추가합니다.```bash# 파일: .env.localAUTH_SECRET=여기에_생성한_랜덤_값을_붙여넣기```text### auth.ts 설정 파일 작성프로젝트 루트에 `auth.ts` 만듭니다. 이 파일이 인증의 핵심 설정입니다.```typescript// 파일: auth.tsimport NextAuth from "next-auth";import Credentials from "next-auth/providers/credentials";export const { handlers, signIn, signOut, auth } = NextAuth({  providers: [    Credentials({      name: "credentials",      credentials: {        email: { label: "이메일", type: "email" },        password: { label: "비밀번호", type: "password" },      },      async authorize(credentials) {        // 실제 프로덕션에서는 DB에서 사용자를 조회하고        // 비밀번호를 bcrypt로 검증해야 합니다.        // 여기서는 간단한 예시로 하드코딩합니다.        if (          credentials.email === "[email protected]" &&          credentials.password === "password123"        ) {          return {            id: "1",            name: "홍길동",            email: "[email protected]",          };        }        // null을 반환하면 로그인 실패        return null;      },    }),  ],  pages: {    signIn: "/login", // 커스텀 로그인 페이지 경로  },  session: {    strategy: "jwt", // JWT 전략 사용  },});```text`NextAuth()` 반환하는 네 가지를 살펴봅니다.- `handlers`: API 라우트에서 사용할 핸들러- `signIn`: 서버에서 로그인을 트리거하는 함수- `signOut`: 서버에서 로그아웃하는 함수- `auth`: 서버 컴포넌트에서 세션을 가져오는 함수### API 라우트 핸들러 연결NextAuth가 실제로 동작하려면 API 엔드포인트가 필요합니다. Auth.js는 `/api/auth/` 경로 아래에서 로그인, 로그아웃, 세션 확인 등 모든 요청을 처리합니다.```typescript// 파일: app/api/auth/[...nextauth]/route.tsimport { handlers } from "@/auth";export const { GET, POST } = handlers;```text 두 줄입니다. `auth.ts`에서 내보낸 `handlers` 그대로 연결하면 됩니다. NextAuth가 나머지를 처리합니다.### 로그인 폼 만들기커스텀 로그인 페이지를 만듭니다. `auth.ts` `pages.signIn`에서 지정한 `/login` 경로입니다.```typescript// 파일: app/login/page.tsximport { signIn } from "@/auth";import { redirect } from "next/navigation";export default function LoginPage() {  async function handleLogin(formData: FormData) {    "use server";    const email = formData.get("email") as string;    const password = formData.get("password") as string;    try {      await signIn("credentials", {        email,        password,        redirectTo: "/dashboard",      });    } catch (error) {      // NextAuth는 리다이렉트를 에러로 throw합니다.      // NEXT_REDIRECT 타입이면 정상 리다이렉트이므로 다시 throw합니다.      if ((error as Error).message === "NEXT_REDIRECT") {        throw error;      }      redirect("/login?error=invalid_credentials");    }  }  return (    <div className="min-h-screen flex items-center justify-center">      <div className="w-full max-w-md p-8 bg-white rounded-lg shadow">        <h2 className="text-2xl font-bold mb-6 text-center">로그인</h2>        <form action={handleLogin} className="space-y-4">          <div>            <label htmlFor="email" className="block text-sm font-medium mb-1">              이메일            </label>            <input              id="email"              name="email"              type="email"              required              className="w-full px-3 py-2 border rounded-md"              placeholder="[email protected]"            />          </div>          <div>            <label              htmlFor="password"              className="block text-sm font-medium mb-1"            >              비밀번호            </label>            <input              id="password"              name="password"              type="password"              required              className="w-full px-3 py-2 border rounded-md"            />          </div>          <button            type="submit"            className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700"          >            로그인          </button>        </form>      </div>    </div>  );}```text### 로그아웃 버튼 만들기로그아웃도 Server Action으로 처리합니다.```typescript// 파일: components/LogoutButton.tsximport { signOut } from "@/auth";export function LogoutButton() {  async function handleLogout() {    "use server";    await signOut({ redirectTo: "/" });  }  return (    <form action={handleLogout}>      <button        type="submit"        className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900"      >        로그아웃      </button>    </form>  );}```text로그아웃을 `<form>` 안에 넣는 이유가 있습니다. Server Action은 `<form>` `action` 속성에 연결해야 합니다. 또한 로그아웃은 상태를 변경하는 작업이므로 GET 요청이 아닌 POST 요청으로 처리하는 것이 올바릅니다.### 세션 확인하기서버 컴포넌트에서 현재 로그인한 사용자 정보를 가져오려면 `auth()` 호출합니다.```typescript// 파일: app/dashboard/page.tsximport { auth } from "@/auth";import { redirect } from "next/navigation";export default async function DashboardPage() {  const session = await auth();  if (!session) {    redirect("/login");  }  return (    <div>      <h2>대시보드</h2>      <p>안녕하세요, {session.user?.name}님!</p>      <p>이메일: {session.user?.email}</p>    </div>  );}

auth()는 세션이 있으면 세션 객체를, 없으면 null을 반환합니다. 로그인이 필요한 페이지에서는 null일 때 로그인 페이지로 리다이렉트합니다.

다음 챕터에서는 JWT 토큰의 내부 구조와 세션 전략을 더 깊이 살펴봅니다.