Skip to content

라우트 가드

12.2.1. 전역 가드

  • 전역 가드는 router.beforeEach를 사용하여 등록합니다.
  • Vue Router 4에서는 next() 콜백 대신 반환값을 사용합니다. next()는 사용하지 않습니다.
  • 네비게이션을 허용하려면 true 또는 undefined를 반환합니다.
  • 네비게이션을 차단하려면 false 또는 리다이렉트 경로를 반환합니다.
typescript
// src/router/index.ts
router.beforeEach((to, from) => {
  // 인증이 필요하지 않은 페이지는 통과
  if (!to.meta.requiresAuth) {
    return true
  }

  const authStore = useAuthStore()

  if (!authStore.isAuthenticated) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  return true
})
  • router.afterEach는 페이지 제목 설정, 분석 이벤트 전송 등에 사용합니다.
typescript
router.afterEach((to) => {
  document.title = `${to.meta.title} | TQS`
})

12.2.2. 라우트별 가드

  • 특정 라우트에만 적용되는 가드는 beforeEnter 속성을 사용합니다.
  • 권한 기반 접근 제어는 meta.roles와 함께 라우트별 가드로 구현합니다.
  • 가드 로직이 복잡할 경우 별도 함수로 분리합니다.
typescript
// src/router/guards/roleGuard.ts
import type { NavigationGuardWithThis } from 'vue-router'
import { useAuthStore } from '@/stores/useAuthStore'

export function requireRoles(
  allowedRoles: string[]
): NavigationGuardWithThis<undefined> {
  return () => {
    const authStore = useAuthStore()
    const userRole = authStore.user?.role

    if (!userRole || !allowedRoles.includes(userRole)) {
      return { name: 'Forbidden' }
    }

    return true
  }
}
typescript
// src/router/routes.ts
import { requireRoles } from './guards/roleGuard'

{
  path: '/admin/dashboard',
  name: 'AdminDashboard',
  component: () => import('@/views/admin/DashboardView.vue'),
  beforeEnter: requireRoles(['admin', 'super-admin']),
  meta: { title: '관리자 대시보드', requiresAuth: true, roles: ['admin'] },
}

12.2.3. 컴포넌트 내 가드

  • 컴포넌트 내 가드는 Composition API의 onBeforeRouteLeaveonBeforeRouteUpdate를 사용합니다.
  • Options API의 beforeRouteEnter, beforeRouteLeave는 사용하지 않습니다.
  • 미저장 데이터가 있을 때 페이지 이탈을 경고하는 패턴에 onBeforeRouteLeave를 사용합니다.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

const hasUnsavedChanges = ref(false)

onBeforeRouteLeave(() => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('저장하지 않은 변경사항이 있습니다. 이동하시겠습니까?')
    if (!answer) {
      return false
    }
  }
})

onBeforeRouteUpdate(async (to) => {
  // 같은 컴포넌트에서 파라미터만 변경될 때 데이터를 다시 로드
  await fetchData(to.params.id as string)
})
</script>

12.2.4. 인증 가드 구현

  • 인증 가드는 다음 순서로 동작해야 합니다.
단계동작실패 시
1토큰 존재 여부 확인로그인 페이지로 리다이렉트
2토큰 유효성 검증토큰 갱신 시도
3사용자 정보 로드로그인 페이지로 리다이렉트
4권한 검증403 페이지로 리다이렉트
typescript
// src/router/guards/authGuard.ts
import { useAuthStore } from '@/stores/useAuthStore'

export async function authGuard(to: RouteLocationNormalized) {
  if (!to.meta.requiresAuth) {
    return true
  }

  const authStore = useAuthStore()

  // 1단계: 토큰 확인
  if (!authStore.token) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  // 2단계: 토큰 유효성 검증 및 갱신
  if (authStore.isTokenExpired) {
    const refreshed = await authStore.refreshToken()
    if (!refreshed) {
      authStore.clearAuth()
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }

  // 3단계: 사용자 정보 로드
  if (!authStore.user) {
    try {
      await authStore.fetchUser()
    } catch {
      authStore.clearAuth()
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }

  // 4단계: 권한 검증
  const requiredRoles = to.meta.roles
  if (requiredRoles && !requiredRoles.includes(authStore.user.role)) {
    return { name: 'Forbidden' }
  }

  return true
}
typescript
// src/router/index.ts
import { authGuard } from './guards/authGuard'

router.beforeEach(authGuard)
  • 인증 가드는 전역 beforeEach에 단일 진입점으로 등록합니다.
  • 로그인 페이지 리다이렉트 시 원래 경로를 query.redirect에 보존합니다.
  • 토큰 갱신 실패 시 저장된 인증 정보를 모두 초기화합니다.

TIENIPIA QUALIFIED STANDARD