테마 전환

Next.js OAuth 로그인 실전: Google, GitHub, WeChat 소셜 로그인 연동 가이드

Easton editorial illustration: monorepo project desk

지난주에 커뮤니티 프로젝트 하나를 맡았는데, 제품 관리자가 “Google 로그인 하나 추가해 주세요. 금방 되겠죠.”라는 말만 남겼습니다. 소셜 로그인은 예전에 튜토리얼도 몇 편 봤으니 어렵지 않을 거라고 생각했습니다. 하지만 설정에 오후 내내 매달렸고, redirect_uri 오류와 token 조회 실패가 이어지며 콘솔에는 빨간 오류 메시지가 끊임없이 나타났습니다. 공식 문서를 그대로 따라 했는데도 동작하지 않는다는 점이 가장 답답했습니다.

나중에야 문제는 코드가 아니라 OAuth 흐름을 너무 얕게 이해한 데 있다는 걸 알았습니다. authorization code, access token, callback 같은 개념은 하나씩 보면 이해되지만 함께 맞물리면 혼란스러웠습니다.

이번 글에서는 RFC 문서를 옮겨 놓은 듯한 추상적인 설명 대신, ‘택배 대리 수령’이라는 일상적인 비유로 OAuth 흐름을 쉽게 설명합니다. 왜 code와 token이 각각 필요한지, callback이 정확히 어떤 역할을 하는지, 어떤 설정에서 오류가 자주 발생하는지 알 수 있습니다. 이어서 Google(국제 표준), GitHub(개발자 친화적), WeChat(중국 내 서비스에 필요하지만 가장 까다로움) 로그인 설정을 단계별로 진행합니다.

솔직히 WeChat 로그인이 가장 까다로운 부분입니다. 문서가 불친절하고 기업 자격이 필요하며 로컬 디버깅도 번거롭습니다. 하지만 중국 내 프로젝트에서는 피하기 어려운 만큼, 터널링 디버깅 방법과 사용자 정의 Provider 설정을 포함해 제가 겪었던 문제를 모두 정리했습니다. 이 글을 다 읽고 나면 하나의 코드 구조로 중국 안팎의 로그인 요구 사항을 처리할 수 있을 것입니다.

OAuth 흐름은 정확히 어떻게 동작할까

택배 대리 수령으로 OAuth 이해하기

처음 OAuth를 접했을 때는 authorization code와 access token 같은 개념만 봐도 머리가 아팠습니다. 나중에 생각해 보니 택배를 대신 찾아 주는 과정과 같았습니다.

이런 상황을 떠올려 보세요. 택배 보관소에 내 소포(사용자 정보)가 있지만 지금 회사에 있어 직접 찾으러 갈 수 없습니다. 그때 친구(내 Next.js 애플리케이션)가 대신 찾아 주겠다고 합니다. 택배 보관소는 아무에게나 소포를 줄 수 없으므로 내가 권한을 부여했는지 확인해야 합니다. 흐름은 다음과 같습니다.

1. 친구에게 수령 코드를 줍니다. 이것이 authorization code입니다. ‘Google로 로그인’ 버튼을 누르면 Google 인증 페이지로 이동합니다. 동의하면 Google이 임시 code를 만들고 URL 매개변수로 애플리케이션에 전달합니다.

2. 친구가 수령 코드를 들고 택배 보관소로 갑니다. 애플리케이션 백엔드가 code로 access token을 교환하는 과정입니다. 택배 보관소는 이 친구가 정말 내가 신뢰하는 사람인지도 확인해야 하므로, 미리 등록한 친구의 신분증 번호(client_secret)도 필요합니다.

3. 신원을 확인한 뒤 소포를 건넵니다. 택배 보관소는 수령 코드와 신분증 번호가 모두 맞는지 확인한 후 친구에게 소포(사용자 정보)를 줍니다. 친구가 다시 내게 소포를 전달합니다. 이것이 access_token으로 사용자 정보를 가져오는 과정입니다.

핵심은 이렇습니다. 수령 코드(code)는 일회용이며 누군가 훔쳐보더라도 신분증 번호(secret)가 없으면 택배 보관소에서 소포를 받을 수 없습니다. 따라서 code는 브라우저 URL에 평문으로 전달할 수 있지만 secret은 반드시 백엔드에 숨겨야 합니다.

네 가지 핵심 단계

기술적인 흐름을 구체적으로 나누면 다음 네 단계입니다.

1단계: OAuth 서버 인증 페이지로 이동

사용자가 ‘Google로 로그인’을 클릭하면 프론트엔드가 URL을 만들고 사용자를 Google로 리디렉션합니다.

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=your_app_id
  &redirect_uri=http://localhost:3000/api/auth/callback/google
  &response_type=code
  &scope=email profile

각 매개변수의 의미는 다음과 같습니다.

  • client_id: Google에 등록된 애플리케이션의 식별자
  • redirect_uri: Google 인증 후 사용자를 돌려보낼 주소
  • scope: 접근하려는 사용자 정보의 범위

2단계: 사용자가 권한을 승인하고 code 수신

사용자가 Google 페이지에서 ‘허용’을 누르면 Google은 애플리케이션으로 다시 리디렉션하며 URL은 다음과 같이 바뀝니다.

http://localhost:3000/api/auth/callback/google?code=4/0AfJohXl...&state=random123

여기서 code가 수령 코드입니다. 유효 기간은 보통 5~10분으로 매우 짧고 한 번만 사용할 수 있습니다.

3단계: 백엔드에서 code를 access_token으로 교환

애플리케이션 백엔드(프론트엔드가 아니라는 점이 중요합니다)는 code와 client_secret을 함께 Google로 보냅니다.

