Skip to content

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)**를 구축해 보겠습니다.

Moon HomeLab Engineering Knowledge Hub