2026๋…„ Vue 3 ํ…Œ์ŠคํŒ…: Vitest, Vue Test Utils ๊ทธ๋ฆฌ๊ณ  ๋ฉด์ ‘ ์งˆ๋ฌธ

2026๋…„ Vue ํ…Œ์ŠคํŒ… ์‹ค์ „ ๊ฐ€์ด๋“œ. Vitest ์„ค์ •, Vue Test Utils๋กœ ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ, ์ปดํฌ์ €๋ธ”๊ณผ Pinia ์Šคํ† ์–ด ํ…Œ์ŠคํŠธ, API ๋ชจํ‚น, ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •, ๊ทธ๋ฆฌ๊ณ  ์ฑ„์šฉ ํŒ€์ด ์‹ค์ œ๋กœ ๋˜์ง€๋Š” ๋ฉด์ ‘ ์งˆ๋ฌธ๊นŒ์ง€ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

2026๋…„ Vitest์™€ Vue Test Utils๋ฅผ ํ™œ์šฉํ•œ Vue ํ…Œ์ŠคํŒ… ์›Œํฌํ”Œ๋กœ์šฐ

Vue ํ…Œ์ŠคํŒ…์€ ์ž์‹  ์žˆ๋Š” ๋ฆฌํŒฉํ„ฐ๋ง๊ณผ ๋ถˆ์•ˆํ•œ ์ถ”์ธก์„ ๊ฐˆ๋ผ๋†“์Šต๋‹ˆ๋‹ค. 2026๋…„ ํ˜„์žฌ ๊ด€๋ จ ๋„๊ตฌ ์ƒํƒœ๊ณ„๋Š” ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ์ •๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋Ÿฌ๋„ˆ ์—ญํ• ์„ ํ•˜๋Š” Vitest์™€ ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Vue Test Utils์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” ์ด ์Šคํƒ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋ถ€ํ„ฐ ์ปดํฌ๋„ŒํŠธ์™€ ์ปดํฌ์ €๋ธ” ํ…Œ์ŠคํŠธ, ์˜์กด์„ฑ ๋ชจํ‚น, ๊ทธ๋ฆฌ๊ณ  ์ฑ„์šฉ ํŒ€์ด ์‹ค์ œ๋กœ ๋ฌผ์–ด๋ณด๋Š” Vue ํ…Œ์ŠคํŒ… ๋ฉด์ ‘ ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€ ์ˆœ์„œ๋Œ€๋กœ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

2026๋…„์˜ ํ˜„๋Œ€์ ์ธ 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๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

vitest.config.tstypescript
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 ๊ฐ™์€ ์กฐํšŒ ํ—ฌํผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

PriceTag.spec.tstypescript
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 ํ—ฌํผ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฐœ์ƒ์‹œํ‚จ ๋ชจ๋“  ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋กํ•˜๋ฉฐ, ์ด๊ฒƒ์ด ๋ถ€๋ชจ-์ž์‹ ๊ฐ„ ๊ณ„์•ฝ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

SearchBar.spec.tstypescript
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')
  })
})
DOM ๊ฐฑ์‹ ์„ ํ•ญ์ƒ awaitํ•˜์„ธ์š”

Vue๋Š” ๋ฐ˜์‘์„ฑ ๊ฐฑ์‹ ์„ ๋น„๋™๊ธฐ๋กœ ์ผ๊ด„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. trigger๋‚˜ setValue ํ˜ธ์ถœ์„ awaitํ•˜๋Š” ๊ฒƒ์„ ์žŠ๋Š” ๊ฒƒ์ด ๋ถˆ์•ˆ์ •ํ•œ(flaky) Vue ํ…Œ์ŠคํŠธ์˜ ๊ฐ€์žฅ ํ”ํ•œ ์›์ธ์ž…๋‹ˆ๋‹ค. DOM์ด ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜๊ธฐ ์ „์— ๊ฒ€์ฆ์ด ์‹คํ–‰๋˜์–ด ์˜ค๋ž˜๋œ ์ถœ๋ ฅ์„ ์ฝ์–ด๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ํ—ฌํผ ๋ฐ”๊นฅ์—์„œ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€Œ๋Š” ๊ฒฝ์šฐ์—๋Š” vue์—์„œ nextTick์„ ์ž„ํฌํŠธํ•œ ๋’ค, ๊ฒ€์ฆ ์ „์— await nextTick()์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Vue ์ปดํฌ์ €๋ธ”์„ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๋ฐ˜์‘์„ฑ API(ref, computed, watch)๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ์ €๋ธ”์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ „ํ˜€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ˜์‘ํ˜• ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ง์ ‘ ํ˜ธ์ถœํ•œ ๋’ค .value๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ์ง‘์ค‘์ ์œผ๋กœ ์ปค๋ฒ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋ฉฐ, ๊ณ ๊ธ‰ Vue 3 ์ปดํฌ์ €๋ธ”์—์„œ ๋‹ค๋ฃจ๋Š” ํŒจํ„ด๊ณผ๋„ ์ž˜ ์–ด์šธ๋ฆฝ๋‹ˆ๋‹ค.

