일상이 개발

Next.js 인증 상태 유지 전략 완전정복 – JWT, 쿠키, 리프레시 토큰, 자동 로그아웃까지 실전 가이드

아빠고미 2025. 5. 21. 12:08
반응형

Next.js 앱에서 사용자 인증 상태 유지 전략 – 로그인 세션, 토큰 관리, 자동 로그아웃까지 완전 정복

Next.js 기반의 웹 서비스에서 로그인 이후 인증 상태를 어떻게 유지할 것인가는 서비스 안정성과 보안, 사용자 경험을 결정짓는 핵심 요소입니다.

로그인을 했는데 새로고침 시 로그아웃되거나, 브라우저를 닫았다 다시 켜면 상태가 초기화되는 문제를 겪어보셨다면 인증 상태 유지 전략이 제대로 구현되지 않은 것입니다.

Next.js 인증 상태 유지 전략 완전정복 – JWT, 쿠키, 리프레시 토큰, 자동 로그아웃까지 실전 가이드

이번 글에서는 실전 프로젝트에 바로 활용 가능한 인증 상태 관리 전략을 다음 구조로 정리합니다:

  • ✅ 인증 토큰(JWT)의 저장 위치 결정
  • ✅ 로그인 후 상태 유지 구조 (쿠키, 로컬스토리지)
  • ✅ 클라이언트/서버 인증 동기화
  • ✅ 자동 로그아웃, 토큰 만료 처리
  • ✅ 보안 강화를 위한 실전 설계 팁

1. 🔐 인증 상태란 무엇인가?

✅ 정의

사용자가 로그인한 뒤, 페이지를 이동하거나 새로고침을 하더라도 로그인 상태가 유지되는 것을 의미합니다.

📦 인증 상태 구성 요소

  • 토큰: JWT 또는 세션 ID
  • 저장소: localStorage, sessionStorage, cookie
  • 사용자 정보: userId, email, role 등
  • 상태 관리: Zustand, Context, Redux 등

2. 🧾 인증 토큰 저장 위치 – localStorage vs Cookie

✅ 비교표

구분 localStorage cookie
접근 범위 클라이언트 전용 서버/클라이언트 모두
자동 전송 ⭕ (httpOnly 쿠키)
XSS 보안 ❌ 취약 ⭕ httpOnly로 보호 가능
사용 편의성 ⭕ 간단함 △ 쿠키 파싱 필요

✅ 권장 전략

  • 보안 중심: httpOnly 쿠키 + 서버 인증
  • 간단한 프로젝트: localStorage + 클라이언트 상태 관리

3. 📦 로그인 후 인증 상태 유지 구조

✅ localStorage 방식

// 로그인 시 토큰 저장
localStorage.setItem('accessToken', token);

✅ Zustand 예시

// store/authStore.ts
import { create } from 'zustand';

export const useAuthStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

📌 로그인 후 상태 초기화

useEffect(() => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    fetch('/api/me', {
      headers: { Authorization: `Bearer ${token}` },
    }).then(res => res.json()).then(data => {
      setUser(data);
    });
  }
}, []);

➡️ 새로고침 시에도 localStorage에 저장된 토큰을 기준으로 사용자 정보를 다시 불러와 상태를 유지합니다.

4. 🍪 쿠키 기반 인증 상태 유지 전략

✅ 쿠키로 인증 토큰 저장하기

쿠키는 서버에서도 접근할 수 있고, httpOnly 속성을 부여하면 클라이언트 JavaScript에서 접근할 수 없어 XSS 공격에 강한 보안 환경을 제공합니다.

📦 로그인 응답에서 쿠키 설정

// /api/login.ts
import { cookies } from 'next/headers';

export async function POST(req: Request) {
  const { email, password } = await req.json();
  const token = await authenticate(email, password);

  cookies().set('access_token', token, {
    httpOnly: true,
    maxAge: 3600,
    path: '/',
  });

  return new Response(JSON.stringify({ success: true }));
}

📦 클라이언트에서 토큰 접근 안됨

// localStorage나 getCookie로 접근 ❌
// 반드시 서버사이드에서 쿠키를 읽어야 함

