2026๋ Vue 3 ํ ์คํ : Vitest, Vue Test Utils ๊ทธ๋ฆฌ๊ณ ๋ฉด์ ์ง๋ฌธ
2026๋ Vue ํ ์คํ ์ค์ ๊ฐ์ด๋. Vitest ์ค์ , Vue Test Utils๋ก ์ปดํฌ๋ํธ ๋ง์ดํธ, ์ปดํฌ์ ๋ธ๊ณผ Pinia ์คํ ์ด ํ ์คํธ, API ๋ชจํน, ์ปค๋ฒ๋ฆฌ์ง ์ธก์ , ๊ทธ๋ฆฌ๊ณ ์ฑ์ฉ ํ์ด ์ค์ ๋ก ๋์ง๋ ๋ฉด์ ์ง๋ฌธ๊น์ง ๋ค๋ฃน๋๋ค.

Vue ํ ์คํ ์ ์์ ์๋ ๋ฆฌํฉํฐ๋ง๊ณผ ๋ถ์ํ ์ถ์ธก์ ๊ฐ๋ผ๋์ต๋๋ค. 2026๋ ํ์ฌ ๊ด๋ จ ๋๊ตฌ ์ํ๊ณ๋ ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์ฌ์ผ๋ก ์ ๋ฆฌ๋์์ต๋๋ค. ๋ฌ๋ ์ญํ ์ ํ๋ Vitest์ ์ปดํฌ๋ํธ ๋ง์ดํธ๋ฅผ ๋ด๋นํ๋ Vue Test Utils์ ๋๋ค. ์ด ๊ฐ์ด๋๋ ์ด ์คํ์ ๊ตฌ์ฑํ๋ ๋ฐฉ๋ฒ๋ถํฐ ์ปดํฌ๋ํธ์ ์ปดํฌ์ ๋ธ ํ ์คํธ, ์์กด์ฑ ๋ชจํน, ๊ทธ๋ฆฌ๊ณ ์ฑ์ฉ ํ์ด ์ค์ ๋ก ๋ฌผ์ด๋ณด๋ Vue ํ ์คํ ๋ฉด์ ์ง๋ฌธ์ ๋ตํ๋ ๋ฐฉ๋ฒ๊น์ง ์์๋๋ก ์ดํด๋ด ๋๋ค.
Vue ๊ณต์ ๋ฌธ์๊ฐ ๊ธฐ๋ณธ์ผ๋ก ๊ถ์ฅํ๋ ์กฐํฉ์ ๋จ์ ๋ฐ ์ปดํฌ๋ํธ ํ ์คํธ์๋ Vitest, ์ ์์ค ๋ง์ดํธ API๋ก๋ Vue Test Utils, ๊ทธ๋ฆฌ๊ณ ์ข ๋จ ๊ฐ(E2E) ํ ์คํธ์๋ Playwright ๋๋ Cypress์ ๋๋ค. Vitest๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ๋์ผํ Vite ์ค์ ์ ์ฌ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ํ ์คํธ๊ฐ ์์ ํ ๊ฐ์ ๋ณํ ํ์ดํ๋ผ์ธ์ ๊ฑฐ์น๋ฉฐ ๊ฑฐ์ ์ฆ๊ฐ์ ์ธ ํซ ๋ฆฌ๋ก๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
Vue 3 ํ๋ก์ ํธ๋ฅผ ์ํ Vitest ๊ตฌ์ฑ
Vitest๋ Vite ํ์ดํ๋ผ์ธ์ ๊ณต์ ํ๋ฏ๋ก, ํ๋์ ์ค์ ํ์ผ์ด ๊ฐ๋ฐ ์๋ฒ์ ํ
์คํธ ๋ฌ๋๋ฅผ ๋์์ ๊ตฌ๋ํฉ๋๋ค. ์ปดํฌ๋ํธ ํ
์คํธ๊ฐ ๋ ๋๋งํ DOM์ ๊ฐ์ถ๋ ค๋ฉด environment ์ต์
์ ๋ฐ๋์ jsdom ๋๋ happy-dom์ผ๋ก ์ง์ ํด์ผ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ globals: true ํ๋๊ทธ๋ฅผ ์ผ๋ฉด ๋ชจ๋ ํ์ผ์์ ๋ณ๋๋ก ์ํฌํธํ์ง ์์๋ describe, it, expect๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
export default defineConfig({
plugins: [vue()],
test: {
// jsdom provides document/window so components can mount
environment: 'jsdom',
// expose describe/it/expect globally
globals: true,
// run this before each test file (matchers, cleanup)
setupFiles: ['./tests/setup.ts'],
// only pick up *.spec.ts and *.test.ts files
include: ['**/*.{test,spec}.ts'],
},
resolve: {
alias: {
// mirror the @ alias used across the app
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})Vitest 3์์๋ vitest, @vue/test-utils, @vitejs/plugin-vue, jsdom์ ์ค์นํ๊ณ ๋๋ฉด ์ ์ค์ ์ด ๋ณ๋ ์กฐ์ ์์ด ๊ทธ๋๋ก ๋์ํฉ๋๋ค. ์ด ๋ณ์นญ์ด ์ ํ๋ฆฌ์ผ์ด์
์ค์ ์ ๊ทธ๋๋ก ๋ฐ์ํ๊ธฐ ๋๋ฌธ์, @/composables/useCart ๊ฐ์ ์ํฌํธ ๊ฒฝ๋ก๊ฐ ํ
์คํธ์ ์ด์ ํ๊ฒฝ์์ ๋์ผํ๊ฒ ํด์๋ฉ๋๋ค.
Vue Test Utils๋ก ์ปดํฌ๋ํธ ๋ง์ดํธํ๊ธฐ
Vue Test Utils๋ ๋ ๊ฐ์ ์ง์
์ ์ ์ ๊ณตํฉ๋๋ค. ์ ์ฒด ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๋ ๋๋งํ๋ mount์, ์์ ์ปดํฌ๋ํธ๋ฅผ ์คํ
์ผ๋ก ๋์ฒดํ๋ shallowMount์
๋๋ค. ๋๋ถ๋ถ์ ๋จ์ ํ
์คํธ์์๋ ์ค์ ๋ ๋๋ง ๋์์ ๊ฒ์ฆํ๋ mount๊ฐ ๋ ๋ซ์ต๋๋ค. ๋ฐํ๋ wrapper๋ ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํ ์ ์๋๋ก find, get, text ๊ฐ์ ์กฐํ ํฌํผ๋ฅผ ์ ๊ณตํฉ๋๋ค.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PriceTag from '@/components/PriceTag.vue'
describe('PriceTag', () => {
it('formats the amount as currency', () => {
const wrapper = mount(PriceTag, {
// props are passed through the mounting options
props: { amount: 4200, currency: 'EUR' },
})
// get() throws if the selector is missing, unlike find()
expect(wrapper.get('[data-test="price"]').text()).toBe('โฌ42.00')
})
it('applies a discount class when on sale', () => {
const wrapper = mount(PriceTag, {
props: { amount: 4200, currency: 'EUR', onSale: true },
})
// classes() returns the array of applied CSS classes
expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale')
})
})CSS ํด๋์ค ๋์ data-test ์์ฑ์ผ๋ก ์์๋ฅผ ์ง์ ํ๋ฉด ํ
์คํธ๊ฐ ํจ์ฌ ๊ฒฌ๊ณ ํด์ง๋๋ค. .price--sale์ ์คํ์ผ์ ๋ฐ๊ฟ๋ ์ ํ์๋ ๊นจ์ง์ง ์๊ณ , ์ค์ง ์ค์ ๋์์ด ๋ฐ๋ ๋๋ง ํ
์คํธ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด๋ฌํ ๋ถ๋ฆฌ๋ ์ ์ง๋ณด์ ๊ฐ๋ฅํ Vue ํ
์คํธ์์ ๋ฐ๋ณต์ ์ผ๋ก ๋ฑ์ฅํ๋ ์์น์
๋๋ค.
์ฌ์ฉ์ ์ด๋ฒคํธ์ ๋ฐ์ ์ด๋ฒคํธ ํ ์คํธํ๊ธฐ
์ํธ์์ฉํ๋ ์ปดํฌ๋ํธ๋ ํด๋ฆญ์ด๋ ์
๋ ฅ ์ดํ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์ง ๊ฒ์ฆํด์ผ ํฉ๋๋ค. Vue Test Utils์ trigger๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํ๋ฉฐ, ์ด๋ฅผ awaitํ๋ฉด Vue์ ๋ฐ์์ฑ ํ๊ฐ ๋น์์ ธ DOM์ด ๊ฐฑ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค. emitted ํฌํผ๋ ์ปดํฌ๋ํธ๊ฐ ๋ฐ์์ํจ ๋ชจ๋ ์ปค์คํ
์ด๋ฒคํธ๋ฅผ ๊ธฐ๋กํ๋ฉฐ, ์ด๊ฒ์ด ๋ถ๋ชจ-์์ ๊ฐ ๊ณ์ฝ์ ๊ฒ์ฆํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchBar from '@/components/SearchBar.vue'
describe('SearchBar', () => {
it('emits search with the typed query', async () => {
const wrapper = mount(SearchBar)
// setValue writes to the input and fires an input event
await wrapper.get('input').setValue('vitest')
// awaiting trigger flushes the reactivity queue before assertions
await wrapper.get('[data-test="submit"]').trigger('click')
// emitted() returns a record of every event and its payloads
const events = wrapper.emitted('search')
expect(events).toHaveLength(1)
// events[0] is the first emission, [0] its first argument
expect(events![0][0]).toBe('vitest')
})
})Vue๋ ๋ฐ์์ฑ ๊ฐฑ์ ์ ๋น๋๊ธฐ๋ก ์ผ๊ด ์ฒ๋ฆฌํฉ๋๋ค. trigger๋ setValue ํธ์ถ์ awaitํ๋ ๊ฒ์ ์๋ ๊ฒ์ด ๋ถ์์ ํ(flaky) Vue ํ
์คํธ์ ๊ฐ์ฅ ํํ ์์ธ์
๋๋ค. DOM์ด ๋ค์ ๋ ๋๋ง๋๊ธฐ ์ ์ ๊ฒ์ฆ์ด ์คํ๋์ด ์ค๋๋ ์ถ๋ ฅ์ ์ฝ์ด๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์
๋๋ค. ์ด๋ฒคํธ ํฌํผ ๋ฐ๊นฅ์์ ์ํ๊ฐ ๋ฐ๋๋ ๊ฒฝ์ฐ์๋ vue์์ nextTick์ ์ํฌํธํ ๋ค, ๊ฒ์ฆ ์ ์ await nextTick()์ ํธ์ถํด์ผ ํฉ๋๋ค.
Vue ์ปดํฌ์ ๋ธ์ ๋ ๋ฆฝ์ ์ผ๋ก ํ ์คํธํ๊ธฐ
๋ฐ์์ฑ API(ref, computed, watch)๋ง ์ฌ์ฉํ๋ ์ปดํฌ์ ๋ธ์ ์ปดํฌ๋ํธ๊ฐ ์ ํ ํ์ ์์ต๋๋ค. ๋ฐ์ํ ์ํ๋ฅผ ๋ฐํํ๋ ์์ ํจ์์ด๊ธฐ ๋๋ฌธ์, ์ง์ ํธ์ถํ ๋ค .value๋ฅผ ๊ฒ์ฆํ๋ ๊ฒ์ผ๋ก ์ถฉ๋ถํฉ๋๋ค. ์ด๊ฒ์ด ๋น์ฆ๋์ค ๋ก์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ณ ์ง์ค์ ์ผ๋ก ์ปค๋ฒํ๋ ๋ฐฉ๋ฒ์ด๋ฉฐ, ๊ณ ๊ธ Vue 3 ์ปดํฌ์ ๋ธ์์ ๋ค๋ฃจ๋ ํจํด๊ณผ๋ ์ ์ด์ธ๋ฆฝ๋๋ค.
import { describe, it, expect } from 'vitest'
import { useCart } from '@/composables/useCart'
describe('useCart', () => {
it('tracks total price as items change', () => {
// composables using only ref/computed run without mounting
const { items, total, addItem } = useCart()
expect(total.value).toBe(0)
addItem({ id: 1, price: 1500, qty: 2 })
// computed total recalculates synchronously on state change
expect(total.value).toBe(3000)
addItem({ id: 2, price: 500, qty: 1 })
expect(items.value).toHaveLength(2)
expect(total.value).toBe(3500)
})
})onMounted ๊ฐ์ ๋ผ์ดํ์ฌ์ดํด ํ
์ ์์กดํ๋ ์ปดํฌ์ ๋ธ์ ์์ธ์
๋๋ค. ์ด๋ค์ ์ปดํฌ๋ํธ ์ธ์คํด์ค ๋ด๋ถ์์ ์คํ๋์ด์ผ ํฉ๋๋ค. ํํ ์ฐ์ด๋ ์ฐํ ๋ฐฉ๋ฒ์ setup์์ ์ปดํฌ์ ๋ธ์ ํธ์ถํ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋
ธ์ถํ๋ ์์ฃผ ์์ ํฌํผ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ ๋ค, ๊ทธ ํฌํผ๋ฅผ Vue Test Utils๋ก ๋ง์ดํธํ๋ ๊ฒ์
๋๋ค.
Vue.js / Nuxt.js ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
vi.mock์ผ๋ก API ํธ์ถ ๋ชจํนํ๊ธฐ
์ค์ ๋คํธ์ํฌ ์์ฒญ์ ํ
์คํธ๋ฅผ ๋๋ฆฌ๊ณ ๋น๊ฒฐ์ ์ ์ผ๋ก ๋ง๋ญ๋๋ค. Vitest๋ vi.mock์ผ๋ก ๋ชจ๋์ ๋์ฒดํ๊ณ vi.fn์ผ๋ก ์คํ์ด ํจ์๋ฅผ ์์ฑํฉ๋๋ค. ์๋ ํจํด์ fetch๋ฅผ ๊ฐ์ธ๋ ๋ชจ๋์ ๋ชจํนํ์ฌ, ํ
์คํธ ๋์ ์ปดํฌ์ ๋ธ์ด ์๋ฒ์ ์ ํ ์ ๊ทผํ์ง ์๊ณ ๋ ํต์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋๋ก ํฉ๋๋ค.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useProducts } from '@/composables/useProducts'
import { getProducts } from '@/api/products'
// replace the whole module with mocked exports
vi.mock('@/api/products', () => ({
getProducts: vi.fn(),
}))
describe('useProducts', () => {
beforeEach(() => {
// reset call history between tests to avoid leakage
vi.mocked(getProducts).mockReset()
})
it('exposes fetched products and clears loading', async () => {
// define what the mocked API returns for this test
vi.mocked(getProducts).mockResolvedValue([
{ id: 1, name: 'Keyboard' },
])
const { products, loading, load } = useProducts()
await load()
expect(getProducts).toHaveBeenCalledOnce()
expect(loading.value).toBe(false)
expect(products.value[0].name).toBe('Keyboard')
})
})๋ชจ๋ ๊ฒฝ๊ณ์์ ๋ชจํนํ๋ฉด ์ปดํฌ์ ๋ธ ๋ด๋ถ ๋ก์ง์ ๊ทธ๋๋ก ์ ์งํ๋ฉด์, ๊ฐ ํ
์คํธ๊ฐ ์ค๋ฅ ๊ฒฝ๋ก๋ฅผ ํฌํจํ API ์๋ต์ ์์ ํ ํต์ ํ ์ ์์ต๋๋ค. mockResolvedValue ๋์ mockRejectedValue๋ฅผ ํธ์ถํ๋ฉด, ํ๋์ ํ
์คํธ๋ก ์คํจ๊ฐ ์ค๋ฅ ์ํ๋ฅผ ์ค์ ํ๊ณ ์คํผ๋๋ฅผ ๋ฉ์ถ๋์ง๊น์ง ๊ฒ์ฆํ ์ ์์ต๋๋ค.
2026๋ ์ Pinia ์คํ ์ด ํ ์คํธ
์คํ ์ด๋ ์ฌ๋ฌ ์ปดํฌ๋ํธ์ ๊ฑธ์น ์ํ๋ฅผ ๋ด๊ณ ์์ผ๋ฉฐ, ๊ณต์ @pinia/testing ํจํค์ง๋ ์ด๋ฅผ ์์ฝ๊ฒ ํ
์คํธํ๋๋ก ํด์ค๋๋ค. createTestingPinia๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์ก์
์ ์คํ
์ผ๋ก ๋์ฒดํ๋ฏ๋ก, ์ปดํฌ๋ํธ ํ
์คํธ์์ ๋ถ์ ํจ๊ณผ๋ฅผ ์คํํ์ง ์๊ณ ๋ ์ก์
์ด ๋์คํจ์น๋์๋์ง ๊ฒ์ฆํ ์ ์์ต๋๋ค. Pinia ํ
์คํ
๊ฐ์ด๋๋ ์ปดํฌ๋ํธ ์์ค ํ
์คํธ์ ์ด ๋ฐฉ์์ ๊ถ์ฅํฉ๋๋ค.
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CheckoutButton from '@/components/CheckoutButton.vue'
import { useCartStore } from '@/stores/cart'
describe('CheckoutButton', () => {
it('dispatches checkout on click', async () => {
const wrapper = mount(CheckoutButton, {
global: {
plugins: [
// vi.fn spies on actions instead of executing them
createTestingPinia({ createSpy: vi.fn }),
],
},
})
const store = useCartStore()
await wrapper.get('[data-test="checkout"]').trigger('click')
// the action is stubbed, so only the dispatch is asserted
expect(store.checkout).toHaveBeenCalledOnce()
})
})์ปดํฌ๋ํธ ํตํฉ์ด ์๋๋ผ ์คํ ์ด ๋ก์ง ์์ฒด๋ฅผ ํ
์คํธํ๋ ค๋ฉด stubActions: false๋ฅผ ์ ๋ฌํ์ฌ ์ก์
์ด ์ ์์ ์ผ๋ก ์คํ๋๋๋ก ํ ๋ค์, ๊ทธ ๊ฒฐ๊ณผ ์ํ๋ฅผ ๊ฒ์ฆํ๋ฉด ๋ฉ๋๋ค. ์ด๋ ๊ฒ ์ก์
์ ์คํ
์ผ๋ก ๋์ฒดํ ์ปดํฌ๋ํธ ํ
์คํธ์ ์ค์ ์ก์
์ ์คํํ๋ ์คํ ์ด ํ
์คํธ๋ฅผ ๋๋๋ฉด, ๊ฐ ํ
์คํธ ์ค์ํธ๊ฐ ๋จ ํ๋์ ์ฑ
์์ ์ง์คํ๊ฒ ๋ฉ๋๋ค.
์์น ๋ชจ๋๋ก ํ ์คํธ ์คํํ๊ณ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ํ๊ธฐ
Vitest๋ ์ ์ฅ๋ ํ์ผ์ ์ํฅ์ ๋ฐ๋ ํ
์คํธ๋ง ๋ค์ ์คํํ๋ ์์น ๋ชจ๋๋ฅผ ๊ธฐ๋ณธ ์ ๊ณตํ์ฌ, ๊ฐ๋ฐ ์ค ํผ๋๋ฐฑ ๋ฃจํ๋ฅผ ์์๊ฐ์ ๋จ์ถ์ํต๋๋ค. ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํ
์ @vitest/coverage-v8 ํ๋ก๋ฐ์ด๋๋ฅผ ํตํด ๋ด์ฅ๋์ด ์์ผ๋ฉฐ, ์ง์์ ํตํฉ(CI) ํ์ดํ๋ผ์ธ์๋ ๊น๋ํ๊ฒ ์ฐ๋๋ฉ๋๋ค. ์ด ํ์ดํ๋ผ์ธ์์๋ ์คํจํ ํ
์คํธ๋ ์ปค๋ฒ๋ฆฌ์ง ํ๋ฝ์ด ๋ณํฉ์ ๋ง์ ์ ์์ต๋๋ค.
export default defineConfig({
test: {
coverage: {
// v8 is the fastest provider and needs no instrumentation step
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
// fail CI when a threshold drops below the target
thresholds: {
statements: 80,
branches: 75,
functions: 80,
},
// exclude generated and config files from the report
exclude: ['**/*.config.ts', '**/types/**'],
},
},
})vitest๋ง ๋จ๋
์ผ๋ก ์คํํ๋ฉด ๋ก์ปฌ์์ ์์น ๋ชจ๋๊ฐ ์์๋๊ณ , vitest run --coverage๋ CI์ ์ ํฉํ ๋จ์ผ ์คํ์ ์ํํฉ๋๋ค. ๋ช
์์ ์ธ ์๊ณ๊ฐ์ ์ค์ ํ๋ฉด ์ปค๋ฒ๋ฆฌ์ง๊ฐ ํ์ธ๋ฟ์ธ ์งํ์์ ๊ฐ์ ๋๋ ํ์ง ๊ฒ์ดํธ๋ก ๋ฐ๋๋๋ค. ๋ธ๋์น ์ปค๋ฒ๋ฆฌ์ง๋ฅผ 75ํผ์ผํธ ์๋๋ก ๋จ์ด๋จ๋ฆฌ๋ ํ ๋ฆฌํ์คํธ๋ ์๋์ผ๋ก ์คํจํ๊ฒ ๋๊ณ , ์ด๋ ๊ธฐ์ฌ์๊ฐ ์ ์ ๊ฒฝ๋ก๋ง์ด ์๋๋ผ ์์ ์ด ์ถ๊ฐํ ๊ฒฝ๋ก๊น์ง ํ
์คํธํ๋๋ก ์ ๋ํฉ๋๋ค.
์์ฃผ ๋์ค๋ Vue ํ ์คํ ๋ฉด์ ์ง๋ฌธ๊ณผ ๋ต๋ณ
ํ ์คํ ์ง๋ฌธ์ ํ๋ณด์๊ฐ ์ ์ง๋ณด์์ฑ์ ์ด๋ป๊ฒ ์ฌ๊ณ ํ๋์ง ๋๋ฌ๋ด๊ธฐ ๋๋ฌธ์ ๋๋ถ๋ถ์ ์๋์ด Vue ๋ฉด์ ์์ ๋ฑ์ฅํฉ๋๋ค. ์๋ ๋ต๋ณ์ ๊ฐ์ฅ ์์ฃผ ๋์ค๋ ๊ฐ๋ ์ ๋ค๋ฃจ๋ฉฐ, ์ ์ฒด ์ธํธ๋ Vue ํ ์คํ ๋ฉด์ ๋ชจ๋์์ ๋ฐ๋ณต ์ฐ์ตํ ์ ์์ต๋๋ค.
mount ๋์ shallowMount๋ ์ธ์ ์จ์ผ ํ ๊น์? ์ปดํฌ๋ํธ๊ฐ ๋ฌด๊ฒ๊ฑฐ๋ ์ด๋ฏธ ํ
์คํธ๋ ์์์ ๋ ๋๋งํ๊ณ , ํ
์คํธ๊ฐ ์ค์ง ๋ถ๋ชจ ์์ ์ ๋ก์ง๋ง ์ ๊ฒฝ ์ธ ๋ shallowMount๋ฅผ ์ ํํฉ๋๋ค. ์์์ ์คํ
์ผ๋ก ๋์ฒดํ๋ฉด ๋จ์๋ฅผ ๊ฒฉ๋ฆฌํ๊ณ ๋ ๋๋ง ์๋๋ฅผ ๋์ผ ์ ์์ง๋ง, ์ปดํฌ๋ํธ ๊ฐ์ ์ค์ ํตํฉ์ ๊ฒ์ฆํ์ง ๋ชปํ๋ค๋ ๋๊ฐ๊ฐ ๋ฐ๋ฆ
๋๋ค.
Vue Test Utils์์ ๋น๋๊ธฐ ๊ฐฑ์ ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์? Vue๋ ๋ฐ์ํ ๋ณ๊ฒฝ์ ๋ค์ ํฑ์ ์ ์ฉํฉ๋๋ค. trigger, setValue ๊ฐ์ ์ด๋ฒคํธ ํฌํผ๋ DOM์ด ๊ฐฑ์ ๋ ๋ค ์๋ฃ๋๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํ๋ฏ๋ก, ์ด๋ฅผ awaitํ๋ ๊ฒ์ผ๋ก ์ถฉ๋ถํฉ๋๋ค. ๊ทธ ํฌํผ ๋ฐ๊นฅ์์ ๋ฐ๋ ์ํ์ ๋ํด์๋ await nextTick()์ผ๋ก ๊ฒ์ฆ ์ ์ ํ๋ฅผ ๊ฐ์ ๋ก ๋น์๋๋ค.
find์ get์ ์ฐจ์ด๋ ๋ฌด์์ธ๊ฐ์? find๋ ์ผ์นํ๋ ์์๊ฐ ์์ผ๋ฉด ๋น ๋ํผ๋ฅผ ๋ฐํํ๊ณ ๊ฒฐ์ฝ ์์ธ๋ฅผ ๋์ง์ง ์์ผ๋ฏ๋ก, expect(wrapper.find('.error').exists()).toBe(false) ๊ฐ์ ๊ฒ์ฆ์ ์ ํฉํฉ๋๋ค. get์ ์์๊ฐ ์์ผ๋ฉด ์ฆ์ ์์ธ๋ฅผ ๋์ง๋ฏ๋ก, ์์๊ฐ ์กด์ฌํ๋ฆฌ๋ผ๊ณ ๊ธฐ๋๋๊ณ ๊ทธ ๋ถ์ฌ๊ฐ ์ง์ง ์คํจ์ ํด๋นํ ๋ ๋ ๋์ ์ ํ์
๋๋ค.
์ ํด๋์ค ์ ํ์๋ณด๋ค data-test ์์ฑ์ ์ ํธํ๋์? ํด๋์ค ์ด๋ฆ์ ์คํ์ผ๊ณผ ๊ตฌ์กฐ์ ๋ฐ๋ผ ๋ฐ๋๋ฏ๋ก, ์ด๋ฅผ ์กฐํํ๋ ํ
์คํธ๋ ๊ฒ๋ชจ์ต๋ง ์๋ด๋ ๊นจ์ง๋๋ค. ์ ์ฉ data-test ํ
์ ํ
์คํธ ์๋๋ฅผ ๋ช
์์ ์ผ๋ก ํํํ๊ณ ๋ฆฌํฉํฐ๋ง์๋ ์ด์๋จ์, ๊ฑฐ์ง ์คํจ(false negative)๋ฅผ ์ค์ฌ์ค๋๋ค.
onMounted์ ์์กดํ๋ ์ปดํฌ์ ๋ธ์ ์ด๋ป๊ฒ ํ
์คํธํ๋์? ๋ผ์ดํ์ฌ์ดํด ํ
์ ์ปดํฌ๋ํธ ์ธ์คํด์ค ๋ด๋ถ์์๋ง ์คํ๋๋ฏ๋ก ์ปดํฌ์ ๋ธ์ ์ง์ ํธ์ถํ ์ ์์ต๋๋ค. ํ์ค์ ์ธ ๊ธฐ๋ฒ์ setup์์ ์ปดํฌ์ ๋ธ์ ํธ์ถํ๊ณ ๊ทธ ๋ฐํ๊ฐ์ ๋
ธ์ถํ๋ ์ผํ์ฉ ํ๋์ค(harness) ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ ๋ค, ๊ทธ ํ๋์ค๋ฅผ Vue Test Utils๋ก ๋ง์ดํธํ์ฌ ๋ํผ๋ฅผ ํตํด ๊ฒ์ฆํ๋ ๊ฒ์
๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ฐ์์ฑ๊ณผ ๋ผ์ดํ์ฌ์ดํด ์ฐ๊ฒฐ์ ์จ์ ํ ์ ์งํ๋ฉด์๋ ํ
์คํธ ๋์ ๋ก์ง์ ๊ฒฉ๋ฆฌํ ์ ์์ต๋๋ค. ๋ ๋ง์ ๊ฐ๋
๋ฐ ์ฝ๋ฉ ์ง๋ฌธ์ ํต์ฌ Vue.js ๋ฉด์ ์ง๋ฌธ ๊ฐ์ด๋์ ์ ๋ฆฌ๋์ด ์์ต๋๋ค.
๊ฒฐ๋ก
2026๋ ์ ๋ฏฟ์ ์ ์๋ Vue ํ ์คํ ๊ตฌ์ฑ์ ๊ฒฐ๊ตญ ๋ช ๊ฐ์ง ์๋์ ์ธ ์ ํ์ผ๋ก ์์ฝ๋ฉ๋๋ค.
environment: 'jsdom'๊ณผ Vue ํ๋ฌ๊ทธ์ธ์ผ๋ก Vitest๋ฅผ ์คํํ์ฌ, ํ ์คํธ๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์์ ํ ๋์ผํ Vite ๋ณํ ํ์ดํ๋ผ์ธ์ ๊ณต์ ํ๊ฒ ๋ง๋ญ๋๋ค.- ์์ ์ปดํฌ๋ํธ๊ฐ ๋ฌด๊ฒ๊ฑฐ๋ ๋ค๋ฅธ ๊ณณ์์ ์ด๋ฏธ ์ปค๋ฒ๋์ง ์๋ ํ,
shallowMount๋ณด๋คmount๋ฅผ ์ฐ์ ํฉ๋๋ค. - ํ์ด๋ฐ์ ์์กดํ๋ ๋ถ์์ ํ ํ
์คํธ๋ฅผ ์์ ๊ธฐ ์ํด, ๊ฒ์ฆ ์ ์
trigger,setValue,nextTick์ ํญ์awaitํฉ๋๋ค. - ๋ฐ์์ฑ API๋ง ์ฌ์ฉํ๋ ์ปดํฌ์ ๋ธ์ ์ง์ ํธ์ถํ๊ณ
.value๋ฅผ ๊ฒ์ฆํ์ฌ ํ ์คํธํฉ๋๋ค. vi.mock์ผ๋ก ๋ชจ๋ ๊ฒฝ๊ณ์์ ๋ชจํนํ์ฌ ๋คํธ์ํฌ ๋์์ ๊ฒฐ์ ์ ์ผ๋ก ๋ง๋ค๊ณ ์ค๋ฅ ๊ฒฝ๋ก๊น์ง ํ ์คํธ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.- ์ปดํฌ๋ํธ ํ
์คํธ์๋
createTestingPinia({ createSpy: vi.fn })์, ์คํ ์ด ๋ก์ง์ ์ค์ ๋ก ์คํํ ๋๋stubActions: false๋ฅผ ์ฌ์ฉํฉ๋๋ค. data-test์์ฑ์ผ๋ก ์์๋ฅผ ์กฐํํ์ฌ, ํ ์คํธ๊ฐ ์คํ์ผ์ด ์๋๋ผ ์ค์ง ์ค์ ๋์ ๋ณํ์๋ง ๊นจ์ง๋๋ก ํฉ๋๋ค.
์ด ํจํด๋ค์ ์๋ฌํ๋ฉด ํ ์คํ ์ ๊ท์ฐฎ์ ์์ ์ด ์๋๋ผ ์ค๊ณ ๋๊ตฌ๊ฐ ๋์ด, ํ๊ท๊ฐ ์ด์ ํ๊ฒฝ์ ๋๋ฌํ๊ธฐ ์ ์ ์ก์๋ ๋๋ค. ๋ฉด์ ์ ๋๋นํ ์ค๋ ฅ์ ๊ณ์ ์์ผ๋ ค๋ฉด Vue์ Nuxt ํ์ต ๊ฒฝ๋ก ์ ์ฒด๋ฅผ ์ดํด๋ณด์๊ธฐ ๋ฐ๋๋๋ค.
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
ํ๊ทธ
๊ณต์
๊ด๋ จ ๊ธฐ์ฌ

Vue 3 ์ปดํฌ์ ๋ธ ์ฌํ ๊ฐ์ด๋: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํจํด๊ณผ ๊ธฐ์ ๋ฉด์ ์ง๋ฌธ 2026
Vue 3 ๊ณ ๊ธ ์ปดํฌ์ ๋ธ ํจํด์ ์ฒด๊ณ์ ์ผ๋ก ๋ถ์ํฉ๋๋ค. ๋น๋๊ธฐ ์ฒ๋ฆฌ, ์์กด์ฑ ์ฃผ์ , ํผ ์ ํจ์ฑ ๊ฒ์ฌ, ํ ์คํธ ์ ๋ต, ๊ทธ๋ฆฌ๊ณ 2026๋ ๊ธฐ์ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ์ง๋ฌธ๊ณผ ๋ต๋ณ์ ๋ค๋ฃน๋๋ค.

Vue 3 Pinia vs Vuex ์๋ฒฝ ๋น๊ต: 2026๋ ์ํ ๊ด๋ฆฌ ์ ๋ต๊ณผ ๋ฉด์ ํต์ฌ ์ง๋ฌธ
Vue 3 ์ํ๊ณ์์ Pinia์ Vuex๋ฅผ ๋น๊ต ๋ถ์ํฉ๋๋ค. Options Store์ Setup Store ํจํด, TypeScript ํตํฉ, ํฌ๋ก์ค ์คํ ์ด ๊ตฌ์ฑ, SSR ์ง์, Vuex์์ Pinia๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต, ๊ทธ๋ฆฌ๊ณ 2026๋ ๋ฉด์ ์์ ์์ฃผ ์ถ์ ๋๋ ์ํ ๊ด๋ฆฌ ์ง๋ฌธ์ ์ฝ๋ ์์ ์ ํจ๊ป ์ ๋ฆฌํฉ๋๋ค.

2026๋ Nuxt 4 ์๋ฒฝ ๊ฐ์ด๋: ์๋ก์ด ๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ์ Nuxt 3 ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต
Nuxt 4์์ ๋์ ๋ app/ ๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ, ์ฑ๊ธํค ๋ฐ์ดํฐ ํจ์นญ ๋ ์ด์ด, shallow reactivity, TypeScript ์ปจํ ์คํธ ๋ถ๋ฆฌ๋ฅผ ์ฝ๋ ์์ ์ ํจ๊ป ์์ธํ ๋ถ์ํฉ๋๋ค.