useCart.spec.tstypescript
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๋ฅผ ๊ฐ์‹ธ๋Š” ๋ชจ๋“ˆ์„ ๋ชจํ‚นํ•˜์—ฌ, ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์ปดํฌ์ €๋ธ”์ด ์„œ๋ฒ„์— ์ „ํ˜€ ์ ‘๊ทผํ•˜์ง€ ์•Š๊ณ ๋„ ํ†ต์ œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

useProducts.spec.tstypescript
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 ํ…Œ์ŠคํŒ… ๊ฐ€์ด๋“œ๋Š” ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ค€ ํ…Œ์ŠคํŠธ์— ์ด ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

CheckoutButton.spec.tstypescript
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) ํŒŒ์ดํ”„๋ผ์ธ์—๋„ ๊น”๋”ํ•˜๊ฒŒ ์—ฐ๋™๋ฉ๋‹ˆ๋‹ค. ์ด ํŒŒ์ดํ”„๋ผ์ธ์—์„œ๋Š” ์‹คํŒจํ•œ ํ…Œ์ŠคํŠธ๋‚˜ ์ปค๋ฒ„๋ฆฌ์ง€ ํ•˜๋ฝ์ด ๋ณ‘ํ•ฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

vitest.config.ts (coverage section)typescript
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
#testing
#vitest
#vue-test-utils
#interview

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Vue 3 ๊ณ ๊ธ‰ ์ปดํฌ์ €๋ธ” ํŒจํ„ด์˜ ํ•ฉ์„ฑ ๋ฐ ์˜์กด์„ฑ ์ฃผ์ž… ๋‹ค์ด์–ด๊ทธ๋žจ

Vue 3 ์ปดํฌ์ €๋ธ” ์‹ฌํ™” ๊ฐ€์ด๋“œ: ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŒจํ„ด๊ณผ ๊ธฐ์ˆ  ๋ฉด์ ‘ ์งˆ๋ฌธ 2026

Vue 3 ๊ณ ๊ธ‰ ์ปดํฌ์ €๋ธ” ํŒจํ„ด์„ ์ฒด๊ณ„์ ์œผ๋กœ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ์˜์กด์„ฑ ์ฃผ์ž…, ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ํ…Œ์ŠคํŠธ ์ „๋žต, ๊ทธ๋ฆฌ๊ณ  2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Vue 3 Pinia vs Vuex state management comparison

Vue 3 Pinia vs Vuex ์™„๋ฒฝ ๋น„๊ต: 2026๋…„ ์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต๊ณผ ๋ฉด์ ‘ ํ•ต์‹ฌ ์งˆ๋ฌธ

Vue 3 ์ƒํƒœ๊ณ„์—์„œ Pinia์™€ Vuex๋ฅผ ๋น„๊ต ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. Options Store์™€ Setup Store ํŒจํ„ด, TypeScript ํ†ตํ•ฉ, ํฌ๋กœ์Šค ์Šคํ† ์–ด ๊ตฌ์„ฑ, SSR ์ง€์›, Vuex์—์„œ Pinia๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต, ๊ทธ๋ฆฌ๊ณ  2026๋…„ ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ์ƒํƒœ ๊ด€๋ฆฌ ์งˆ๋ฌธ์„ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Nuxt 4 directory structure and migration guide

2026๋…„ Nuxt 4 ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ์ƒˆ๋กœ์šด ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ์™€ Nuxt 3 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต

Nuxt 4์—์„œ ๋„์ž…๋œ app/ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ, ์‹ฑ๊ธ€ํ†ค ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ ˆ์ด์–ด, shallow reactivity, TypeScript ์ปจํ…์ŠคํŠธ ๋ถ„๋ฆฌ๋ฅผ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ƒ์„ธํžˆ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.