본문 바로가기

프로그래밍

NextAuth.js v5(App Router)로 구글·카카오 OAuth 소셜 로그인 구현 가이드

반응형

왜 NextAuth.js v5와 App Router 조합이 필요한가

소셜 로그인을 붙이려면 보안, 리다이렉트, 토큰 갱신 같은 세부가 한꺼번에 얽힙니다. NextAuth.js v5(App Router)를 사용하면 구글·카카오 OAuth 흐름을 서버 액션과 라우트 핸들러로 일관되게 관리할 수 있습니다. Next 13+ 환경에서는 pages 라우팅과 설정 방식이 달라 혼선이 생기기 쉽습니다. 이 글은 v5 기준으로 폴더 구조, 환경변수, 콜백 처리까지 처음 적용하는 분도 실무에서 막히지 않도록 필요한 부분만 순서대로 다룹니다.

 

구글과 카카오는 승인 URL, 스코프, 프로필 매핑 규칙이 달라 같은 코드로 처리하면 오류를 만납니다. 각 프로바이더 차이를 최소한의 설정으로 맞추는 방법과, 팀 협업에서 재사용 가능한 설정 패턴을 함께 제시합니다.마지막으로, 배포 환경에서 종종 발생하는 “리다이렉트 URI 불일치”와 쿠키 옵션 문제를 체크리스트로 정리해 예방할 수 있도록 안내합니다.

 

OAuth 흐름을 App Router에 맞게 이해하기

소셜 로그인은 “사용자 동의 → 제공자(구글·카카오) 인증 → 우리 앱으로 콜백 → 세션 발급” 순서로 돌아갑니다. NextAuth v5는 이 흐름을 라우트 핸들러(/app/api/auth/[...nextauth]/route.ts)에서 일관되게 처리해, 서버와 클라이언트 간 상태를 최소화합니다.

 

구글은 이메일·프로필이 기본 스코프에 잘 포함되지만, 카카오는 프로필 동의 항목과 닉네임/이메일 제공 여부가 사용자 설정에 따라 달라집니다. 그래서 프로필 매핑 단계에서 null 가능성을 고려해 기본값을 지정하거나, 추가 동의(Additional Consent) 시나리오를 분기하는 편이 안전합니다.

 

App Router에서는 세션 조회가 서버 컴포넌트와 서버 액션에서 자연스럽습니다. 예를 들어 대시보드 레이아웃에서 서버 측 세션을 확인해 보호 라우팅을 구현하고, 클라이언트 버튼에선 useSession으로 상태만 구독하는 구성을 권장합니다.

실무 체크포인트:
- 리다이렉트 URI는 로컬과 배포 도메인을 각각 OAuth 콘솔에 등록
- 구글: openid email profile 스코프 기본, 필요 시 추가 스코프 명시
- 카카오: 동의 항목 노출 설정과 프로필 응답 구조(카카오계정/프로필) 점검
- 프로필 매핑에서 id, email, name, image의 누락 대비 기본값 처리

App Router용 NextAuth 설정 한눈에 보기

환경변수부터 정리하세요. 리다이렉트 URI는 개발·배포 각각 정확히 등록하고, 카카오는 동의 범위에 따라 이메일이 비어 있을 수 있습니다.

- 필수: NEXTAUTH_URL, NEXTAUTH_SECRET
- OAuth: GOOGLE_CLIENT_ID/SECRET, KAKAO_CLIENT_ID/SECRET

 

App Router에 인증 라우트를 만들고, 구글·카카오 프로필을 공통 스키마(id, name, email, image, provider)로 매핑합니다. 세션에는 최소 정보(id, provider)만 담아 불필요한 노출을 줄입니다.

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Kakao from "next-auth/providers/kakao";