5. 🖥️ SSR 환경에서 인증 상태 유지

✅ getServerSideProps 활용

export async function getServerSideProps(context) {
  const { req } = context;
  const token = req.cookies['access_token'];

  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  const user = await verifyToken(token);

  return {
    props: { user },
  };
}

➡️ SSR에서는 클라이언트 상태와 무관하게 서버에서 쿠키를 읽어 인증 상태를 직접 판별할 수 있습니다.


6. ⏳ 자동 로그아웃 구현하기

✅ 왜 필요한가?

  • 보안 강화 (PC를 잠깐 비운 사이 세션 유지 방지)
  • 토큰 만료 기반 자동 로그아웃 처리

📦 클라이언트 타이머 방식 예시

// 로그인 시 시작
const startAutoLogoutTimer = () => {
  setTimeout(() => {
    localStorage.removeItem('accessToken');
    window.location.href = '/login';
  }, 1000 * 60 * 60); // 1시간 후 자동 로그아웃
};

✅ JWT 자체 만료 확인

import jwtDecode from 'jwt-decode';

const token = localStorage.getItem('accessToken');
const decoded = jwtDecode(token);

if (decoded.exp * 1000 < Date.now()) {
  localStorage.removeItem('accessToken');
  alert('로그인 세션이 만료되었습니다.');
}

➡️ 리프레시 토큰 전략과 함께 적용 시 안정적인 로그인 상태 유지 + 자동 로그아웃 처리가 가능합니다.

7. 🔄 리프레시 토큰 전략 – 긴 세션 유지의 핵심

✅ 리프레시 토큰이란?

Access Token의 유효 시간이 짧을 경우, 사용자가 로그인 상태를 유지하기 위해 보다 긴 만료시간을 가진 Refresh Token을 함께 발급받습니다.

📌 구조 요약

  • 🔑 Access Token: 15분 ~ 1시간 (httpOnly 쿠키 or localStorage)
  • 🔁 Refresh Token: 7일 이상 (httpOnly + Secure Cookie)

📦 서버에서 토큰 발급 예시

// POST /api/login
cookies().set('access_token', accessToken, { httpOnly: true });
cookies().set('refresh_token', refreshToken, { httpOnly: true, secure: true });

📦 토큰 재발급 흐름

  1. 1️⃣ access_token 만료 → 401 Unauthorized
  2. 2️⃣ 클라이언트가 /api/refresh-token 호출
  3. 3️⃣ 서버가 refresh_token 유효성 확인 후 access_token 재발급

➡️ 이 방식은 보안성과 사용자 경험을 동시에 잡을 수 있습니다.


8. 🚪 인증 리디렉션 UX 설계

✅ 인증이 필요한 페이지 접근 시 UX

// useEffect or SSR에서
if (!user) {
  router.replace('/login?redirect=/target-page');
}

📦 로그인 후 원래 페이지로 복귀

const router = useRouter();
const redirectTo = router.query.redirect || '/';

useEffect(() => {
  if (loginSuccess) {
    router.replace(redirectTo);
  }
}, [loginSuccess]);

✅ UX 개선 팁

  • 🔁 로그인 후 이전 경로로 복귀
  • ⚠️ 비인가 접근 시 "권한 없음" 메시지 제공
  • 🔐 로그인 중에는 로딩 스피너 or skeleton UI 제공

9. ✅ 인증 상태 유지 실무 체크리스트

📌 기본 설계 체크

  • ✔️ JWT 토큰 사용 여부
  • ✔️ 저장소: localStorage vs cookie 결정
  • ✔️ 사용자 상태 전역 관리 방식 (Zustand 등)

📌 보안 체크

  • ✔️ httpOnly + Secure 쿠키 적용
  • ✔️ 토큰 만료 및 재발급 처리
  • ✔️ XSS/CSRF 방지 고려

📌 UX 체크

  • ✔️ 로그인 유지, 자동 로그아웃 흐름 구현
  • ✔️ 로그인 후 리디렉션 UX
  • ✔️ 사용자 정보 캐싱 전략 검토

긴 글 읽어주셔서 감사합니다! 공감, 댓글, 공유는 다음 글 제작에 큰 힘이 됩니다 🙌

반응형