챗봇 공부 노트

[7편] Google OAuth 인증 체계 구축

frontend-diary-log 2026. 1. 21. 07:10

📘 Aura_AI Day 7

Google OAuth 인증 체계 구축 — 개요 & 환경 설정

안녕하세요.
오늘은 Aura_AI 프로젝트의 인증 파트 핵심인 Google OAuth 로그인 체계
Next.js App Router 환경에 맞게 처음부터 끝까지 구축했습니다.

단순히

“로그인 버튼이 눌리면 되는 수준”

이 아니라,

  • 외부 인증(OAuth)의 전체 흐름 이해
  • Auth.js(v5) 기반 세션 구조 설계
  • 실제 개발 중 마주친 환경 변수 / UI / Hydration 오류 분석
    까지 함께 정리한 날입니다.

🎯 Day 7 목표

  • Google 로그인 버튼 클릭 → OAuth 인증 성공
  • 인증 후 유저 정보를 세션에 안전하게 저장
  • App Router 환경에서 안정적으로 동작
  • 로그인 성공 시 /chat 페이지로 리다이렉트

✅ 오늘 작업 결과 한 줄 요약

  • Google OAuth 로그인 성공
  • 로그인 후 /chat 정상 이동
  • session.user.id 접근 가능
  • Day 8 (Zustand, middleware)로 바로 확장 가능한 상태 완성

①  Google OAuth를 사용하기 위한 외부 환경 준비와 보안 설정

1️⃣ Google OAuth 전체 설정 흐름 (A → Z 개요)

Day 7의 작업은 크게 보면 아래 흐름입니다.

  1. Google Cloud Console에서 OAuth 앱 생성
  2. 로컬 환경 변수(.env.local) 세팅
  3. Auth.js(v5) 기반 인증 서버 구성
  4. App Router API Route 연결
  5. Provider 분리로 Hydration 오류 해결
  6. 로그인 UI 연결 및 실제 오류 수정

👉 이 Part 1에서는 **1~2번 (환경 & OAuth 준비 단계)**에 집중합니다.


2️⃣ A단계 — Google Cloud Console 설정 (가장 중요)

Google OAuth에서 가장 실수가 많이 발생하는 단계입니다.
여기서 설정이 틀리면 코드는 완벽해도 로그인이 100% 실패합니다.

A-1. 프로젝트 생성

  1. https://console.cloud.google.com 접속
  2. 새 프로젝트 생성
    • 프로젝트 이름: Aura_AI

A-2. OAuth 동의 화면 설정

  • User Type: External
  • 앱 이름: Aura AI
  • 사용자 지원 이메일: 본인 이메일
  • Scope: 기본값 유지
  • 테스트 사용자: 본인 Google 계정 이메일 추가

⚠️ 주의
로고 경고가 떠도 실제 로그인 동작에는 영향 없습니다.


A-3. OAuth 클라이언트 ID 생성

  • 애플리케이션 유형: 웹 애플리케이션
  • 이름: Aura_AI_Web

✅ 승인된 리디렉션 URI (핵심)

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

📌 반드시 지켜야 할 사항

  • 마지막 / 붙이지 않기
  • http / https 정확히 구분
  • 오타 발생 시 → 로그인 100% 실패

 


3️⃣ B단계 — 환경 변수 세팅 (.env.local)

OAuth에서 발급받은 값들은 절대 코드에 직접 쓰지 않습니다.

📁 .env.local

GOOGLE_CLIENT_ID=발급받은_클라이언트_ID
GOOGLE_CLIENT_SECRET=발급받은_클라이언트_SECRET

AUTH_SECRET=랜덤한_긴_문자열
AUTH_URL=http://localhost:3000

📌 AUTH_SECRET 생성 방법

openssl rand -base64 32

→ 출력된 실제 문자열만 복사해서 사용해야 합니다.
(명령어 자체를 넣으면 인증 오류 발생)


4️⃣ .env 와 .env.local 차이 (중요 개념)

🔹 .env

  • 공통 환경 변수
  • 팀과 공유 가능한 값
  • Git에 커밋될 수도 있음
  • ❌ 민감 정보 비추천
NEXT_PUBLIC_APP_NAME=Aura AI

🔹 .env.local

  • 로컬 개발 전용
  • OAuth Secret, API Key 등 민감 정보 전용
  • Git에서 자동으로 무시됨 (.gitignore)
GOOGLE_CLIENT_SECRET=xxxx

중요

