📘 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의 작업은 크게 보면 아래 흐름입니다.
- Google Cloud Console에서 OAuth 앱 생성
- 로컬 환경 변수(.env.local) 세팅
- Auth.js(v5) 기반 인증 서버 구성
- App Router API Route 연결
- Provider 분리로 Hydration 오류 해결
- 로그인 UI 연결 및 실제 오류 수정
👉 이 Part 1에서는 **1~2번 (환경 & OAuth 준비 단계)**에 집중합니다.
2️⃣ A단계 — Google Cloud Console 설정 (가장 중요)
Google OAuth에서 가장 실수가 많이 발생하는 단계입니다.
여기서 설정이 틀리면 코드는 완벽해도 로그인이 100% 실패합니다.
A-1. 프로젝트 생성
- https://console.cloud.google.com 접속
- 새 프로젝트 생성
- 프로젝트 이름: 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️⃣ 전체 요청 흐름을 단계별로 보면
✅ 로그인 버튼 클릭 시
- signIn("google") 실행
- 브라우저 → POST /api/auth/signin
- route.ts가 요청 수신
- auth.ts의 POST 핸들러 실행
- Google OAuth 페이지로 리디렉트
✅ Google 인증 후 돌아올 때
- Google → /api/auth/callback/google
- route.ts가 요청 수신
- auth.ts의 GET 핸들러 실행
- jwt / session 콜백 실행
- 로그인 완료
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 오류를 방지하고
전역 상태를 안정적으로 관리할 수 있었습니다.
'챗봇 공부 노트' 카테고리의 다른 글
| [9편] Zustand로 인증 상태 통합 (0) | 2026.01.25 |
|---|---|
| [8편] OAuth vs 자체 회원가입 — 서비스에 맞는 인증 전략 (0) | 2026.01.23 |
| [6편] MyPage분리 + 채팅 검색 기능 추가 (0) | 2026.01.20 |
| [5편] chat 관련 ui 페이지 추가 (0) | 2026.01.18 |
| [4편] 다크모드 + 레이아웃 환경 구축 (0) | 2026.01.16 |