네비게이션 가드는 라우트 전환을 가로채어 인증 확인, 권한 검사, 데이터 프리페치 등을 수행하는 Vue Router의 핵심 기능입니다.

면접에서 "인증이 필요한 페이지를 어떻게 보호하나요?"라고 물으면, beforeEach 가드와 메타 필드 를 활용한 패턴을 설명할 수 있어야 합니다.


전역 가드

TYPESCRIPT
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */]
})

// beforeEach — 모든 라우트 전환 전에 실행
router.beforeEach(async (to, from) => {
  const isAuthenticated = checkAuth()

  // 인증이 필요한 페이지인데 로그인하지 않은 경우
  if (to.meta.requiresAuth && !isAuthenticated) {
    // 로그인 페이지로 리다이렉트, 원래 가려던 경로를 쿼리로 전달
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  // 이미 로그인한 상태에서 로그인 페이지 접근 시
  if (to.name === 'login' && isAuthenticated) {
    return { name: 'home' }
  }

  // undefined 또는 true를 반환하면 네비게이션 허용
})

// afterEach — 네비게이션 완료 후 실행 (네비게이션 변경 불가)
router.afterEach((to, from) => {
  // 페이지 제목 변경
  document.title = (to.meta.title as string) || '기본 제목'

  // 분석 추적
  // analytics.trackPageView(to.fullPath)
})

// beforeResolve — 모든 컴포넌트 가드와 비동기 라우트 해결 후 실행
router.beforeResolve(async (to) => {
  if (to.meta.requiresData) {
    try {
      await fetchRequiredData(to.params.id as string)
    } catch (error) {
      return { name: 'error' }
    }
  }
})

export default router

라우트별 가드

TYPESCRIPT
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    // 이 라우트에만 적용되는 가드
    beforeEnter: (to, from) => {
      const hasPermission = checkPermission('dashboard')
      if (!hasPermission) {
        return { name: 'forbidden' }
      }
    }
  },

  // 여러 가드를 배열로 전달
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    beforeEnter: [checkAuth, checkAdminRole, logAccess]
  }
]

// 가드 함수를 분리하여 재사용
function checkAuth(to, from) {
  if (!isAuthenticated()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
}

function checkAdminRole(to, from) {
  if (getUserRole() !== 'admin') {
    return { name: 'forbidden' }
  }
}

컴포넌트 내 가드

VUE
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

const hasUnsavedChanges = ref(false)

// 라우트를 떠나기 전 — 저장하지 않은 변경사항 확인
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('저장하지 않은 변경사항이 있습니다. 정말 나가시겠습니까?')
    if (!answer) {
      return false  // 네비게이션 취소
    }
  }
})

// 같은 컴포넌트에서 라우트 파라미터만 변경될 때
onBeforeRouteUpdate(async (to, from) => {
  // /users/1 → /users/2
  // await fetchUser(to.params.id)
})
</script>

네비게이션 가드 실행 순서

PLAINTEXT
1. 이전 컴포넌트의 onBeforeRouteLeave
2. 전역 beforeEach
3. 재사용 컴포넌트의 onBeforeRouteUpdate
4. 라우트의 beforeEnter
5. 비동기 컴포넌트 해결
6. 새 컴포넌트의 beforeRouteEnter (Options API)
7. 전역 beforeResolve
8. 네비게이션 확정
9. 전역 afterEach
10. DOM 업데이트

스크롤 동작

TYPESCRIPT
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 뒤로/앞으로 가기 시 이전 스크롤 위치 복원
    if (savedPosition) {
      return savedPosition
    }

    // 해시(앵커) 이동
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'  // 부드러운 스크롤
      }
    }

    // 기본: 페이지 상단으로
    return { top: 0 }
  }
})
TYPESCRIPT
// 지연된 스크롤 — 트랜지션 완료 후 스크롤
scrollBehavior(to, from, savedPosition) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ top: 0 })
    }, 300)  // 트랜지션 시간만큼 대기
  })
}

라우트 전환 애니메이션

VUE
<template>
  <RouterView v-slot="{ Component, route }">
    <Transition :name="route.meta.transition || 'fade'" mode="out-in">
      <component :is="Component" :key="route.path" />
    </Transition>
  </RouterView>
</template>

<style>
/* 페이드 전환 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 슬라이드 전환 */
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease;
}

.slide-enter-from {
  transform: translateX(100%);
}

.slide-leave-to {
  transform: translateX(-100%);
}
</style>

라우트별 데이터 가져오기 전략

VUE
<!-- 방법 1: 네비게이션 후 가져오기 -->
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const post = ref(null)
const loading = ref(true)
const error = ref(null)

const fetchPost = async (id: string) => {
  loading.value = true
  try {
    const res = await fetch(`/api/posts/${id}`)
    post.value = await res.json()
  } catch (e) {
    error.value = e
  } finally {
    loading.value = false
  }
}

watch(() => route.params.id, (id) => fetchPost(id as string), { immediate: true })
</script>

<template>
  <div v-if="loading">로딩 중...</div>
  <div v-else-if="error">에러 발생</div>
  <div v-else>{{ post }}</div>
</template>

동적 라우트 추가/제거

TYPESCRIPT
// 런타임에 라우트 추가
const removeRoute = router.addRoute({
  path: '/dynamic',
  name: 'dynamic',
  component: () => import('@/views/DynamicView.vue')
})

// 추가된 라우트 제거
removeRoute()

// 이름으로 제거
router.removeRoute('dynamic')

// 현재 등록된 모든 라우트 확인
console.log(router.getRoutes())

면접 팁

  • 네비게이션 가드의 실행 순서 를 알고 있으면 복잡한 인증/권한 시나리오를 설명할 수 있습니다
  • beforeEach에서 return false vs return { name: 'login' }의 차이를 설명할 수 있어야 합니다
  • 스크롤 동작 커스터마이징은 UX 개선 관점에서 실무에 중요합니다

요약

네비게이션 가드는 전역(beforeEach), 라우트별(beforeEnter), 컴포넌트별(onBeforeRouteLeave) 세 단계로 구성됩니다. 인증/권한 검사에는 전역 가드와 메타 필드 조합이 일반적이고, 스크롤 동작과 전환 애니메이션으로 UX를 개선할 수 있습니다.

댓글 로딩 중...