Git이 .env.local을 못 읽는 게 아니라,
의도적으로 Git이 추적하지 않도록 설정된 파일입니다.


5️⃣ 실무에서의 정석 구조

  • 로컬 개발
    → .env.local
  • GitHub 저장소
    → .env.example만 커밋
  • 배포 환경(Vercel)
    → Dashboard에서 Environment Variables 직접 등록

②. 실제로 인증이 어떻게 동작하는지,
그리고 왜 Auth.js(v5) + 이 구조를 선택했는지

 

1️⃣ C단계 — Auth.js(v5 beta)를 선택한 이유

npm install next-auth@beta

Next.js App Router 환경에서는
Auth.js **v5(@beta)**가 사실상 정석입니다.

왜 v5인가?

  • App Router와 구조적으로 잘 맞음
  • handlers(GET / POST) 기반 API 설계
  • 인증 로직을 **한 파일(auth.ts)**로 집중 관리
  • 이후 DB Adapter, middleware 확장이 쉬움

👉 “지금은 OAuth지만, 나중에 인증 서버로 확장 가능”한 구조


2️⃣ D단계 — auth.ts의 역할 (한 줄 요약)

auth.ts는 Google 로그인 설정 파일이 아니라,
인증 상태 흐름(JWT → Session)을 설계한 파일입니다.

