[Vue] Pinia(인증 API 연동)

배창민·2025년 11월 28일
post-thumbnail

Pinia(인증 API 연동) 정리

Vue 3 + Pinia + Spring Security(JWT) 조합으로 인증을 구현할 때 핵심 정리


1. 전역 상태 관리와 인증

1-1. 왜 상태 관리 라이브러리가 필요한가

컴포넌트가 많아질수록 다음 문제가 터진다.

  • 부모 → 자식으로 props를 계속 내려야 하고
  • 자식 → 부모로 emit을 계속 올려야 하고
  • 중간 단계 컴포넌트가 많아지면
    “이 상태를 도대체 누가 들고 있는지” 추적하기가 힘들다.

전역 상태 관리 라이브러리를 쓰면:

  • 공통 상태를 store 하나에 모아 두고
  • 모든 컴포넌트가 그 store에서 읽고, store의 action으로만 상태를 바꾸게 해서
  • 구조를 단순하게 만들 수 있다.

특히 인증(auth)은 대표적인 전역 상태다.

  • 헤더, 사이드바, 마이페이지 등 여러 곳에서 동시에 로그인 상태를 보여줘야 하고
  • 로그인/로그아웃 시 전체 화면이 한 번에 일관되게 갱신돼야 한다.

그래서 보통 “전역 상태 = 인증부터 Pinia에 태운다” 라고 생각하면 된다.


1-2. Vuex vs Pinia

  • Vuex

    • Vue 2 시대 공식 상태 관리 라이브러리
    • state / mutation / action / getter 구조가 명확하지만 코드가 장황해지는 편
  • Pinia

    • Vue 3에서 공식 추천 상태 관리 도구
    • API가 훨씬 단순하고 Composition API랑 잘 맞는다
    • 스토어를 여러 개로 나눠 모듈처럼 관리하기 좋다
    • 타입스크립트 지원에 신경 쓴 구조

Vue 3 + Vite라면 그냥 Pinia를 기본값으로 두고 설계하는 게 자연스럽다.


2. Pinia 기본 개념

2-1. 설치와 초기 설정

npm install pinia
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount('#app');

이렇게 해 두면 어디서든 useAuthStore(), useCartStore() 같은 형태로 스토어를 사용할 수 있다.


2-2. 스토어란 무엇인가

하나의 스토어 = 하나의 전역 상태 모듈.

예:

  • useAuthStore: 로그인 상태, 토큰, 사용자 정보
  • useCartStore: 장바구니 목록, 합계 금액
  • useUiStore: 모달 열림/닫힘, 테마 등

각 스토어는 공통적으로 세 가지를 가진다.

  • state: 실제 상태 값
  • getters: state 기반 파생 값
  • actions: 비즈니스 로직 + 상태 변경 함수

2-3. Option Store vs Setup Store

  • Option Store

    • Vue Options API 스타일
    • state, getters, actions를 옵션으로 나눠 작성
  • Setup Store

    • Composition API 스타일
    • ref, reactive, computed를 그대로 사용

Composition API 기반 프로젝트라면 인증 스토어 같은 건 Setup Store로 두는 편이 자연스럽다.


3. Spring Security + JWT 인증 흐름 개요

3-1. 로그인 → 토큰 발급

전형적인 JWT 흐름:

  1. Vue가 /api/v1/auth/login 으로 아이디/비밀번호 전송

  2. Spring Security가 유저 검증

  3. 성공하면

    • Access Token (짧은 수명)
    • Refresh Token (긴 수명)
      을 발급
  4. 이후 모든 보호된 API 요청에 Access Token을 Authorization: Bearer ... 로 실어서 보냄

  5. 서버가 매 요청마다 JWT 서명/유효기간을 검증해서 인증 처리

서버는 세션에 상태를 들고 있지 않고, 토큰만 보고 판단하므로 구조가 거의 stateless에 가깝다.


3-2. Access Token vs Refresh Token

  • Access Token

    • 유효기간이 짧다 (예: 15분 ~ 30분)
    • 실제 API 인증에 사용
    • 탈취되더라도 피해 기간을 제한하기 위해 짧게 가져가는 편
  • Refresh Token

    • 유효기간이 길다 (예: 7일 ~ 30일)
    • 직접 API 호출에 사용하지 않고
    • 오직 “Access Token 재발급”에만 쓰는 마스터 키 역할

요약하면:

  • Access Token = 자주 쓰는 열쇠
  • Refresh Token = 새 열쇠를 만들어 주는 마스터 키

3-3. 401 vs 403

  • 401 Unauthorized

    • “인증이 안 됐다”

    • 토큰 없음, 만료, 위조 등의 상황

    • 프런트에서는 보통:

      • 토큰 재발급 시도 → 실패하면 로그아웃 + 로그인 화면 이동
  • 403 Forbidden

    • “인증은 됐지만 권한이 없다”

    • 일반 유저가 관리자 전용 API를 호출할 때 등

    • 프런트에서는:

      • “권한 없음” 안내 후 접근 가능한 화면으로 돌려보내는 패턴