const response = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: JSON.stringify({
    code: '방금 받은 code',
    client_id: 'your_client_id',
    client_secret: 'your_secret', // 절대 프론트엔드에 노출하면 안 됩니다
    redirect_uri: 'http://localhost:3000/api/auth/callback/google',
    grant_type: 'authorization_code'
  })
})

const { access_token } = await response.json()

access_token을 받아야 비로소 사용자 정보를 조회할 수 있는 실제 열쇠를 얻은 것입니다.

4단계: access_token으로 사용자 정보 조회

token이 있으면 Google API를 호출해 사용자 데이터를 가져올 수 있습니다.

const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
  headers: {
    Authorization: `Bearer ${access_token}`
  }
})

const user = await userInfo.json()
// { email: "[email protected]", name: "홍길동", picture: "프로필 이미지 URL" }

사용자 정보를 받은 뒤 자체 데이터베이스에서 사용자 레코드를 생성하거나 업데이트하고 session을 발급하면 로그인이 완료됩니다.

핵심 개념 빠른 참조표

자주 쓰는 용어를 한눈에 비교할 수 있도록 정리했습니다.

용어쉬운 설명확인할 수 있는 위치
Client ID공개해도 되는 애플리케이션 식별자.env.local 파일, 브라우저 URL 매개변수
Client Secret절대 공개하면 안 되는 애플리케이션 비밀번호백엔드 코드와 환경 변수에서만 사용
Authorization Code일회용 수령 코드콜백 URL의 code 매개변수
Access Token실제로 정보를 가져올 수 있는 열쇠백엔드 코드 내부, 프론트엔드에 전달하지 않음
Redirect URI인증 후 돌아올 주소OAuth 애플리케이션 설정, 인증 URL 매개변수
Scope접근하려는 권한 범위인증 URL 매개변수(예: email profile)
StateCSRF 공격을 막는 무작위 문자열인증 URL과 콜백 URL의 매개변수

State 매개변수의 역할: 인증을 시작할 때 무작위 문자열(예: abc123)을 만들어 session에 저장하고 OAuth 서버에도 전달합니다. 콜백에서는 반환된 state가 abc123인지 확인합니다. 다르다면 공격 가능성이 있으므로 요청을 거부합니다. NextAuth.js가 이 과정을 자동으로 처리합니다.

token을 바로 반환하면 안 되는 이유

어차피 마지막에 access_token이 필요하다면 URL로 token을 바로 반환하지 않고 굳이 code를 한 번 더 교환하는 이유가 궁금할 수 있습니다.

이유는 보안입니다.

브라우저 주소창의 URL은 평문이며 브라우저 기록, 서버 로그, 브라우저 확장 프로그램에 노출될 수 있습니다. token을 바로 반환하면 사용자 정보에 접근할 수 있는 열쇠를 평문으로 퍼뜨리는 셈이라 위험합니다.

반면 code가 탈취되더라도 다음 이유로 쓸 수 없습니다.

  1. Code는 한 번만 사용할 수 있고 사용 후 즉시 무효화됩니다.
  2. token으로 교환하려면 백엔드만 알고 있는 client_secret을 함께 제공해야 합니다.
  3. OAuth 서버는 redirect_uri가 일치하는지도 확인합니다.

따라서 공격자가 code를 가로채더라도 secret이 없으면 token을 얻을 수 없고 사용자 정보도 안전하게 보호됩니다.

이 설계를 ‘Authorization Code Flow’라고 하며, 백엔드 서버가 있는 웹 애플리케이션에 특히 적합한 OAuth 2.0의 가장 안전한 인증 방식 중 하나입니다. token을 바로 반환하는 ‘Implicit Flow’도 있지만 너무 안전하지 않아 더 이상 권장되지 않습니다.

NextAuth.js 빠르게 시작하기

NextAuth.js를 선택한 이유

Next.js에서 OAuth 로그인을 구현하는 방법은 여러 가지입니다. 전부 직접 작성할 수도 있고 라이브러리를 사용할 수도 있습니다. 저도 직접 만들어 보다가 수없이 문제를 겪은 뒤 NextAuth.js로 바꿨습니다.

추천하는 이유는 다음과 같습니다.

이유 1: 공식 추천과 성숙한 생태계

NextAuth.js는 Next.js 공식 문서가 추천하는 인증 솔루션이고 GitHub 스타가 7만 개가 넘으며 활발하게 유지되고 있습니다. 2024년 11월에 출시된 v5는 App Router와 Server Components를 완벽하게 지원하므로 호환성을 걱정할 필요가 없습니다.

이유 2: 30개 이상의 OAuth 제공자 내장

Google, GitHub, Facebook, Twitter 같은 주요 제공자를 바로 사용할 수 있어 코드 몇 줄만 설정하면 됩니다. WeChat처럼 내장되지 않은 서비스에도 사용자 정의 Provider 메커니즘을 제공하므로 OAuth 흐름을 처음부터 작성할 필요가 없습니다.

이유 3: 복잡한 로직 자동 처리

Session 관리, JWT 서명, 데이터베이스 저장, CSRF 방어를 모두 자동으로 처리합니다. 개발자는 ‘사용자가 처음 로그인할 때 환영 메일을 보낼지’ 같은 비즈니스 로직에 집중하면 됩니다.

핵심 설정 파일 구조

NextAuth.js의 핵심은 API route입니다. App Router(Next.js 13 이상)를 사용한다면 파일 경로는 다음과 같습니다.

app/api/auth/[...nextauth]/route.ts