이 파일 하나가 다음을 모두 담당합니다.

  • Google OAuth Provider 설정
  • JWT 생성 및 관리
  • 클라이언트 Session 구조 정의
  • /api/auth/* 요청의 실제 처리 로직

3️⃣ auth.ts 전체 코드

📍 위치: /auth.ts

import NextAuth from "next-auth";
import Google from "next-auth/providers/google";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.id = user.id;
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string;
      }
      return session;
    },
  },
});

4️⃣ NextAuth()가 반환하는 것들의 의미

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth(...)

Auth.js v5에서는 NextAuth()가 여러 기능을 묶어서 반환합니다.

항목역할

handlers.GET / POST /api/auth/* 요청 처리
auth() 서버 컴포넌트에서 세션 조회
signIn() 로그인 트리거
signOut() 로그아웃 처리

👉 App Router에 맞게 API / 서버 / 클라이언트에서 재사용 가능


5️⃣ Provider 설정 — Google OAuth

providers: [
  Google({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  }),
],

이 코드의 의미

  • Google을 인증 Provider로 사용
  • 민감 정보는 .env.local에서만 관리
  • !는 TypeScript에게
  • “이 값은 undefined가 아니다”라고 명시

📌 실제 값이 없으면 런타임 에러 발생 → 설정 실수 즉시 감지 가능


6️⃣ 핵심 포인트 — callbacks (진짜 중요한 부분)

이 auth.ts의 핵심은 callbacks입니다.

✅ jwt 콜백

async jwt({ token, user }) {
  if (user) token.id = user.id;
  return token;
}

언제 실행될까?

  • 최초 로그인 시
  • 세션이 갱신될 때마다

무엇을 하나?

  • 로그인 최초 1회에만 user 존재
  • 그때 user.id를 JWT(token)에 저장

📌 JWT = 서버가 유지하는 인증 상태의 원본


✅ session 콜백

async session({ session, token }) {
  if (session.user) {
    session.user.id = token.id as string;
  }
  return session;
}

언제 실행될까?

  • 클라이언트에서 useSession() 호출 시
  • /api/auth/session 요청 시

무엇을 하나?

  • JWT에 저장된 token.id
  • → 클라이언트에서 쓰기 쉬운 session.user.id로 전달

📌 클라이언트는 JWT를 직접 만지지 않음 → 보안 & 안정성 확보


7️⃣ 왜 jwt → session 구조로 설계했을까?

❌ 잘못된 접근

session.user.id = user.id;
  • 로그인 직후에는 동작
  • 새로고침 / SSR 이후 user 없음 ❌

✅ 현재 방식 (정석)

user (로그인 시)
 → jwt (서버 인증 상태)
   → session (클라이언트 전달)
  • 새로고침에도 ID 유지
  • SSR / CSR 환경 모두 안정적

8️⃣ 다른 방법도 있었는데 왜 이걸 선택했을까?

❌ session-only 방식

  • 구현은 쉬움
  • 확장성 약함
  • middleware 연동 불리

❌ 처음부터 DB Adapter 연결

  • 초기 복잡도 급상승
  • Day 7 단계에서는 과함

✅ 현재 방식의 장점

  • App Router 최적화
  • DB 연동 시 구조 그대로 확장
  • middleware, Zustand와 궁합 좋음
  • 실무/면접에서 설명 가능

9️⃣ 면접용 설명 예시

Q. auth.ts 파일은 어떤 역할을 하나요?

Google OAuth 인증을 설정하고,
로그인 시 생성된 JWT에 유저 ID를 저장한 뒤
이를 클라이언트 세션으로 전달하는 인증 핵심 파일입니다.
App Router 환경에서 API 라우트와 인증 로직을 한 곳에서 관리하도록 설계했습니다.


export { GET, POST } from "@/auth";

③ . /api/auth/* 요청이 어떻게 auth.ts로 위임되는지 정리

1️⃣ E단계 — Auth API Route 연결의 목적

왜 API Route가 필요한가요?

Google OAuth 로그인 과정에서는
브라우저와 서버 사이에서 자동으로 여러 요청이 발생합니다.

예를 들면 다음과 같습니다.

  • /api/auth/signin
  • /api/auth/callback/google
  • /api/auth/session
  • /api/auth/signout

👉 이 요청들을 어디선가 받아서 처리해줘야
로그인이 정상적으로 동작합니다.


2️⃣ Pages Router(v4) 방식과의 차이

❌ 예전 방식 (Pages Router)

// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";

export default NextAuth({
  providers: [...],
});
  • 한 파일에
    • API 라우트
    • 인증 설정
    • 로직
      전부 몰아넣음

👉 App Router 구조와는 맞지 않음


3️⃣ App Router에서 API는 어떻게 동작할까?

App Router에서는
HTTP 메서드 단위로 API를 정의합니다.

export async function GET() {}
export async function POST() {}

즉,

  • GET 요청이면 → GET 함수
  • POST 요청이면 → POST 함수

가 반드시 필요합니다.


4️⃣ auth.ts에서 이미 GET / POST를 만들었다

Part 2에서 봤던 이 코드가 핵심입니다.

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({...});

이 의미는:

Auth.js가
/signin, /callback, /session, /signout 등
모든 인증용 HTTP 로직을 이미 만들어두었고,
그걸 GET / POST 핸들러 형태로 꺼내준 것

즉, 우리는 새로 API를 만들 필요가 없습니다.


5️⃣ route.ts는 무엇을 하는 파일인가?

📍 위치
app/api/auth/[...nextauth]/route.ts

export { GET, POST } from "@/auth";

이 파일은 로직을 구현하지 않습니다.

이 한 줄의 정확한 의미는 다음과 같습니다.

“auth.ts에서 이미 만들어진
GET / POST 인증 핸들러를
이 API 경로에 그대로 연결하겠다”


6️⃣ [...nextauth]의 의미 (중요)

/api/auth/anything

처럼 /api/auth 아래로 들어오는 모든 요청을
전부 이 파일이 받습니다.

예시:

  • /api/auth/signin
  • /api/auth/callback/google
  • /api/auth/session

⬇️ 전부 여기로 들어옴

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

그리고 그 요청을 다시 ⬇️

GET / POST → auth.ts

로 전달합니다.


7️⃣ 전체 요청 흐름을 단계별로 보면

✅ 로그인 버튼 클릭 시

  1. signIn("google") 실행
  2. 브라우저 → POST /api/auth/signin
  3. route.ts가 요청 수신
  4. auth.ts의 POST 핸들러 실행
  5. Google OAuth 페이지로 리디렉트

✅ Google 인증 후 돌아올 때

  1. Google → /api/auth/callback/google
  2. route.ts가 요청 수신
  3. auth.ts의 GET 핸들러 실행
  4. jwt / session 콜백 실행
  5. 로그인 완료

8️⃣ “자동으로 연결된다”의 진짜 의미

문서에서 자주 나오는 문장:

/api/auth/* 경로가 자동으로 Auth.js에 연결됩니다

이 말은 ❌ 마법이 아닙니다.

정확히는:

우리가 GET / POST를 auth.ts에 위임했기 때문에
자동처럼 보이는 것

입니다.


9️⃣ 왜 이렇게 분리했을까? (설계 이유)

❌ 한 파일에 다 넣는 방식

  • App Router와 구조적으로 충돌
  • 재사용성 낮음
  • 테스트/확장 어려움

✅ 지금 구조의 장점

  • 인증 로직 → auth.ts 한 곳
  • API 라우트 → 얇고 단순
  • middleware / server component에서 auth() 재사용 가능
  • 유지보수, 확장성 모두 좋음

🔥 면접용 한 줄 설명

App Router에서는 API가 HTTP 메서드 기반이기 때문에,
Auth.js v5에서 제공하는 GET/POST 핸들러를
route.ts에서 그대로 연결해
/api/auth/* 요청을 처리하도록 구성했습니다.


④. 왜 Providers를 분리했는지,
Hydration 오류가 왜 발생했고 어떻게 해결했는지

 

1️⃣ F단계 — Provider 설정의 목적

Day 7에서 설정한 Provider 구조는 다음과 같습니다.

// app/providers.tsx
"use client";

import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
        {children}
      </ThemeProvider>
    </SessionProvider>
  );
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

이 구조의 목표는 단 하나입니다.

“클라이언트 전용 상태(Session, Theme)를
App Router 구조에 맞게 안전하게 관리한다.”


2️⃣ 왜 Providers 파일을 따로 만들었을까?

❌ 가능한 다른 방법 (하지만 비추천)

// app/layout.tsx
"use client";

import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }) {
  return (
    <SessionProvider>
      <ThemeProvider>{children}</ThemeProvider>
    </SessionProvider>
  );
}

이 방식의 문제점은 명확합니다.

❌ 문제 1: 루트 레이아웃이 Client Component가 됨

  • "use client"가 붙는 순간
  • layout.tsx 전체가 클라이언트 컴포넌트가 됩니다

👉 App Router의 핵심 장점인
Server Component 기반 구조를 포기하게 됩니다.


3️⃣ 그래서 Server / Client 경계를 나눴습니다

현재 구조의 핵심은 이것입니다.

파일역할

layout.tsx Server Component (구조, HTML, 레이아웃)
providers.tsx Client Component (상태, Context)

즉,

layout.tsx  → 서버
providers.tsx → 클라이언트

👉 App Router가 권장하는 정석 구조


4️⃣ SessionProvider는 왜 전역에 있어야 할까?

<SessionProvider>
  {children}
</SessionProvider>

이 Provider가 하는 일은 다음과 같습니다.

  • /api/auth/session 자동 호출
  • 로그인 상태를 React Context로 관리
  • useSession()을 어디서든 사용 가능하게 함

즉,

전역 인증 상태 관리자

이기 때문에
페이지마다 두는 것이 아니라 앱 전체를 감싸야 합니다.


5️⃣ ThemeProvider는 왜 Hydration 오류를 만들까?

<ThemeProvider attribute="class" defaultTheme="system" enableSystem>

이 설정은 내부적으로 다음을 수행합니다.

  • 서버 렌더링 시:
  • <html>
  • 클라이언트 렌더링 시 (다크모드라면):
  • <html class="dark">

👉 서버 HTML과 클라이언트 HTML이 달라짐
👉 Hydration mismatch 발생


6️⃣ suppressHydrationWarning의 정확한 의미

<html lang="ko" suppressHydrationWarning>

이 속성의 의미는 다음과 같습니다.

“이 요소의 서버/클라이언트 차이는
내가 의도한 것이니 React가 경고하지 않도록 한다.”

❗ 오류를 숨기는 것이 아니라
의도된 차이를 허용하는 선언입니다.


7️⃣ Day 7에서 실제로 발생한 Hydration 오류들

❌ 오류 1. Theme class mismatch

  • 원인: next-themes가 클라이언트에서 <html> class 변경
  • 해결: <html suppressHydrationWarning>

❌ 오류 2. button 중첩 오류

<button>
  <button>...</button>
</button>
  • React DOM 규칙 위반
  • 서버/클라이언트 렌더 결과 불일치

✅ 해결

<div role="button">...</div>

8️⃣ 왜 이 구조가 실무에서 “정석”인가?

✅ 장점 정리

  • App Router 철학에 맞는 구조
  • Server Component 이점 유지
  • Hydration 오류 최소화
  • Provider 확장 쉬움
<SessionProvider>
  <ThemeProvider>
    <ToastProvider>
      <ModalProvider>
        {children}
      </ModalProvider>
    </ToastProvider>
  </ThemeProvider>
</SessionProvider>

👉 실제 실무 구조 그대로 확장 가능


9️⃣ 면접에서 이렇게 설명

App Router에서는 루트 레이아웃을 서버 컴포넌트로 유지하는 것이 중요하기 때문에,
인증(Session)과 테마처럼 클라이언트 전용 상태는
별도의 providers.tsx로 분리했습니다.
이 구조를 통해 hydration 오류를 방지하고
전역 상태를 안정적으로 관리할 수 있었습니다.