iBetter Books
수정

Ch 06. OAuth로 소셜 로그인 추가하기

회원가입 페이지에서 이메일, 비밀번호, 이름을 입력하고, 이메일 인증을 거치고, 비밀번호 규칙을 맞추고... 이 과정이 귀찮아서 그냥 창을 닫아버린 경험이 한 번쯤 있을 겁니다.

"Google로 로그인" 버튼 하나면 해결됩니다. OAuth는 이미 구글이나 깃허브가 사용자를 인증해줬으니, 우리 서비스는 그 결과만 받으면 됩니다. 사용자 입장에서는 클릭 한 번, 개발자 입장에서는 비밀번호 관리 부담이 없어집니다.

Google OAuth 설정

Google Cloud Console에서 클라이언트 ID 발급

  1. Google Cloud Console에 접속합니다.
  2. 새 프로젝트를 만들거나 기존 프로젝트를 선택합니다.
  3. 왼쪽 메뉴에서 API 및 서비스사용자 인증 정보로 이동합니다.
  4. 사용자 인증 정보 만들기OAuth 클라이언트 ID를 클릭합니다.
  5. 애플리케이션 유형: 웹 애플리케이션 선택합니다.
  6. 승인된 리다이렉트 URI를 추가합니다.
    • 개발: http://localhost:3000/api/auth/callback/google
    • 프로덕션: https://yourdomain.com/api/auth/callback/google
  7. 클라이언트 ID와 클라이언트 보안 비밀번호를 복사합니다.

.env.local에 추가합니다.

# 파일: .env.localAUTH_SECRET=your_secret_hereAUTH_GOOGLE_ID=your_google_client_idAUTH_GOOGLE_SECRET=your_google_client_secret```text### auth.ts에 Google Provider 추가```typescript// 파일: auth.tsimport NextAuth from "next-auth";import Credentials from "next-auth/providers/credentials";import Google from "next-auth/providers/google";export const { handlers, signIn, signOut, auth } = NextAuth({  providers: [    Google({      clientId: process.env.AUTH_GOOGLE_ID,      clientSecret: process.env.AUTH_GOOGLE_SECRET,    }),    Credentials({      credentials: {        email: { label: "이메일", type: "email" },        password: { label: "비밀번호", type: "password" },      },      async authorize(credentials) {        // 기존 credentials 로그인 로직        if (          credentials.email === "[email protected]" &&          credentials.password === "password123"        ) {          return { id: "1", name: "홍길동", email: "[email protected]" };        }        return null;      },    }),  ],  session: { strategy: "jwt" },});```text### GitHub Provider 추가GitHub OAuth App을 만들어야 합니다.1. GitHub → `Settings`  `Developer settings`  `OAuth Apps`  `New OAuth App`으로 이동합니다.2. 정보를 입력합니다.   - Application name: 앱 이름   - Homepage URL: `http://localhost:3000`   - Authorization callback URL: `http://localhost:3000/api/auth/callback/github`3. Client ID와 Client secrets를 복사합니다.```bash# 파일: .env.local (GitHub 추가)AUTH_GITHUB_ID=your_github_client_idAUTH_GITHUB_SECRET=your_github_client_secret```text```typescript// 파일: auth.tsimport NextAuth from "next-auth";import Credentials from "next-auth/providers/credentials";import Google from "next-auth/providers/google";import GitHub from "next-auth/providers/github";export const { handlers, signIn, signOut, auth } = NextAuth({  providers: [    Google({      clientId: process.env.AUTH_GOOGLE_ID,      clientSecret: process.env.AUTH_GOOGLE_SECRET,    }),    GitHub({      clientId: process.env.AUTH_GITHUB_ID,      clientSecret: process.env.AUTH_GITHUB_SECRET,    }),    Credentials({      credentials: {        email: { label: "이메일", type: "email" },        password: { label: "비밀번호", type: "password" },      },      async authorize(credentials) {        if (          credentials.email === "[email protected]" &&          credentials.password === "password123"        ) {          return { id: "1", name: "홍길동", email: "[email protected]" };        }        return null;      },    }),  ],  session: { strategy: "jwt" },  pages: {    signIn: "/login",  },});```text### 소셜 로그인 버튼 UI 만들기로그인 페이지에 소셜 로그인 버튼을 추가합니다.```typescript// 파일: app/login/page.tsximport { signIn } from "@/auth";import { redirect } from "next/navigation";export default function LoginPage() {  async function handleEmailLogin(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) {      if ((error as Error).message === "NEXT_REDIRECT") throw error;      redirect("/login?error=invalid_credentials");    }  }  async function handleGoogleLogin() {    "use server";    await signIn("google", { redirectTo: "/dashboard" });  }  async function handleGitHubLogin() {    "use server";    await signIn("github", { redirectTo: "/dashboard" });  }  return (    <div className="min-h-screen flex items-center justify-center bg-gray-50">      <div className="w-full max-w-md p-8 bg-white rounded-xl shadow-sm">        <h2 className="text-2xl font-bold mb-8 text-center">로그인</h2>        {/* 소셜 로그인 버튼 */}        <div className="space-y-3 mb-6">          <form action={handleGoogleLogin}>            <button              type="submit"              className="w-full flex items-center justify-center gap-3 px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors"            >              <svg className="w-5 h-5" viewBox="0 0 24 24">                <path                  fill="#4285F4"                  d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"                />                <path                  fill="#34A853"                  d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"                />                <path                  fill="#FBBC05"                  d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"                />                <path                  fill="#EA4335"                  d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"                />              </svg>              Google로 로그인            </button>          </form>          <form action={handleGitHubLogin}>            <button              type="submit"              className="w-full flex items-center justify-center gap-3 px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors"            >              <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">                <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />              </svg>              GitHub로 로그인            </button>          </form>        </div>        <div className="relative mb-6">          <div className="absolute inset-0 flex items-center">            <div className="w-full border-t border-gray-200" />          </div>          <div className="relative flex justify-center text-sm">            <span className="px-2 bg-white text-gray-500">또는</span>          </div>        </div>        {/* 이메일 로그인 */}        <form action={handleEmailLogin} 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-lg focus:outline-none focus:ring-2 focus:ring-blue-500"            />          </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-lg focus:outline-none focus:ring-2 focus:ring-blue-500"            />          </div>          <button            type="submit"            className="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"          >            이메일로 로그인          </button>        </form>      </div>    </div>  );}```text### 소셜 계정과 이메일 계정 연동같은 이메일로 Google 로그인과 이메일/비밀번호 로그인을 따로 사용하는 경우, 두 계정을 연동해야 할 수 있습니다. 이를 위해서는 데이터베이스 어댑터(Prisma Adapter 등)가 필요합니다. 데이터베이스 기반 세션을 사용하면 NextAuth가 계정 연동을 자동으로 처리합니다.```typescript// 파일: auth.ts (DB 어댑터 사용)import { PrismaAdapter } from "@auth/prisma-adapter";import { prisma } from "@/lib/prisma";export const { handlers, signIn, signOut, auth } = NextAuth({  adapter: PrismaAdapter(prisma),  providers: [Google(/*...*/), GitHub(/*...*/)],  session: { strategy: "database" }, // 어댑터 사용 database 전략 권장});

어댑터를 사용하면 사용자, 계정, 세션 정보가 자동으로 데이터베이스에 저장되고, 같은 이메일의 여러 소셜 계정이 하나의 사용자로 연결됩니다.

이것으로 PART 06의 인증 시스템이 완성되었습니다. 다음 PART에서는 여기서 배운 모든 것을 활용해 실제 풀스택 블로그를 만들어봅니다.