라우터 가드, UI 권한 분기 설계할 때 401/403 구분이 중요하다.


4. Spring Security 설정 핵심 포인트

4-1. SecurityConfig 요약

SecurityConfig에서 하는 일:

  • cors() 활성화 + corsConfigurationSource() 로 CORS 정책 지정

  • csrf().disable()
    → 토큰 기반 인증에서는 CSRF를 비활성화하고 대신 다른 방식을 쓴다

  • sessionManagement().sessionCreationPolicy(STATELESS)
    → 세션을 생성하지 않는 JWT 기반 구조

  • authorizeHttpRequests

    • /api/v1/users, /api/v1/auth/login, /api/v1/auth/refreshpermitAll()
    • /api/v1/users/mehasAuthority("USER")
    • 그 외 → authenticated()
  • addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
    → UsernamePasswordAuthenticationFilter 전에 JWT 필터를 태워서 매 요청마다 토큰 검증

  • exceptionHandling()

    • 인증 실패 → RestAuthenticationEntryPoint
    • 인가 실패 → RestAccessDeniedHandler

4-2. CORS 설정

corsConfigurationSource() 에서:

  • setAllowedOrigins(List.of("http://localhost:5173"))
  • setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"))
  • setAllowedHeaders(List.of("Authorization", "Content-Type"))
  • setAllowCredentials(true) → 쿠키(Refresh Token) 허용

Vue 개발 서버(5173)에서 오는 요청만 허용하는 구조다.

이 경우 axios/fetch 에서 withCredentials: true 를 켜야 쿠키가 오간다.


5. AuthController + HttpOnly Refresh Token 패턴

5-1. 로그인

POST /api/v1/auth/login 흐름:

  1. authService.login(request) 로 토큰 두 개를 발급받는다.
  2. Refresh Token은 ResponseCookie로 만들어서 HttpOnly 쿠키로 내려보낸다.
  3. 응답 body에는 Access Token만 담아서 보낸다.

핵심 포인트:

  • Refresh Token은 HttpOnly + Secure(운영에서) + SameSite 옵션으로 보호
  • JS 코드에서는 Refresh Token에 직접 접근할 수 없다 → XSS에 더 안전

5-2. 토큰 재발급

POST /api/v1/auth/refresh 흐름:

  1. @CookieValue("refreshToken") 로 쿠키에 담긴 Refresh Token을 읽는다.
  2. 없거나 비어 있으면 401 응답
  3. 있으면 authService.refreshToken(refreshToken) 로 새 토큰 발급
  4. 새 Refresh Token을 또 HttpOnly 쿠키로 내려보내면서 “회전” 시킨다.
  5. body에는 새 Access Token만 담아서 보낸다.

이렇게 하면 Refresh Token이 탈취되더라도 회전 전략으로 제어가 가능하다.


5-3. 로그아웃

POST /api/v1/auth/logout 흐름:

  1. 서버 단에서 authService.logout(username) 으로 Refresh Token을 무효화 (DB/Redis 등)
  2. 응답에 Refresh Token 쿠키를 maxAge 0 으로 내려서 즉시 만료
  3. 프런트에서는 authStore.logout() 으로 상태 초기화

6. 인증용 Pinia 스토어 설계(authStore)

6-1. state 구성

대표적으로 이런 값들을 가진다.

  • accessToken
  • user (id, email, role 등)
  • loading (로그인/재발급 진행 중 여부)
  • authErrorMessage (최근 인증 에러 메시지)
  • 필요하면 initialized (스토어 초기 로딩이 끝났는지 여부)

Refresh Token은 HttpOnly 쿠키에 숨기고, 프런트 스토어에는 올리지 않는 패턴을 가정할 수 있다.


6-2. getters 예시

  • isLoggedIn!!accessToken
  • isAdminuser?.role === 'ADMIN'
  • displayName → 유저 이름/이메일 가공

컴포넌트 쪽에서는 이런 식으로 쓴다.

const auth = useAuthStore();

if (auth.isLoggedIn) { ... }
if (auth.isAdmin) { ... }

6-3. actions 예시

핵심 동작:

  1. login(credentials)

    • /auth/login 호출
    • 성공 시 accessToken + user 정보를 state에 저장
    • 필요하면 localStorage에도 저장
  2. logout()

    • /auth/logout 호출 (선택)
    • accessToken, user 초기화
    • localStorage/쿠키 상태도 정리
  3. refreshTokens()

    • /auth/refresh 호출 (Refresh Token은 HttpOnly 쿠키에서 사용)
    • 성공하면 accessToken 갱신
    • 실패하면 logout() 후 로그인 페이지 이동
  4. loadFromStorage()

    • 새로고침 시 localStorage에서 accessToken/user를 읽어와 초기화

컴포넌트는 단순히 authStore.login(), authStore.logout()만 부르면 되고, 내부 디테일은 스토어에 숨겨 두는 구조가 깔끔하다.


7. 토큰 저장 전략

7-1. Access Token

  • localStorage

    • 새로고침해도 로그인 유지
    • 여러 탭에서 공유 쉬움
    • 하지만 JS에서 접근 가능 → XSS에 취약
  • 메모리(Pinia state만 사용)

    • 새로고침 시 사라짐
    • “현재 탭에서만 유효한 세션”처럼 동작
    • 그래도 XSS 공격이 JS에 접근 가능한 건 같기 때문에 완전 안전한 것은 아님

실무에서는:

  • Access Token은 짧게 가져가고
  • 필요하면 localStorage + 정기적 재발급 패턴을 섞어서 사용한다.

7-2. Refresh Token

  • localStorage / sessionStorage

    • 구현은 쉽지만, XSS에 그대로 노출된다.
    • 유출되면 장기간 악용 가능
  • HttpOnly Cookie

    • JS에서 접근 불가 → XSS로 훔치기 훨씬 어려움
    • SameSite, Secure 옵션으로 CSRF 위험도 줄일 수 있다
    • 대신 CSRF 방어 전략을 따로 고려해야 한다.

실 서비스 기준으로는:

  • Access Token: 헤더로만 사용
  • Refresh Token: HttpOnly 쿠키 + 서버 측 검증

이 조합이 가장 많이 쓰이는 패턴이다.


8. Axios 인터셉터로 인증 자동화

8-1. Request Interceptor

역할:

  • 모든 요청이 나가기 전에 실행
  • authStore에서 accessToken을 꺼내서 Authorization: Bearer ... 헤더에 붙인다.

이렇게 하면 개별 API 호출코드에서 헤더를 일일이 건드릴 필요가 없다.

주의할 점:

  • /auth/login, /auth/refresh 같은 엔드포인트에는 토큰을 안 붙이는게 자연스럽다.
  • 조건문으로 예외 처리하는 식으로 관리한다.

8-2. Response Interceptor

역할:

  • 응답 에러를 한 곳에서 처리
  • 401이 떨어졌을 때 토큰 재발급을 시도하는 트리거 역할

흐름 예시:

  1. 보호 API에서 401 발생

  2. 아직 한 번도 재발급을 시도하지 않았다면

    • /auth/refresh 호출
    • 성공하면 accessToken 교체 후 실패했던 요청을 한 번 재시도
  3. 재발급도 401/403이면

    • authStore.logout()
    • 로그인 페이지로 이동

기억해야 할 포인트:

  • 무한 재시도를 막기 위해 “재시도 1회만 허용” 같은 플래그를 둔다.
  • 동시에 여러 요청이 401을 맞으면 재발급 요청이 여러 번 나갈 수 있으니, 학습 단계에서는 단순 로직부터 구현하는 게 좋다.

9. Vue Router로 인증이 필요한 페이지 보호

9-1. meta로 접근 조건 정의

라우트 정의에 meta를 붙여서 페이지별 권한 요구사항을 표현할 수 있다.

예:

const routes = [
  {
    path: '/mypage',
    component: MyPageView,
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    component: AdminView,
    meta: { requiresAuth: true, requiresAdmin: true }
  },
  {
    path: '/login',
    component: LoginView,
    meta: { guestOnly: true }
  }
];
  • requiresAuth: true → 로그인 필수
  • requiresAdmin: true → 로그인 + role=ADMIN
  • guestOnly: true → 로그인 안 한 사용자만 접근 가능

9-2. beforeEach 네비게이션 가드 흐름

전역 가드 개념:

  1. 라우트가 바뀔 때마다 실행

  2. to.meta.requiresAuth 체크

    • true인데 authStore.isLoggedIn === false/login으로 리다이렉트
  3. to.meta.requiresAdmin 체크

    • true인데 authStore.isAdmin === false → “권한 없음” 페이지로 리다이렉트
  4. to.meta.guestOnly 체크

    • true인데 이미 로그인 상태 → / 또는 /mypage 같은 곳으로 되돌리기

이렇게 하면 “라우트 정의 = 권한 정보”, “beforeEach = 실제 체크 로직” 구조로 깔끔하게 분리된다.


10. 전체 흐름 한 번에 정리

  1. Spring Security + JWT 로 백엔드 인증/인가 구조를 잡는다.

    • SecurityConfig에서 CORS, 세션 정책, 권한 규칙, JWT 필터, 예외 핸들러 설정
    • AuthController에서 Access/Refresh Token 발급, 재발급, 로그아웃 처리
    • Refresh Token은 HttpOnly 쿠키로 관리
  2. 프런트(Vue 3 + Vite)에서는

    • Pinia에 authStore 를 만들어 accessToken, user, login/logout/refresh 로직을 모아 둔다.
    • Axios 인스턴스 + 인터셉터로 토큰 첨부, 401 처리, 재발급 흐름을 자동화한다.
    • Vue Router의 meta + beforeEach 가드로 인증이 필요한 페이지를 보호한다.

결국 구조를 한 줄로 줄이면 이렇게 된다.

Spring Security가 토큰을 발급/검증하고,
Pinia가 인증 상태를 기억하고,
Axios 인터셉터와 라우터 가드가
“언제 토큰을 붙이고, 언제 재발급하고, 어디로 보내 줄지”를 자동으로 관리한다.

profile
개발자 희망자

0개의 댓글