[...nextauth]는 Next.js의 catch-all route입니다. 다음을 포함해 /api/auth/* 아래의 모든 요청을 이 파일이 처리한다는 뜻입니다.

  • /api/auth/signin - 로그인 페이지
  • /api/auth/callback/google - Google 콜백
  • /api/auth/signout - 로그아웃
  • /api/auth/session - 현재 session 조회

기본 설정은 다음과 같습니다.

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GitHubProvider from "next-auth/providers/github"

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
  // 선택 사항: 사용자 정의 로그인 페이지
  pages: {
    signIn: '/login',
  },
  // 선택 사항: 로그인 후 로직을 처리하는 콜백 함수
  callbacks: {
    async signIn({ user, account, profile }) {
      // 여기서 사용자가 허용 목록에 있는지 확인할 수 있습니다
      return true // false를 반환하면 로그인이 차단됩니다
    },
    async session({ session, token }) {
      // session에 추가 정보 삽입
      session.user.id = token.sub
      return session
    },
  },
}

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

마지막 줄의 export { handler as GET, handler as POST }는 매우 중요합니다. App Router에서는 HTTP 메서드를 처리하는 함수를 명시적으로 내보내야 합니다.

환경 변수 이름 규칙

NextAuth.js는 환경 변수 이름에 몇 가지 규칙이 있으며 특히 v5에서 주의해야 합니다.

필수 환경 변수:

# .env.local

# NextAuth 설정
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here

# 또는 새 이름 사용(v5 권장)
AUTH_SECRET=your-secret-key-here

# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

# 또는 AUTH_ 접두사 사용(v5에서 자동 인식)
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

# GitHub OAuth
GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret

# 또는
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret

핵심 사항:

  1. NEXTAUTH_URL: 애플리케이션의 전체 URL입니다. 개발 환경에서는 http://localhost:3000, 운영 환경에서는 https를 포함한 실제 도메인을 사용해야 합니다.

  2. NEXTAUTH_SECRET / AUTH_SECRET: JWT 서명에 사용하는 무작위 문자열 키입니다. 다음 명령으로 생성할 수 있습니다.

openssl rand -base64 32

절대로 노출하거나 Git에 커밋하면 안 됩니다. 유출되었다면 즉시 교체해야 합니다. 그렇지 않으면 다른 사람이 session을 위조할 수 있습니다.

  1. AUTH_ 접두사의 편의 기능: NextAuth.js v5는 AUTH_PROVIDER_IDAUTH_PROVIDER_SECRET 형식의 변수를 자동으로 인식하므로 코드에 process.env.XXX를 쓸 필요가 없어 더 편리합니다.

설치와 최소 설정

설명이 길었지만 시작은 간단합니다.

1단계: 설치

npm install next-auth
# 또는
pnpm add next-auth

2단계: API route 생성

app/api/auth/[...nextauth]/route.ts를 만들고 앞의 코드를 붙여 넣습니다.

3단계: .env.local 생성

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=openssl rand -base64 32 명령으로 생성한 문자열

4단계: 프론트엔드에 로그인 버튼 추가

// app/login/page.tsx
'use client'

import { signIn } from 'next-auth/react'

export default function LoginPage() {
  return (
    <div>
      <button onClick={() => signIn('google')}>
        Google로 로그인
      </button>
      <button onClick={() => signIn('github')}>
        GitHub로 로그인
      </button>
    </div>
  )
}

5단계: Provider로 애플리케이션 감싸기

클라이언트 컴포넌트에서 useSession()을 사용하려면 Provider를 추가해야 합니다.

// app/layout.tsx
import { SessionProvider } from 'next-auth/react'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  )
}

여기까지 설정하면 기본 구조가 준비됩니다. 물론 아직 Google과 GitHub의 OAuth 애플리케이션을 설정하지 않았기 때문에 로그인 버튼을 누르면 오류가 발생합니다. 이제 이 설정을 진행해 보겠습니다.

Google 로그인 실전 설정

Google Cloud Console 설정

Google OAuth 설정은 Google Cloud Console에서 OAuth 애플리케이션을 만드는 과정과 Next.js에 연동하는 과정으로 나뉩니다.

1단계: Google Cloud Console 접속

https://console.cloud.google.com 에 접속해 Google 계정으로 로그인합니다. 처음 사용하는 경우 프로젝트를 만들라는 안내가 나오며, ‘내 블로그’처럼 원하는 이름을 지정하면 됩니다.

2단계: Google+ API 활성화

Google+는 이미 종료되었지만 OAuth는 여전히 이 API에 의존합니다. 왼쪽 메뉴에서 ‘APIs & Services’ → ‘Library’를 선택하고 ‘Google+ API’를 검색한 다음 ‘Enable’을 누릅니다.

3단계: OAuth 사용자 인증 정보 생성

  • 왼쪽에서 ‘Credentials’ 선택
  • 상단의 ‘Create Credentials’ → ‘OAuth client ID’ 선택
  • 처음이라면 ‘OAuth consent screen’을 먼저 설정하라는 안내가 나옵니다. 애플리케이션 이름과 사용자 지원 이메일을 입력하고 나머지는 우선 건너뛸 수 있습니다.
  • Application type에서 ‘Web application’ 선택
  • Name에는 ‘Next.js App’처럼 원하는 이름 입력

4단계: Redirect URIs 설정(가장 중요)

오류가 가장 자주 발생하는 부분입니다. Authorized redirect URIs에는 다음 두 주소를 입력해야 합니다.

개발 환경:

http://localhost:3000/api/auth/callback/google

운영 환경(배포 후 추가):

https://yourdomain.com/api/auth/callback/google

다음 세부 사항에 주의하세요.

  • httphttps: 로컬 개발은 http를 사용하지만 운영 환경에서는 반드시 https를 써야 합니다. Google은 운영 환경의 http를 허용하지 않습니다.
  • 포트 번호: 로컬 서버가 3001에서 실행된다면 localhost:3001을 입력해야 하며 포트를 생략할 수 없습니다.
  • 경로: /api/auth/callback/google은 글자 하나도 틀리면 안 됩니다. 슬래시가 더 있거나 빠져도 안 됩니다.
  • 쿼리 매개변수를 추가하지 마세요. /google까지만 입력하면 되며 뒤의 ?code=xxx는 Google이 자동으로 붙입니다.

설정이 끝나면 ‘Create’를 누릅니다. 표시되는 Client ID와 Client Secret을 복사해 안전하게 보관합니다.

5단계: .env.local에 복사

GOOGLE_CLIENT_ID=your_Client_ID.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxx

Next.js 코드 연동

앞의 NextAuth.js 설정에 이미 GoogleProvider를 추가했으므로 환경 변수만 맞으면 작동합니다. 다만 다음처럼 조금 더 다듬을 수 있습니다.

// app/api/auth/[...nextauth]/route.ts
import GoogleProvider from "next-auth/providers/google"

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
}

여기서는 authorization.params를 추가했습니다.

  • prompt: "consent": 로그인할 때마다 인증 페이지를 표시하므로 테스트에 편리합니다. 운영 환경에서는 제거해도 되며 Google이 사용자의 선택을 기억합니다.
  • access_type: "offline": refresh token을 반환합니다. 사용자가 한 번 로그인한 뒤 offline 상태에서도 token을 갱신할 수 있습니다. 사용자 데이터에 장기간 접근해야 할 때만 추가하세요.
  • response_type: "code": authorization code flow를 명시적으로 사용합니다.

로그인 흐름 테스트

Next.js를 시작합니다.

npm run dev

http://localhost:3000/login 에 접속해 ‘Google로 로그인’을 누르면 Google 인증 페이지로 이동합니다. ‘허용’을 누른 뒤 애플리케이션으로 돌아오면 정상입니다.

원하는 페이지에서 session을 가져올 수 있습니다.

// app/page.tsx
import { getServerSession } from "next-auth"
import { authOptions } from "./api/auth/[...nextauth]/route"

export default async function Home() {
  const session = await getServerSession(authOptions)

  if (session) {
    return <div>안녕하세요, {session.user?.name}</div>
  }

  return <div>로그인하지 않음</div>
}

클라이언트 컴포넌트에서는 다음처럼 사용합니다.

'use client'
import { useSession } from "next-auth/react"

export default function Profile() {
  const { data: session, status } = useSession()

  if (status === "loading") return <div>불러오는 중...</div>
  if (!session) return <div>로그인하지 않음</div>

  return (
    <div>
      <img src={session.user?.image} alt="프로필 이미지" />
      <p>{session.user?.name}</p>
      <p>{session.user?.email}</p>
    </div>
  )
}

흔한 오류 해결

오류 1: redirect_uri_mismatch

전체 오류 메시지:

Error 400: redirect_uri_mismatch
The redirect URI in the request, http://localhost:3000/api/auth/callback/google,
does not match the ones authorized for the OAuth client.

원인: Google Console에 설정한 URI와 실제 callback URI가 완전히 일치하지 않습니다.

확인 단계:

  1. 브라우저 개발자 도구에서 Network 탭을 엽니다.
  2. 로그인 버튼을 누르고 Google로 이동하는 URL을 확인합니다.
  3. redirect_uri 매개변수를 찾아 전체 값을 복사합니다.
  4. Google Console로 돌아가 Authorized redirect URIs에 정확히 붙여 넣습니다.
  5. 프로토콜(http/https), 포트, 경로, 불필요한 슬래시가 있는지 확인합니다.

오류 2: Access blocked: This app’s request is invalid

OAuth consent screen을 설정하지 않았거나 테스트 사용자를 추가하지 않았다는 뜻입니다.

해결 방법:

  • Google Console → ‘OAuth consent screen’으로 이동
  • User Type에서 ‘External’(외부 공개) 또는 ‘Internal’(조직 내부) 선택
  • 애플리케이션 정보 입력
  • External이면서 아직 게시하지 않았다면 ‘Test users’에 테스트 계정 이메일 추가

오류 3: 포트 번호 문제

로컬 개발 서버는 localhost:3001에서 실행되는데 redirect URI가 localhost:3000으로 설정되어 있어도 오류가 발생합니다.

가장 간단한 방법은 항상 3000 포트를 사용하거나 Google Console에 여러 redirect URI(3000, 3001, 3002)를 추가하는 것입니다.

오류 4: HTTPS 요구 사항

운영 환경에 배포한 뒤에도 도메인이 http라면 Google이 요청을 거부합니다. 반드시 https를 사용해야 합니다. Vercel과 Netlify 같은 플랫폼은 https를 자동으로 제공하므로 걱정할 필요가 없습니다.

GitHub 로그인 설정

GitHub OAuth App 설정

GitHub의 OAuth 설정은 Google보다 간단합니다. API를 활성화할 필요 없이 OAuth App을 바로 만들면 됩니다.

1단계: Developer settings 열기

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 입력

GitHub의 callback URL은 비교적 유연하며 나중에 수정할 수 있어 Google만큼 엄격하지 않습니다.

Register application을 누르면 Client ID가 생성됩니다. 이어서 ‘Generate a new client secret’을 눌러 Secret을 만듭니다. 한 번만 표시되므로 꼭 저장하세요.

3단계: .env.local에 복사

GITHUB_ID=your_Client_ID
GITHUB_SECRET=your_Client_Secret

Next.js 연동

NextAuth.js 설정은 다음과 같습니다.

import GitHubProvider from "next-auth/providers/github"

export const authOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
}

추가 설정 없이 이것으로 끝입니다.

GitHub와 Google의 차이

비교 항목GoogleGitHub
설정 난이도보통, API 활성화 필요쉬움, 바로 생성
Redirect URI엄격, 완전히 일치해야 함유연, 와일드카드 지원
HTTPS 요구 사항운영 환경에서 필수localhost는 http 가능
Scope 설정명시적으로 지정해야 함기본값으로 충분함
사용자 정보email, name, picturelogin, email, avatar_url

한 가지 주의할 점은 사용자가 이메일 공개를 제한하면 GitHub의 이메일이 null일 수 있다는 것입니다. 따라서 코드에서 예외 처리가 필요합니다.

const userEmail = session.user?.email || '이메일이 제공되지 않음'

WeChat 로그인 설정(중국 내 환경)

WeChat 로그인 세 가지 방식

가장 혼동하기 쉬운 부분입니다. WeChat에는 적용 환경이 전혀 다른 세 가지 로그인 방식이 있습니다.

로그인 방식적용 환경요구 사항사용자 경험
Open Platform 웹사이트 애플리케이션독립 웹사이트기업 자격, 등록된 도메인, HTTPSQR 코드 로그인, PC 지원
공식 계정 웹페이지 인증WeChat 내부 H5 페이지인증된 공식 계정WeChat 브라우저에서만 사용
WeCom기업 내부 시스템WeCom 계정기업 직원만 사용

여기서는 독립적인 Next.js 웹사이트에 적합한 첫 번째 방식, 즉 Open Platform 웹사이트 애플리케이션 로그인을 설명합니다.

WeChat Open Platform 설정(높은 진입 장벽)

사전 조건:

  • 기업 사업자등록증(개인 개발자는 불가)
  • ICP 등록이 완료된 도메인
  • HTTPS 인증서

1단계: 개발자 계정 등록

https://open.weixin.qq.com 에 접속해 ‘등록’을 누르고 ‘웹사이트 애플리케이션 개발자’를 선택한 뒤 사업자등록증을 업로드하고 심사를 기다립니다(보통 영업일 기준 1~2일).

2단계: 웹사이트 애플리케이션 생성

심사가 통과되면 관리 센터 → 웹사이트 애플리케이션 → 웹사이트 애플리케이션 생성으로 이동해 다음 정보를 입력합니다.

  • 애플리케이션 이름
  • 애플리케이션 소개
  • 애플리케이션 공식 웹사이트: 실제 도메인(ICP 등록 필수)
  • 승인 콜백 도메인: 프로토콜과 경로를 제외한 도메인만 입력(예: yourdomain.com)

Google/GitHub와 달리 여기에는 전체 URL이 아니라 도메인만 입력합니다. WeChat은 이 도메인 아래의 모든 경로를 자동으로 일치시킵니다.

제출한 뒤 다시 심사를 기다립니다(1~7일). 통과되면 AppID와 AppSecret이 발급됩니다.

3단계: 환경 변수

WECHAT_APP_ID=your_AppID
WECHAT_APP_SECRET=your_AppSecret

NextAuth.js 사용자 정의 Provider

WeChat은 내장 Provider가 없으므로 직접 작성해야 합니다. 다행히 NextAuth.js는 사용자 정의 Provider 메커니즘을 제공합니다.

// app/api/auth/[...nextauth]/route.ts

const WeChatProvider = {
  id: "wechat",
  name: "WeChat",
  type: "oauth",
  authorization: {
    url: "https://open.weixin.qq.com/connect/qrconnect",
    params: {
      appid: process.env.WECHAT_APP_ID,
      scope: "snsapi_login",
      response_type: "code",
    },
  },
  token: {
    url: "https://api.weixin.qq.com/sns/oauth2/access_token",
    async request({ params, provider }) {
      const response = await fetch(
        `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${process.env.WECHAT_APP_ID}&secret=${process.env.WECHAT_APP_SECRET}&code=${params.code}&grant_type=authorization_code`
      )
      const tokens = await response.json()
      return { tokens }
    },
  },
  userinfo: {
    url: "https://api.weixin.qq.com/sns/userinfo",
    async request({ tokens }) {
      const response = await fetch(
        `https://api.weixin.qq.com/sns/userinfo?access_token=${tokens.access_token}&openid=${tokens.openid}`
      )
      return await response.json()
    },
  },
  profile(profile) {
    return {
      id: profile.unionid || profile.openid,
      name: profile.nickname,
      email: null, // WeChat은 이메일을 제공하지 않습니다
      image: profile.headimgurl,
    }
  },
}

export const authOptions = {
  providers: [
    WeChatProvider,
    // ...다른 providers
  ],
}

개발 환경 디버깅 방법

WeChat 로그인에서 가장 번거로운 부분은 로컬 디버깅입니다. HTTPS와 ICP 등록 도메인이 필요하기 때문입니다. 저는 보통 다음 두 가지 방법을 사용합니다.

방법 1: 터널링(권장)

ngrok 또는 cpolar를 사용해 로컬 3000 포트를 공개 주소에 연결합니다.

# ngrok 사용
ngrok http 3000

# 또는 cpolar 사용(중국 내에서 더 안정적)
cpolar http 3000

https://abc123.ngrok.io 같은 임시 도메인이 생성되면 이 도메인을 WeChat Open Platform의 승인 콜백 도메인으로 설정합니다.

방법 2: 테스트 계정 사용

WeChat은 기업 자격 없이 사용할 수 있는 테스트 계정을 제공합니다.

다만 테스트 계정은 본인만 사용할 수 있고 다른 사용자가 QR 코드를 스캔하면 ‘공식 계정을 팔로우하지 않았습니다’라는 안내가 표시됩니다.

WeChat 로그인의 고유한 특징

차이 1: openid와 unionid 반환

  • openid: 현재 애플리케이션에서 사용자를 구분하는 고유 식별자
  • unionid: 동일한 Open Platform 계정 아래의 모든 애플리케이션에서 사용자를 구분하는 고유 식별자(여러 애플리케이션을 연결해야 제공됨)

사용자 식별자에는 unionid를 사용하고, unionid가 없으면 openid를 사용하는 것이 좋습니다.

차이 2: 이메일을 제공하지 않음

WeChat API는 이메일을 반환하지 않으므로 profile의 email은 null입니다. 사용자 시스템에 이메일이 필요하다면 별도로 입력하도록 해야 합니다.

차이 3: 짧은 access_token 유효 기간

Google/GitHub의 token은 보통 1시간 유효하지만 WeChat은 2시간이며 하루 갱신 횟수도 제한되어 있습니다(기억하기로는 10회입니다). 따라서 token 갱신 로직을 적절히 처리해야 합니다.

마무리

OAuth 원리부터 세 가지 주요 플랫폼의 실전 설정까지 소셜 로그인 전체 과정을 살펴봤습니다.

핵심 요점:

  • OAuth가 code와 token의 두 단계를 사용하는 이유는 보안입니다. code는 브라우저에서 평문으로 전달되어도 괜찮지만 secret은 반드시 백엔드에 숨겨야 합니다.
  • NextAuth.js를 사용하면 Session 관리나 CSRF 방어 같은 저수준 로직 대신 비즈니스 로직에 집중할 수 있습니다.
  • Google은 설정이 가장 엄격해 redirect URI가 완전히 일치해야 하고, GitHub는 가장 친화적이며, WeChat은 진입 장벽이 가장 높지만 중국 내 서비스에는 필요합니다.
  • WeChat 로그인에는 기업 자격과 ICP 등록 도메인이 필요하며 개발 단계에서는 터널링과 테스트 계정으로 디버깅할 수 있습니다.

실용적인 팁:

  • NEXTAUTH_SECRET은 직접 작성하지 말고 openssl rand -base64 32로 생성하세요.
  • 포트를 바꿀 때 오류가 나지 않도록 Google Console에 여러 포트의 redirect URI를 등록하세요.
  • GitHub 이메일은 null일 수 있으므로 null 처리를 추가하세요.
  • 여러 애플리케이션에서 사용자를 구분할 때는 WeChat의 openid보다 unionid가 더 적합합니다.

솔직히 소셜 로그인에서 가장 어려운 부분은 코드를 작성하는 일이 아니라 OAuth의 설계 방식과 플랫폼별 설정 차이를 이해하는 것입니다. 이 글이 몇 가지 시행착오를 줄이는 데 도움이 되기를 바랍니다.

다음번에 ‘Google 로그인 하나 추가해 주세요’라는 요구를 받더라도 더는 설정에 오후를 통째로 쓰지 않을 것입니다.

Next.js OAuth 로그인 전체 설정 과정

OAuth 원리를 이해하는 단계부터 Google, GitHub, WeChat 로그인을 설정하는 전체 과정

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: OAuth 흐름 이해하기(택배 대리 수령 비유)

    OAuth의 핵심은 비밀번호를 서드파티 애플리케이션에 넘기지 않고, OAuth 제공자로부터 임시 통행증만 받아 오도록 권한을 부여하는 것입니다.

    택배 대리 수령 비유:
    • 나(사용자) → 로그인하려는 사람
    • 친구(Next.js 애플리케이션) → 대신 택배를 찾아 주는 사람
    • 택배 보관소(OAuth 제공자) → Google, GitHub, WeChat
    • 출입 카드(비밀번호) → 다른 사람에게 줄 수 없음
    • 임시 통행증(access_token) → 유효 기간과 권한 제한이 있음

    네 단계 흐름:
    1. 친구에게 수령 코드 전달(authorization code)
    2. 친구가 수령 코드+신분증을 들고 택배 보관소 방문(code+client_secret으로 access_token 교환)
    3. 택배 보관소가 신원 확인 후 소포 전달(사용자 정보)
    4. 친구가 내게 소포 전달(로그인 성공)

    핵심:
    • 수령 코드(code)는 일회용이며 유효 기간이 짧음(보통 10분)
    • 신분증(client_secret)은 반드시 비밀로 유지하고 서버에서만 사용
    • 임시 통행증(access_token)은 유효 기간과 권한 제한이 있음
  2. 2

    Step 2: Google 로그인 설정

    1. Google Cloud Console에서 OAuth 클라이언트 생성:
    • https://console.cloud.google.com 접속
    • 프로젝트 생성 → API 및 서비스 → 사용자 인증 정보 → OAuth 클라이언트 ID 만들기
    • 애플리케이션 유형: 웹 애플리케이션
    • 승인된 리디렉션 URI: http://localhost:3000/api/auth/callback/google

    2. client_id와 client_secret 확인

    3. NextAuth.js 설정:
    ```ts
    // app/api/auth/[...nextauth]/route.ts
    import NextAuth from 'next-auth'
    import GoogleProvider from 'next-auth/providers/google'

    export const authOptions = {
    providers: [
    GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    })
    ],
    }

    const handler = NextAuth(authOptions)
    export { handler as GET, handler as POST }
    ```

    4. 환경 변수 설정:
    ```
    GOOGLE_CLIENT_ID=your_client_id
    GOOGLE_CLIENT_SECRET=your_client_secret
    NEXTAUTH_URL=http://localhost:3000
    NEXTAUTH_SECRET=random_string
    ```

    5. 페이지에서 사용:
    ```tsx
    import { signIn } from 'next-auth/react'

    <button onClick={() => signIn('google')}>
    Google로 로그인
    </button>
    ```
  3. 3

    Step 3: GitHub 로그인 설정

    1. GitHub에서 OAuth App 생성:
    • https://github.com/settings/developers 접속
    • New OAuth App
    • Authorization callback URL: http://localhost:3000/api/auth/callback/github

    2. Client ID와 Client Secret 확인

    3. NextAuth.js 설정:
    ```ts
    import GitHubProvider from 'next-auth/providers/github'

    providers: [
    GitHubProvider({
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    })
    ]
    ```

    4. 환경 변수 설정:
    ```
    GITHUB_CLIENT_ID=your_client_id
    GITHUB_CLIENT_SECRET=your_client_secret
    ```

    핵심: GitHub 설정 과정은 Google과 비슷하며 OAuth 제공자만 다릅니다.
  4. 4

    Step 4: WeChat 로그인 설정(특수 처리)

    1. WeChat Open Platform에서 애플리케이션 등록:
    • https://open.weixin.qq.com 접속
    • 웹사이트 애플리케이션 생성
    • AppID와 AppSecret 확인
    • 기업 자격 필요

    2. 승인 콜백 도메인 설정:
    • 형식: yourdomain.com(http:// 또는 https:// 제외)
    • ICP 등록이 완료된 도메인 필요

    3. 사용자 정의 Provider 설정:
    ```ts
    import WeChatProvider from 'next-auth/providers/wechat'

    providers: [
    WeChatProvider({
    clientId: process.env.WECHAT_CLIENT_ID!,
    clientSecret: process.env.WECHAT_CLIENT_SECRET!,
    })
    ]
    ```

    4. 로컬 디버깅에 터널링 사용:
    • ngrok 또는 frp 사용
    • 콜백 주소를 터널링 주소로 설정
    • 테스트 완료 후 운영 주소로 변경

    핵심:
    • WeChat 로그인에는 기업 자격이 필요함
    • 로컬 디버깅에는 터널링을 사용
    • 사용자 고유 식별자에는 openid보다 unionid가 더 적합
  5. 5

    Step 5: 흔한 오류 해결

    오류 1: redirect_uri_mismatch
    • 원인: 콜백 주소 불일치
    • 해결: OAuth 제공자 콘솔에서 올바른 redirect_uri 설정
    • 주의: 로컬과 운영 환경의 콜백 주소를 모두 설정해야 함

    오류 2: 환경 변수 누락
    • 확인: .env.local 파일이 있는지 확인
    • 확인: 환경 변수 이름이 올바른지 확인
    • 확인: Vercel Dashboard에 환경 변수가 설정되었는지 확인

    오류 3: 로컬 테스트는 정상이지만 배포 후 오류 발생
    • 원인: 운영 환경과 로컬의 콜백 주소가 다름
    • 해결: OAuth 제공자 콘솔에 운영 환경의 redirect_uri 설정
    • 형식: https://yourdomain.com/api/auth/callback/google

    보안 권장 사항:
    • client_secret은 반드시 비밀로 유지하고 서버에서만 사용
    • state 매개변수로 CSRF 공격 방지
    • 콜백 URL의 state 매개변수 검증

FAQ

OAuth 흐름은 정확히 어떻게 동작하나요?
택배 대리 수령으로 OAuth를 이해해 보겠습니다.

상황: 택배 보관소에 내 소포(사용자 정보)가 있지만 지금 회사에 있어 직접 찾으러 갈 수 없습니다. 친구(Next.js 애플리케이션)가 대신 찾아 주겠다고 합니다.

흐름:
1. 친구에게 수령 코드 전달(authorization code)
• ‘Google로 로그인’ 버튼을 누르면 Google 인증 페이지로 이동합니다.
• 동의하면 Google이 임시 code를 만들어 URL 매개변수로 애플리케이션에 전달합니다.

2. 친구가 수령 코드를 들고 택배 보관소 방문(code+client_secret으로 access_token 교환)
• 애플리케이션 백엔드가 code+client_secret으로 Google에서 access_token을 교환합니다.
• Google은 이 친구가 실제로 신뢰받은 사람인지도 확인합니다(client_secret).

3. 신원 확인 후 소포 전달(사용자 정보)
• Google은 code+client_secret이 모두 맞으면 친구에게 소포(사용자 정보)를 건넵니다.
• 친구가 이를 내게 전달하면 로그인이 성공합니다.

핵심:
• 수령 코드(code)는 일회용이며 유효 기간이 짧습니다(보통 10분).
• 신분증(client_secret)은 반드시 비밀로 유지하고 서버에서만 사용해야 합니다.
• 임시 통행증(access_token)은 유효 기간과 권한 제한이 있습니다.

장점: 비밀번호를 서드파티 애플리케이션에 줄 필요 없이 OAuth 제공자로부터 임시 통행증을 받아 오도록 권한만 부여하면 됩니다.
redirect_uri_mismatch는 어떤 오류인가요?
오류 원인: 콜백 주소가 일치하지 않습니다.

OAuth 제공자는 콜백 주소를 검증하며, 설정한 값과 다르면 이 오류를 반환합니다.

해결 방법:
1. OAuth 제공자 콘솔에 올바른 redirect_uri 설정
2. 로컬 개발: http://localhost:3000/api/auth/callback/google
3. 운영 환경: https://yourdomain.com/api/auth/callback/google
4. 주의: 로컬과 운영 환경의 콜백 주소를 모두 설정해야 함

흔한 실수:
• 로컬 주소만 설정하고 운영 환경 주소를 설정하지 않음
• 콜백 주소 오타(슬래시가 더 있거나 빠짐)
• 프로토콜 오류(http와 https)

확인 방법:
• NextAuth.js의 기본 콜백 경로 확인: /api/auth/callback/[provider]
• OAuth 제공자 콘솔의 redirect_uri가 이 경로와 완전히 일치하는지 확인

주의: 설정이 적용되기까지 몇 분 정도 걸릴 수 있습니다.
WeChat 로그인이 특히 까다로운 이유는 무엇인가요?
문제:

1. 기업 자격 필요
• 개인 개발자는 신청할 수 없음
• 사업자등록증 등의 자료 필요
• 심사에는 보통 영업일 기준 1~3일 소요

2. 불친절한 문서
• 공식 문서의 설명이 충분히 명확하지 않음
• 오류 메시지가 구체적이지 않음
• 디버깅이 어려움

3. 번거로운 로컬 디버깅
• 터널링(ngrok 또는 frp) 필요
• 복잡한 콜백 주소 설정
• 테스트 환경 제약이 많음

4. 복잡한 설정
• 사용자 정의 Provider 설정 필요
• unionid와 openid의 차이
• 승인 콜백 도메인에 ICP 등록 필요

해결 방법:
• 터널링을 사용해 디버깅
• 사용자 정의 Provider 설정
• 사용자 고유 식별자에는 openid보다 unionid 사용
• 심사가 끝날 때까지 기다리기

가능하다면 Google 또는 GitHub 로그인을 우선 사용하고 WeChat 로그인은 보조 수단으로 추가하는 편이 좋습니다.
사용자 정의 Provider는 어떻게 설정하나요?
WeChat 로그인에는 사용자 정의 Provider가 필요합니다.

```ts
import type { OAuthConfig, OAuthUserConfig } from 'next-auth/providers'

function WeChatProvider(options: OAuthUserConfig<WeChatProfile>): OAuthConfig<WeChatProfile> {
return {
id: 'wechat',
name: 'WeChat',
type: 'oauth',
authorization: {
url: 'https://open.weixin.qq.com/connect/qrconnect',
params: {
appid: options.clientId,
redirect_uri: options.callbackUrl,
response_type: 'code',
scope: 'snsapi_login',
state: 'state',
},
},
token: {
url: 'https://api.weixin.qq.com/sns/oauth2/access_token',
},
userinfo: {
url: 'https://api.weixin.qq.com/sns/userinfo',
},
profile(profile) {
return {
id: profile.openid,
name: profile.nickname,
email: null,
image: profile.headimgurl,
}
},
...options,
}
}
```

핵심:
• authorization URL 설정
• token URL 설정
• userinfo URL 설정
• profile 함수 구현

주의: WeChat 로그인 설정은 비교적 복잡하므로 공식 문서를 참고하거나 기존 Provider를 사용하는 것이 좋습니다.
unionid와 openid는 무엇이 다른가요?
openid:
• 현재 애플리케이션 안에서 사용자를 구분하는 고유 식별자
• 애플리케이션마다 openid가 다름
• 단일 애플리케이션 환경에 적합

unionid:
• WeChat Open Platform 전체에서 사용자를 구분하는 고유 식별자
• 같은 사용자는 서로 다른 애플리케이션에서도 동일한 unionid를 가짐
• 여러 애플리케이션을 운영하는 환경에 적합

선택 기준:
• 단일 애플리케이션 → openid 사용
• 여러 애플리케이션 → unionid 사용

코드 예시:
```ts
// unionid 가져오기
const response = await fetch(
`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openid}`
)
const data = await response.json()
const unionid = data.unionid // 사용자 고유 식별자
```

핵심:
• 사용자 고유 식별자에는 openid보다 unionid가 더 적합
• unionid를 가져오려면 사용자 승인이 필요함
• unionid를 사용하려면 WeChat Open Platform 설정이 필요함
로컬에서 WeChat 로그인을 어떻게 디버깅하나요?
문제: WeChat 로그인은 승인 콜백 도메인을 설정해야 하지만 localhost는 설정할 수 없습니다.

해결 방법: 터널링 사용

1. ngrok 사용:
```bash
ngrok http 3000
```

2. 공개 주소 확인:
```
https://xxxxx.ngrok.io
```

3. 콜백 주소 설정:
• WeChat Open Platform에서 설정: https://xxxxx.ngrok.io/api/auth/callback/wechat
• .env.local에서 설정: NEXTAUTH_URL=https://xxxxx.ngrok.io

4. 테스트:
• https://xxxxx.ngrok.io 접속
• WeChat 로그인 클릭
• 흐름 테스트

주의 사항:
• ngrok 무료 버전의 주소는 바뀌므로 재시작할 때마다 업데이트해야 함
• 운영 환경에서는 ngrok을 사용하지 말 것
• 테스트 완료 후 운영 주소로 변경

더 안정적인 주소가 필요하면 frp 같은 자체 구축 터널링 도구를 사용하는 것이 좋습니다.

5분 읽기 · 게시일: 2025년 12월 19일 · 수정일: 2026년 9월 8일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog