SPA(Vue) 환경에서 백엔드와 안전하게 인증을 연동하는 가장 일반적인 방식은 JWT (JSON Web Token) 기반 인증입니다.
Vue + Axios + TypeScript 조합에서는 이 JWT 인증 구조를 어떻게 구현하고 관리하는 것이 좋을까요?
이번 포스팅에서는 아래와 같은 순서로 JWT 기반 로그인 인증 흐름을 구축하는 방법을 실습 예제와 함께 소개합니다.
1. JWT 인증이란?
JWT는 로그인 성공 시 서버가 클라이언트에게 발급해주는 디지털 서명된 토큰입니다.
이 토큰을 클라이언트가 저장하고, 이후 서버에 요청을 보낼 때 Authorization 헤더에 포함시키면 서버는 이를 인증 수단으로 사용합니다.
- ✅ 상태를 서버에 저장하지 않기 때문에 Stateless 인증이 가능
- ✅ REST API와 궁합이 좋고 SPA에 많이 사용됨
- ❗ 토큰 자체는 정보를 담고 있어 노출되면 위험 → 보안 고려 필수
2. 인증 흐름
- 사용자가 로그인 정보를 제출
- 서버가 토큰을 응답 (accessToken)
- 클라이언트는 토큰을 저장 (localStorage 또는 cookie)
- Axios 요청 시 Authorization 헤더에 토큰 자동 추가
- 로그아웃 시 저장된 토큰 삭제
3. Axios 인터셉터 구성 – 토큰 자동 첨부
// src/lib/axios.ts
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 5000,
})
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
export default api
💡 이렇게 하면 Axios 요청마다 Authorization 헤더를 자동으로 추가해줍니다.
4. 로그인 / 로그아웃 처리 – Pinia Store 활용
// src/stores/authStore.ts
import { defineStore } from 'pinia'
import api from '@/lib/axios'
interface LoginPayload {
username: string
password: string
}
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('access_token') || '',
isAuthenticated: !!localStorage.getItem('access_token'),
error: '',
}),
actions: {
async login(payload: LoginPayload) {
try {
const res = await api.post('/auth/login', payload)
this.token = res.data.accessToken
localStorage.setItem('access_token', this.token)
this.isAuthenticated = true
this.error = ''
} catch (err: any) {
this.error = err?.response?.data?.message || '로그인 실패'
this.isAuthenticated = false
}
},
logout() {
this.token = ''
this.isAuthenticated = false
localStorage.removeItem('access_token')
},
},
})
5. 로그인 폼 컴포넌트 예제
<!-- src/views/LoginView.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useAuthStore } from '@/stores/authStore'
const username = ref('')
const password = ref('')
const authStore = useAuthStore()
const handleLogin = async () => {
await authStore.login({ username: username.value, password: password.value })
}
</script>
<template>
<div>
<h1>로그인</h1>
<input v-model="username" placeholder="아이디" />
<input v-model="password" placeholder="비밀번호" type="password" />
<button @click="handleLogin">로그인</button>
<p v-if="authStore.error" style="color: red">{{ authStore.error }}</p>
</div>
</template>
6. 인증된 사용자만 접근 가능한 라우트 보호
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
const routes = [
{ path: '/login', component: () => import('@/views/LoginView.vue') },
{
path: '/mypage',
component: () => import('@/views/MyPageView.vue'),
meta: { requiresAuth: true },
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else {
next()
}
})
export default router
✅ 라우터 가드에서 로그인 여부를 체크해서, 인증이 필요한 페이지는 로그인 후에만 접근할 수 있게 처리합니다.
7. 마무리 체크포인트
항목설명
🔐 인증 방식 | JWT (access token only) |
💾 저장소 | localStorage 사용 (refresh token은 쿠키에 저장 고려) |
🚫 로그아웃 처리 | 토큰 삭제 + 상태 초기화 |
🚨 에러 처리 | 로그인 실패 메시지 UI 표시 |
🔁 자동 로그인 유지 | localStorage에서 token 읽어 초기 상태 구성 |
✅ 보안 측면 추가 고려 사항
- XSS 공격 방지를 위해 가능한 경우 HttpOnly Cookie 방식 사용
- Access Token이 만료되었을 때 자동으로 Refresh Token을 사용해 갱신하는 로직 추가
- CSRF 대응을 위한 CORS 설정 및 서버 측 보안 검토
✍️ 마무리
SPA에서의 인증은 무조건 복잡한 것이 아니라, 구조를 정해놓고 구성하면 오히려 관리가 쉬워집니다.
위 예제를 기반으로 다음 단계에서는 다음과 같은 주제를 다루는 것이 좋습니다:
- Refresh Token 기반 자동 로그인 갱신
- 로그인 유지 UX 개선 (스피너, 알림 등)
- 쿠키 기반 인증 vs LocalStorage의 보안 비교
- JWT 토큰 페이로드 파싱 및 만료 체크
'FrontEND > vue' 카테고리의 다른 글
Pinia에서 비동기 API 호출을 관리하는 실전 패턴 (with Axios + TypeScript) (0) | 2025.05.01 |
---|---|
배열, 중첩 객체, 에러 응답을 안전하게 처리하는 TypeScript + DTO 패턴 가이드 (0) | 2025.04.30 |
TypeScript에서 안전하게 API 응답 객체 처리 및 Axios + DTO 구조로 타입 안정성 높이기 (0) | 2025.04.29 |
TypeScript에서 객체를 안전하게 초기화하는 방법 (0) | 2025.04.28 |
TypeScript에서 옵셔널 파라미터(?), 옵셔널 체이닝(?.), ??, || 정리 (0) | 2025.04.27 |