다크 모드
Chapter 11. 라우터 네비게이션 가드를 활용한 인가(Authorization) AOP
"10장까지 발전한
BoardApp.vue에 URL 라우팅을 입히고, 로그인하지 않은 사용자의 글쓰기 직접 진입을 관문(Gateway)에서 원천 차단하자."
11.1 단일 화면에서 SPA 라우팅 아키텍처로의 전환
지금까지 BoardApp.vue는 목록과 상세, 작성 폼이 하나의 화면에 모여 있었습니다.
이제 실제 서비스처럼 브라우저 URL에 따라 화면을 분할해야 합니다:
/: 기술 Q&A 목록 화면 (BoardListView.vue)/post/:id: 특정 게시글 상세 및 댓글 트리 화면 (PostDetailView.vue)/write: 새 글 작성 화면 (PostWriteView.vue)
하지만 누군가 로그인을 하지 않은 상태에서 주소창에 http://localhost:3000/write를 직접 치고 들어오면 화면이 그대로 노출되는 보안 문제가 발생합니다.
각 컴포넌트마다 if (!isLoggedIn) router.push('/login')을 일일이 복붙하는 대신,
스프링의 Filter/Interceptor처럼 단일 진입점에서 모든 페이지 접근을 가로채는 AOP(관점 지향 프로그래밍) 가드 파이프라인을 구축합니다.
11.2 라우트 메타 필드(meta)의 선언적 설계
Vue Router 4의 각 라우트 정의에 선언적으로 보안 규칙을 부여합니다.
[코드 11-1] src/router/index.ts
typescript
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean; // 로그인 필수 여부
title?: string;
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'BoardList',
component: () => import('@/views/BoardListView.vue'),
meta: { title: '사내 기술 게시판 목록' }
},
{
path: '/post/:id',
name: 'PostDetail',
component: () => import('@/views/PostDetailView.vue'),
meta: { title: '게시글 상세 보기' }
},
{
path: '/write',
name: 'PostWrite',
component: () => import('@/views/PostWriteView.vue'),
// 🌟 선언적 인가 규칙 명시: 로그인이 반드시 필요한 보호 구역!
meta: {
requiresAuth: true,
title: '새 글 작성'
}
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
meta: { title: '로그인' }
}
];
export const router = createRouter({
history: createWebHistory(),
routes
});11.3 전역 네비게이션 가드: router.beforeEach 인가 파이프라인
컴포넌트가 마운트되기 직전에 실행되는 가드에서 비인가 접근을 차단합니다.
[코드 11-2] 보안 인가 가드와 리다이렉트 체인
typescript
import { router } from './router';
import { useAuthStore } from '@/stores/useAuthStore';
router.beforeEach(async (to, from) => {
const authStore = useAuthStore();
// 1. 브라우저 탭 타이틀 동적 변경 AOP
if (to.meta.title) {
document.title = `${to.meta.title} - 사내 인트라넷`;
}
// 2. 인증(Authentication) 검사
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
console.warn(`🚨 [AOP Guard] 미인증 사용자의 글쓰기 진입 차단: ${to.fullPath}`);
// 🌟 핵심: 사용자가 원래 가려던 목적지(to.fullPath)를 redirect 쿼리로 보존!
return {
path: '/login',
query: { redirect: to.fullPath }
};
}
return true;
});11.4 로그인 완료 후 원래 쓰려던 글쓰기 화면으로 자동 복귀
로그인 화면(LoginView.vue)에서는 인증 성공 후 보존된 redirect 쿼리를 확인하여 사용자를 목적지로 돌려보냅니다:
typescript
// LoginView.vue
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
async function handleLogin() {
await authStore.login(username.value, password.value);
// 원래 가려던 /write 화면이 있으면 그곳으로 즉시 이동, 없으면 메인(/)으로!
const targetRedirect = (route.query.redirect as string) || '/';
router.replace(targetRedirect);
}이제 사용자는 "글을 쓰려다가 로그인이 풀려 로그인 화면으로 갔더라도, 로그인 즉시 원래 쓰려던 글쓰기 페이지로 자연스럽게 돌아오는" 완벽한 엔터프라이즈 UX를 누리게 됩니다.
11.5 다음 챕터로 이어지는 질문
이제 화면 라우팅과 인가 가드까지 완성되었습니다.
하지만 소스 코드의 네트워크 통신 부분을 보십시오:
현재 컴포넌트와 스토어 곳곳에 axios.get('/api/posts'), axios.post(...) 처럼 Raw Axios가 노출되어 있습니다.
만약 백엔드에서 JWT 토큰이 만료되었을 때 Refresh Token으로 자동 갱신해야 하거나, 서버가 죽어 500 에러를 뿜어낼 때 컴포넌트가 흰색 화면으로 크래시되는 것은 어떻게 막아야 할까요?
이어지는 Chapter 12에서는 컴포넌트에서 Axios를 완전히 도려내어 postRepository 패턴으로 격리하고, Axios 인터셉터 AOP와 **프론트엔드 에러 바운더리(onErrorCaptured)**를 구축해 보겠습니다.