본문으로 건너뛰기
Advertisement

12.5 Vue Router 4 + TypeScript — 라우트 타입과 네비게이션 가드

Vue Router 4 기본 설정

npm install vue-router@4
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/users',
name: 'users',
component: () => import('@/views/UsersView.vue'),
},
{
path: '/users/:id',
name: 'user-detail',
component: () => import('@/views/UserDetailView.vue'),
props: true, // params를 props로 전달
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: {
requiresAuth: true,
roles: ['admin', 'user'],
},
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
},
]

const router = createRouter({
history: createWebHistory(),
routes,
})

export default router

라우트 메타 타입 확장

// router/types.ts
import 'vue-router'

// 라우트 메타 타입 확장
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: ('admin' | 'user' | 'guest')[]
title?: string
breadcrumb?: string
keepAlive?: boolean
}
}

useRoute와 useRouter 타입

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()
const router = useRouter()

// route.params: RouteParamsRaw
const userId = computed(() => route.params.id as string)

// route.query: LocationQuery
const page = computed(() => {
const p = route.query.page
return typeof p === 'string' ? parseInt(p) : 1
})

// 프로그래머틱 네비게이션
function goToUser(id: string) {
router.push({ name: 'user-detail', params: { id } })
}

function goBack() {
router.back()
}

function goWithQuery() {
router.push({
name: 'users',
query: { page: '2', sort: 'name' },
})
}
</script>

타입 안전한 라우트 — TypedRouteMap

Vue Router 4.4+에서는 타입 안전한 라우트를 설정할 수 있습니다.

// router/typed-routes.ts
import {
createRouter,
createWebHistory,
RouteRecordRaw,
} from 'vue-router'

// 라우트별 params/query 타입 정의
interface TypedRoutes {
home: {
params: {}
query: {}
}
userDetail: {
params: { id: string }
query: { tab?: string }
}
search: {
params: {}
query: { q: string; page?: string; category?: string }
}
}

네비게이션 가드 타입

전역 가드

// router/guards.ts
import { Router, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'

export function setupGuards(router: Router) {
// 전역 beforeEach
router.beforeEach(async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// 인증 체크
if (to.meta.requiresAuth) {
const userStore = useUserStore()

if (!userStore.isLoggedIn) {
return next({
name: 'login',
query: { redirect: to.fullPath },
})
}

// 권한 체크
if (to.meta.roles && !to.meta.roles.includes(userStore.currentUser!.role)) {
return next({ name: 'forbidden' })
}
}

// 페이지 제목 설정
if (to.meta.title) {
document.title = `${to.meta.title} | 내 앱`
}

next()
})

// 전역 afterEach
router.afterEach((to, from) => {
// 페이지 추적
analytics.pageView(to.fullPath)
})
}

컴포넌트 내 가드

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

// 라우트 변경 전 훅
onBeforeRouteUpdate(async (to, from) => {
// 같은 컴포넌트에서 params만 변경 (예: /users/1 → /users/2)
const newId = to.params.id as string
await loadUser(newId)
})

// 라우트 이탈 전 훅 (저장 안 된 데이터 경고)
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('변경사항이 저장되지 않았습니다. 이탈하시겠습니까?')
if (confirmed) {
next()
} else {
next(false) // 이탈 취소
}
} else {
next()
}
})
</script>

<template>
<!-- 기본 RouterLink -->
<RouterLink to="/home">홈</RouterLink>

<!-- 명명된 라우트 -->
<RouterLink :to="{ name: 'user-detail', params: { id: userId } }">
사용자 상세
</RouterLink>

<!-- query 포함 -->
<RouterLink :to="{ name: 'search', query: { q: searchQuery, page: '1' } }">
검색
</RouterLink>
</template>

동적 라우트 추가

// 권한에 따른 동적 라우트 등록
import { router } from '@/router'

function addAdminRoutes() {
router.addRoute({
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
children: [
{
path: 'users',
name: 'admin-users',
component: () => import('@/views/admin/UsersManagement.vue'),
},
],
})
}

// 사용자 로그인 후 라우트 추가
const userStore = useUserStore()
watch(() => userStore.isAdmin, (isAdmin) => {
if (isAdmin) addAdminRoutes()
})

고수 팁

1. useLink로 커스텀 RouterLink

<script setup lang="ts">
import { useLink, RouterLinkProps } from 'vue-router'

const props = defineProps<RouterLinkProps>()
const { isActive, isExactActive, href, navigate } = useLink(props)
</script>

<template>
<a
:href="href"
:class="{ 'active': isActive, 'exact-active': isExactActive }"
@click.prevent="navigate"
>
<slot />
</a>
</template>

2. 라우트별 컴포넌트 타입 자동 생성 (unplugin-vue-router)

npm install -D unplugin-vue-router

파일 기반 라우팅으로 자동 타입 생성을 지원합니다. useRoute('user-detail') 처럼 라우트 이름으로 타입 안전한 params 접근이 가능합니다.

Advertisement