const handler = NextAuth({
  providers: [
    Google({
      allowDangerousEmailAccountLinking: true,
      profile(p) {
        return {
          id: p.sub,
          name: p.name || p.email?.split("@")[0],
          email: p.email,
          image: p.picture,
          provider: "google",
        };
      },
    }),
    Kakao({
      clientId: process.env.KAKAO_CLIENT_ID!,
      clientSecret: process.env.KAKAO_CLIENT_SECRET!,
      profile(p) {
        const a = p.kakao_account || {};
        const prof = a.profile || {};
        return {
          id: String(p.id),
          name: prof.nickname || "사용자",
          email: a.email || null,
          image: prof.profile_image_url || null,
          provider: "kakao",
        };
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.sub!;
      session.user.provider = token.provider as string;
      return session;
    },
    async jwt({ token, account }) {
      if (account) token.provider = account.provider;
      return token;
    },
  },
});
export { handler as GET, handler as POST };

 

카카오 이메일이 null일 수 있으니 DB 유니크 키는 id+provider 조합이 안전합니다. 로그인 UI는 클라이언트 컴포넌트로 분리합니다.

// app/login/LoginButtons.tsx
"use client";
import { signIn } from "next-auth/react";
export default function LoginButtons() {
  return (
    <div>
      <button onClick={() => signIn("google")}>Google로 로그인</button>
      <button onClick={() => signIn("kakao")}>Kakao로 로그인</button>
    </div>
  );
}

 

배포 전 체크리스트
- 도메인/HTTPS, 쿠키 sameSite·secure 설정
- OAuth 리다이렉트 URI 정확성
- 환경변수 주입 시점과 값 검증

 

흔한 실수와 선택 기준 정리

가장 흔한 실수는 리다이렉트 URI 불일치, 환경변수 누락, 카카오 이메일 null 처리 미흡입니다. 리다이렉트·콜백·도메인 프로토콜(https)·포트까지 정확히 일치하는지 배포 전후 각각 점검하세요.

쿠키 설정도 자주 놓칩니다. Vercel/프록시 환경에서는 secure, sameSite, domain을 환경에 맞춰 분리하고, App Router에선 Route Handler 캐싱이 켜지지 않도록 동적 처리 옵션을 확인하는 편이 안전합니다.

구글·카카오 스코프는 최소 권한 원칙을 따르되, 카카오는 추가 동의 흐름을 대비해 프로필 매핑에서 기본값을 두세요. 이메일이 없을 때는 provider+id로 내부 계정을 생성하거나, 재동의 유도 화면으로 분기하는 정책을 미리 정해둡니다.

선택 기준은 단순합니다. 빠른 온보딩과 범용성이 필요하면 구글을 기본값으로, 국내 앱에서 카카오 채널/톡 공유와 연계한다면 카카오를 함께 둡니다.

 

팀 관점에서는 프로필 공통 스키마, 에러 코드 표준화, 재시도/재동의 UX를 모듈로 분리하는 구성이 유지보수에 유리합니다.

체크리스트
- Redirect URI: dev/production 각각 정확히 등록, http/https 구분
- 환경변수: NEXTAUTH_URL/SECRET, 각 Provider ID/SECRET 유효성
- 쿠키: secure/sameSite=“lax|none” 결정, 프록시 뒤 domain 설정
- 카카오: 이메일 null 대비, 추가 동의 분기, 연령대/성별 등 민감 정보 미요청 기본
- 세션: 최소 정보만 저장, 토큰 갱신·만료 에러 핸들링 로그 분리

 

핵심만 다시 정리하고 바로 실행하기

이번 글의 요지는 App Router 기준으로 구글·카카오 흐름을 동일한 라우트 핸들러에서 처리하고, 프로필 매핑과 세션 최소화로 안정성을 확보하는 것입니다. 배포 전에는 리다이렉트 URI, 쿠키 옵션, 카카오 이메일 null을 반드시 점검하세요.

바로 적용하려면 다음 순서로 진행해 보세요.
- 환경변수 설정: NEXTAUTH_URL/SECRET, GOOGLE/KAKAO 클라이언트 키
- 라우트 생성: app/api/auth/[...nextauth]/route.ts
- 프로필 매핑: id, name, email, image, provider 공통 스키마
- 세션 정책: 최소 필드, 보안 쿠키 분리(dev/prod)
- 테스트: 개발/배포 각각 리다이렉트·동의·세션 확인

추가로, 카카오에서 이메일이 비어도 로그인 흐름이 끊기지 않도록 대체 키(provider+id)로 내부 사용자 식별 정책을 준비하세요. 구글 기본 온보딩 후 국내 사용자 비중이 높다면 카카오를 옵션으로 병행하는 구성을 추천합니다.

구글·카카오 동시 로그인: 공통 스키마와 최소 세션

구글·카카오는 제공 필드가 달라 공통 스키마 정리가 필요합니다. 특히 카카오는 email이 비어 있을 수 있어, provider+id 조합으로 내부 식별자를 두는 편이 안전합니다.

아래는 NextAuth v5(App Router)에서 두 프로바이더를 등록하고 누락값을 null로 통일해 매핑하는 예시입니다. 옵션·콜백 시그니처는 적용 시점에 공식 문서에서 다시 확인하세요.

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Kakao from "next-auth/providers/kakao";

const handler = NextAuth({
  providers: [
    Google({
      profile(p) {
        return {
          id: p.sub,
          name: p.name ?? p.email?.split("@")[0],
          email: p.email ?? null,
          image: p.picture ?? null,
          provider: "google",
        };
      },
    }),
    Kakao({
      profile(p) {
        const acc = p.kakao_account ?? {};
        const prof = acc.profile ?? {};
        return {
          id: String(p.id),
          name: prof.nickname ?? `kakao_${p.id}`,
          email: acc.email ?? null,
          image: prof.profile_image_url ?? null,
          provider: "kakao",
        };
      },
    }),
  ],
  session: { strategy: "jwt" },
  callbacks: {
    async session({ session, token }) {
      session.user = {
        id: token.sub!,
        name: session.user?.name ?? null,
        email: session.user?.email ?? null,
        image: session.user?.image ?? null,
        provider: token.provider as string,
      };
      return session;
    },
    async jwt({ token, account }) {
      if (account) token.provider = account.provider;
      return token;
    },
  },
});

export { handler as GET, handler as POST };

 

체크리스트
- 누락 가능 필드는 null로 명시
- 세션에는 최소 키만 유지(id, provider, name/email/image)
- 배포 전 콘솔의 리다이렉트 URI·환경 변수 키 이름 일치 여부 확인

반응형