Vue Router 심화
네비게이션 가드는 라우트 전환을 가로채어 인증 확인, 권한 검사, 데이터 프리페치 등을 수행하는 Vue Router의 핵심 기능입니다.
면접에서 "인증이 필요한 페이지를 어떻게 보호하나요?"라고 물으면, beforeEach 가드와 메타 필드 를 활용한 패턴을 설명할 수 있어야 합니다.
전역 가드
// 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
라우트별 가드
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' }
}
}
컴포넌트 내 가드
<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>
네비게이션 가드 실행 순서
1. 이전 컴포넌트의 onBeforeRouteLeave
2. 전역 beforeEach
3. 재사용 컴포넌트의 onBeforeRouteUpdate
4. 라우트의 beforeEnter
5. 비동기 컴포넌트 해결
6. 새 컴포넌트의 beforeRouteEnter (Options API)
7. 전역 beforeResolve
8. 네비게이션 확정
9. 전역 afterEach
10. DOM 업데이트
스크롤 동작
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 }
}
})
// 지연된 스크롤 — 트랜지션 완료 후 스크롤
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 })
}, 300) // 트랜지션 시간만큼 대기
})
}
라우트 전환 애니메이션
<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>
라우트별 데이터 가져오기 전략
<!-- 방법 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>
동적 라우트 추가/제거
// 런타임에 라우트 추가
const removeRoute = router.addRoute({
path: '/dynamic',
name: 'dynamic',
component: () => import('@/views/DynamicView.vue')
})
// 추가된 라우트 제거
removeRoute()
// 이름으로 제거
router.removeRoute('dynamic')
// 현재 등록된 모든 라우트 확인
console.log(router.getRoutes())
면접 팁
- 네비게이션 가드의 실행 순서 를 알고 있으면 복잡한 인증/권한 시나리오를 설명할 수 있습니다
beforeEach에서return falsevsreturn { name: 'login' }의 차이를 설명할 수 있어야 합니다- 스크롤 동작 커스터마이징은 UX 개선 관점에서 실무에 중요합니다
요약
네비게이션 가드는 전역(beforeEach), 라우트별(beforeEnter), 컴포넌트별(onBeforeRouteLeave) 세 단계로 구성됩니다. 인증/권한 검사에는 전역 가드와 메타 필드 조합이 일반적이고, 스크롤 동작과 전환 애니메이션으로 UX를 개선할 수 있습니다.
댓글 로딩 중...