Auth.js로 카카오 로그인을 구현하다 보면 두 종류의 코드를 마주치게 된다.

버전 A — 내장 provider 사용:

import Kakao from "next-auth/providers/kakao"

export const { handlers, auth } = NextAuth({
  providers: [Kakao],
})

버전 B — 커스텀 OIDC 구현:

providers.push({
  id: "kakao",
  type: "oidc",
  issuer: "https://kauth.kakao.com",
  profile(profile) {
    return {
      id: String(profile.sub),
      name: profile.nickname,
      email: profile.email,
      image: profile.picture,
    }
  },
})

두 코드 모두 작동한다. 그런데 profile 필드 구조가 완전히 다르다. 이유가 있다.


1. 카카오에는 API가 두 개 있다

카카오는 로그인 후 사용자 정보를 가져오는 엔드포인트를 두 개 운영한다.

OAuth APIOIDC API
엔드포인트kapi.kakao.com/v2/user/mekapi.kakao.com/v1/oidc/userinfo
프로토콜OAuth 2.0OpenID Connect
응답 구조카카오 커스텀 (중첩)표준 OIDC 클레임 (flat)

내장 provider는 OAuth API를 쓰고, 커스텀 OIDC 구현은 OIDC API를 쓴다. 같은 카카오 로그인이지만 사용자 정보를 서로 다른 API에서 가져오기 때문에 profile 구조가 달라진다.


2. OAuth API — 내장 provider 방식

Auth.js 내장 Kakao provider의 실제 소스:

// @auth/core/providers/kakao.js
export default function Kakao(options) {
  return {
    id: "kakao",
    name: "Kakao",
    type: "oauth",
    authorization: "https://kauth.kakao.com/oauth/authorize?scope",
    token: "https://kauth.kakao.com/oauth/token",
    userinfo: "https://kapi.kakao.com/v2/user/me",
    profile(profile) {
      return {
        id: profile.id.toString(),
        name: profile.kakao_account?.profile?.nickname,
        email: profile.kakao_account?.email,
        image: profile.kakao_account?.profile?.profile_image_url,
      }
    },
  }
}

v2/user/me의 응답이 중첩 구조라서 kakao_account.profile.nickname처럼 두 단계를 들어가야 닉네임이 나온다. 이게 카카오 OAuth API의 응답 형태다.


3. OIDC API — 커스텀 구현 방식

카카오가 OIDC를 지원하는지 확인하는 방법:

curl https://kauth.kakao.com/.well-known/openid-configuration
{
  "issuer": "https://kauth.kakao.com",
  "userinfo_endpoint": "https://kapi.kakao.com/v1/oidc/userinfo",
  "claims_supported": [
    "sub", "nickname", "picture", "email", ...
  ]
}

OIDC 전용 userinfo 엔드포인트(v1/oidc/userinfo)가 따로 있고, 표준 OIDC 클레임(sub, nickname, email, picture)을 flat하게 반환한다. 중첩 구조가 없다.

Auth.js에서 커스텀 OIDC로 구현하면:

providers.push({
  id: "kakao",
  name: "Kakao",
  type: "oidc",
  issuer: "https://kauth.kakao.com",
  clientId: process.env.AUTH_KAKAO_ID,
  clientSecret: process.env.AUTH_KAKAO_SECRET,
  profile(profile) {
    return {
      id: String(profile.sub),
      name: profile.nickname ?? profile.name,
      email: profile.email,
      image: profile.picture,
    }
  },
})

issuer 하나만 지정하면 Auth.js가 /.well-known/openid-configuration을 조회해서 나머지 엔드포인트를 자동으로 찾는다. authorization, token, userinfo URL을 직접 쓸 필요가 없다.


4. 두 방식 비교

내장 provider커스텀 OIDC
코드import Kakao from "next-auth/providers/kakao"객체 직접 정의
userinfo 엔드포인트v2/user/me (OAuth)v1/oidc/userinfo (OIDC)
profile.idprofile.id.toString()String(profile.sub)
profile.nameprofile.kakao_account?.profile?.nicknameprofile.nickname
profile.emailprofile.kakao_account?.emailprofile.email
표준 준수카카오 커스텀OIDC 표준 클레임

어느 쪽을 쓰든 로그인 자체는 동작한다. 단, 두 방식을 섞어 쓰면 profile 필드에서 undefined가 나온다. 방식을 하나로 통일해야 한다.


5. 네이버 — OIDC가 있지만 OAuth가 현실적

네이버도 OIDC를 지원한다:

curl https://nid.naver.com/.well-known/openid-configuration
{
  "issuer": "https://nid.naver.com",
  "userinfo_endpoint": "https://openapi.naver.com/v1/nid/me",
  "scopes_supported": ["openid", "profile"]
}

그런데 scopes_supportedemail이 없다. OIDC로 연동하면 이메일을 받을 수 없다.

더 큰 문제는 네이버 OIDC의 profile() 콜백에서 어떤 필드를 꺼내야 하는지 공식 문서에 나와 있지 않다는 것이다. id_token이 어떤 클레임을 포함하는지 명세가 없다.

반면 기존 OAuth 방식의 userinfo 응답은 문서화가 잘 되어 있다:

providers.push({
  id: "naver",
  name: "Naver",
  type: "oauth",
  authorization: {
    url: "https://nid.naver.com/oauth2.0/authorize",
    params: { scope: "profile email" },
  },
  token: "https://nid.naver.com/oauth2.0/token",
  userinfo: "https://openapi.naver.com/v1/nid/me",
  clientId: process.env.AUTH_NAVER_ID,
  clientSecret: process.env.AUTH_NAVER_SECRET,
  profile(profile) {
    const response = profile.response  // 네이버 응답은 response 안에 중첩
    return {
      id: response.id,
      name: response.name ?? response.nickname,
      email: response.email,
      image: response.profile_image,
    }
  },
})

네이버 userinfo API는 응답이 response 객체 안에 중첩되어 있다. profile.email이 아니라 profile.response.email이다. 이걸 놓치면 이메일이 undefined로 저장된다.


6. 콘솔 설정

카카오:

  • 앱 키: REST API 키 (AUTH_KAKAO_ID)
  • Client Secret: 제품 설정 → 카카오 로그인 → 보안에서 생성 (AUTH_KAKAO_SECRET)
  • Redirect URI: /api/auth/callback/kakao
  • 동의항목: 닉네임, 카카오계정(이메일) — 이메일은 비즈니스 앱 전환 필요

네이버:

  • API 권한: 네아로(네이버 아이디로 로그인) → 이름, 이메일, 프로필 사진 선택
  • Callback URL: /api/auth/callback/naver

마무리

카카오는 OAuth와 OIDC 두 가지 API를 운영한다. Auth.js 내장 provider는 OAuth, 커스텀 구현은 OIDC를 쓰고, 각각 다른 엔드포인트에서 다른 구조의 응답을 받는다. 방식을 섞으면 profile 필드가 깨진다.

네이버는 OIDC를 지원하지만 이메일 scope가 없고 profile 클레임이 문서화되어 있지 않다. 프로덕션에서는 OAuth 방식이 안전하다.

이 설정 — Auth.js v5, DrizzleAdapter, JWT 콜백, 카카오(OIDC)·네이버(OAuth)·구글 동시 지원, Feature Toggle — 이 전부 kindie에 들어있다. .env.local에 키 넣고 config.ts에서 토글 켜면 된다.