← 가이드 목록으로 ← Back to guides

Vue Router와 테스팅 완벽 가이드 Vue Router & Testing Complete Guide

Vue 앱을 완성하려면 라우팅과 테스팅이 필수입니다. Vue Router 심화, 네비게이션 가드, Vitest와 Vue Test Utils로 견고한 앱을 만들어봅시다.

Routing and testing are essential to complete a Vue app. Let's build robust apps with Vue Router deep dive, navigation guards, and Vitest with Vue Test Utils.

Vue Router 기본 설정 Vue Router Basic Setup

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true // params를 props로 전달
  }
]

export const router = createRouter({
  history: createWebHistory(),
  routes
})
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true // Pass params as props
  }
]

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

네비게이션 가드 Navigation Guards

라우트 전환 시 인증 확인, 데이터 로딩 등을 처리할 수 있습니다.

Handle authentication checks, data loading, etc. during route transitions.

// 전역 가드
router.beforeEach((to, from) => {
  const isAuthenticated = checkAuth()

  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'Login' }
  }
})

// 라우트별 가드
{
  path: '/admin',
  component: AdminPanel,
  meta: { requiresAuth: true, role: 'admin' },
  beforeEnter: (to, from) => {
    if (!isAdmin()) {
      return { name: 'Home' }
    }
  }
}
// Global guard
router.beforeEach((to, from) => {
  const isAuthenticated = checkAuth()

  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'Login' }
  }
})

// Per-route guard
{
  path: '/admin',
  component: AdminPanel,
  meta: { requiresAuth: true, role: 'admin' },
  beforeEnter: (to, from) => {
    if (!isAdmin()) {
      return { name: 'Home' }
    }
  }
}

컴포넌트 내 가드 In-Component Guards

// Composition API
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = confirm('저장하지 않은 변경사항이 있습니다. 계속할까요?')
    if (!answer) return false
  }
})

onBeforeRouteUpdate(async (to, from) => {
  // 같은 컴포넌트에서 params가 변경될 때
  await fetchUser(to.params.id)
})
// Composition API
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = confirm('You have unsaved changes. Continue?')
    if (!answer) return false
  }
})

onBeforeRouteUpdate(async (to, from) => {
  // When params change in the same component
  await fetchUser(to.params.id)
})

Vitest로 컴포넌트 테스트 Component Testing with Vitest

// Counter.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('초기값이 0이다', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('0')
  })

  it('클릭하면 증가한다', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })
})
// Counter.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('starts at 0', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('0')
  })

  it('increments on click', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })
})

Props와 Emit 테스트 Testing Props & Emit

it('props를 받아 렌더링한다', () => {
  const wrapper = mount(UserCard, {
    props: {
      user: { name: 'Kim', age: 30 }
    }
  })
  expect(wrapper.text()).toContain('Kim')
})

it('이벤트를 emit한다', async () => {
  const wrapper = mount(DeleteButton)
  await wrapper.find('button').trigger('click')

  expect(wrapper.emitted()).toHaveProperty('delete')
  expect(wrapper.emitted('delete')).toHaveLength(1)
})
it('renders with props', () => {
  const wrapper = mount(UserCard, {
    props: {
      user: { name: 'Kim', age: 30 }
    }
  })
  expect(wrapper.text()).toContain('Kim')
})

it('emits events', async () => {
  const wrapper = mount(DeleteButton)
  await wrapper.find('button').trigger('click')

  expect(wrapper.emitted()).toHaveProperty('delete')
  expect(wrapper.emitted('delete')).toHaveLength(1)
})

Pinia 스토어 테스트 Testing Pinia Stores

import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('increment 액션이 동작한다', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)

    store.increment()
    expect(store.count).toBe(1)
  })

  it('getter가 올바른 값을 반환한다', () => {
    const store = useCounterStore()
    store.count = 5
    expect(store.doubleCount).toBe(10)
  })
})
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('increment action works', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)

    store.increment()
    expect(store.count).toBe(1)
  })

  it('getter returns correct value', () => {
    const store = useCounterStore()
    store.count = 5
    expect(store.doubleCount).toBe(10)
  })
})

✨ 테스트 베스트 프랙티스 ✨ Testing Best Practices

  • 구현이 아닌 동작을 테스트
  • 사용자 관점에서 테스트 작성
  • 테스트 간 독립성 유지 (beforeEach)
  • 의미있는 테스트 이름 사용
  • Test behavior, not implementation
  • Write tests from user perspective
  • Maintain test independence (beforeEach)
  • Use meaningful test names

⚠️ 흔한 실수들 ⚠️ Common Mistakes

  • 내부 구현 세부사항 테스트
  • 비동기 테스트에서 await 누락
  • 테스트 간 상태 공유
  • 너무 많은 것을 mocking
  • Testing internal implementation details
  • Missing await in async tests
  • Sharing state between tests
  • Mocking too much