챗봇 공부 노트

[9편] Zustand로 인증 상태 통합

frontend-diary-log 2026. 1. 25. 06:52

 

📘 Aura_AI Day 9 — Zustand로 인증 상태 통합

(완전 A~Z · 코드 · 문법 · 데이터 흐름까지 전부 설명)

안녕하세요.
오늘은 새로운 기능을 추가하는 날이 아니라,
이미 구현된 인증 기능을 “제대로 쓰기 위한 구조”를 설계한 날입니다.

Day 7–8에서

  • Google OAuth 로그인
  • 이메일/비밀번호 회원가입 및 로그인

기능은 모두 완성되었습니다.
하지만 **“로그인 상태를 어떻게 앱 전체에서 다룰 것인가?”**라는 문제는 남아 있었습니다.

그래서 Day 9의 목표는 명확했습니다.


0️⃣ 오늘 왜 이 작업을 했나요? (목적 선언)

기존 앱에서는 로그인 상태를 화면마다 다르게 판단하고 있었습니다.

  • 어떤 컴포넌트는 useSession()을 직접 사용하고
  • 어떤 화면은 잠깐 로그아웃처럼 보였다가 다시 바뀌고
  • Google 로그인과 ID/PW 로그인을 UI에서 계속 구분해야 했습니다.

그래서 오늘 한 작업은 다음 세 가지를 해결하기 위한 것이었습니다.

✅ 로그인 상태를 한 곳에서만 관리
✅ OAuth / ID 로그인 모두 완전히 동일한 방식으로 처리
✅ 새로고침해도 UI가 안정적으로 유지

이를 위해 Zustand를 도입했습니다.


1️⃣ Zustand란 무엇인가요?

Zustand는 전역 상태 관리 라이브러리입니다.
“앱 전체에서 공유해야 하는 데이터”를 한 곳에 저장하고,
모든 컴포넌트가 그 데이터를 동일하게 바라보게 합니다.

로그인 상태는 대표적인 전역 상태입니다.

  • 로그인 여부
  • 로그인한 유저 정보
  • 세션 로딩 중인지 여부

이것을 컴포넌트마다 따로 관리하면
상태 불일치와 중복 로직이 발생합니다.
그래서 전역 스토어가 필요합니다.

❓ 왜 React Context 대신 Zustand인가요?

  • Context는 값이 자주 바뀌면 리렌더링 범위가 커짐
  • 인증 상태는 앱 전반에 영향을 주므로 경량 스토어가 적합
  • Zustand는 설정이 단순하고 실무 사용 빈도가 높음

2️⃣ 오늘 새로 추가한 파일들 (A~Z 순서)

✅ 2-1. AuthUser 모델 (유저 타입 통합)

export interface AuthUser {
  id: string;
  name: string;
  email: string;
  image?: string | null;
  provider: "google" | "credentials";
}

🔍 설명

  • interface는 객체의 형태를 정의하는 TypeScript 문법입니다.
  • Google 로그인과 ID 로그인은 구조가 다르기 때문에
    앱 내부에서 사용할 “표준 유저 모델”이 필요했습니다.

✅ 효과

“이 앱에서 유저는 항상 이 모양이다.”

UI, 스토어, API 연동 모두에서
유저 구조를 의심하지 않고 바로 사용할 수 있습니다.


✅ 2-2. authStore.ts (Zustand 인증 스토어)

import { create } from "zustand";
import { AuthUser } from "@/types/auth";

interface AuthState {
  user: AuthUser | null;
  isAuthenticated: boolean;
  isLoading: boolean;

  setUser: (user: AuthUser | null) => void;
  setLoading: (loading: boolean) => void;
  clearUser: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  isLoading: true,

  setUser: (user) =>
    set({
      user,
      isAuthenticated: !!user,
      isLoading: false,
    }),

  clearUser: () =>
    set({
      user: null,
      isAuthenticated: false,
      isLoading: false,
    }),

  setLoading: (loading) => set({ isLoading: loading }),
}));

🔍 핵심 포인트

  • create() : Zustand 스토어 생성
  • isAuthenticated를 직접 관리하지 않고
    user 존재 여부로 자동 계산
  • isLoading은 새로고침 시 UI 깜빡임 방지용

✅ 2-3. AuthSync.tsx (Auth.js ↔ Zustand 브릿지)

"use client";

import { useEffect } from "react";
import { useSession } from "next-auth/react";
import { useAuthStore } from "@/store/authStore";
import { AuthUser } from "@/types/auth";

export default function AuthSync() {
  const { data: session, status } = useSession();
  const { setUser, clearUser, setLoading } = useAuthStore();

  useEffect(() => {
    if (status === "loading") {
      setLoading(true);
      return;
    }

    if (!session?.user) {
      clearUser();
      return;
    }

    const user: AuthUser = {
      id: session.user.id!,
      name: session.user.name!,
      email: session.user.email!,
      image: session.user.image,
      provider: session.user.provider as "google" | "credentials",
    };

    setUser(user);
  }, [session, status, setUser, clearUser, setLoading]);

  return null;
}

🔍 왜 필요한가요?

  • useSession()은 React Hook → 컴포넌트 안에서만 사용 가능
  • Zustand는 전역 스토어
  • 둘을 직접 연결할 수 없기 때문에 중간 브릿지 컴포넌트가 필요

✅ 2-4. next-auth.d.ts (타입 확장)

import "next-auth";

declare module "next-auth" {
  interface Session {
    user?: {
      id?: string;
      name?: string | null;
      email?: string | null;
      image?: string | null;
      provider?: "google" | "credentials";
    };
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id?: string;
    provider?: "google" | "credentials";
  }
}

🔍 이유

NextAuth 기본 타입에는 id, provider가 없습니다.
TypeScript 에러를 타입 확장으로 정식 해결했습니다.


3️⃣ 수정된 파일들

✅ providers.tsx

<SessionProvider>
  <ThemeProvider ...>
    <AuthSync />
    {children}
  </ThemeProvider>
</SessionProvider>

→ 앱 시작 시 항상 인증 상태 동기화


✅ auth.ts (JWT / Session 콜백)

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

async session({ session, token }) {
  if (session.user) {
    session.user.id = token.id as string;
    session.user.provider = token.provider as "google" | "credentials";
  }
  return session;
}

✅ Header / page.tsx

const { isAuthenticated, isLoading } = useAuthStore();
if (isLoading) return null;

→ UI는 이제 Zustand만 신뢰


4️⃣ 실제로 겪은 오류 정리

  • AuthSync import 누락
  • provider 타입 에러 → 타입 확장
  • Header에 useSession 잔존 → Zustand 기준 통일

5️⃣ 전체 데이터 흐름 (A → Z)

로그인 성공
→ Auth.js 세션 생성
→ auth.ts에서 JWT에 id/provider 저장
→ AuthSync가 세션 감지
→ Zustand에 user 저장
→ UI는 Zustand만 사용

6️⃣ 왜 이 방법을 선택했나요?

  • UI마다 useSession 직접 사용 ❌
  • 분기 로직 중복 ❌
  • 테스트/유지보수 어려움 ❌

현재 구조:

  • 한 번만 동기화
  • 단일 기준
  • 이후 API 연동 매우 쉬움

✅ Day 9 최종 체크리스트

  • AuthUser 모델 정의
  • Zustand 인증 스토어
  • AuthSync 브릿지
  • Provider 연결
  • UI Zustand 기준 통일
  • 타입 확장
  • 오류 해결