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

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

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

์ปดํฌ์ €๋ธ”์€ Vue 3์—์„œ ๋กœ์ง ์žฌ์‚ฌ์šฉ์„ ์œ„ํ•œ ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค. Composition API๊ฐ€ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ๋„์ž…๋œ ํ˜„์žฌ, ๊ฒฌ๊ณ ํ•˜๊ณ  ํƒ€์ž… ์•ˆ์ „ํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ปดํฌ์ €๋ธ”์„ ์„ค๊ณ„ํ•˜๋Š” ๋Šฅ๋ ฅ์€ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์‹œ๋‹ˆ์–ด ๊ฐœ๋ฐœ์ž์™€ ์ค‘๊ธ‰ ๊ฐœ๋ฐœ์ž๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ธฐ์ค€์ด ๋ฉ๋‹ˆ๋‹ค. 2026๋…„์—๋Š” ๋‹จ์ˆœํ•œ ref์™€ computed์˜ ์‚ฌ์šฉ๋ฒ•์„ ๋„˜์–ด, ๋ผ์ดํ”„์‚ฌ์ดํด์˜ ์ •๋ฐ€ํ•œ ๊ด€๋ฆฌ, ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ•จ์ˆ˜์˜ ํ•ฉ์„ฑ, ๋ณต์žกํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“ˆ๋Ÿฌ ์•„ํ‚คํ…์ฒ˜๊นŒ์ง€ ์š”๊ตฌ๋ฉ๋‹ˆ๋‹ค.

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

์ปดํฌ์ €๋ธ”์ด๋ž€?

์ปดํฌ์ €๋ธ”์€ Vue์˜ Composition API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ๊ด€๋ก€์ ์œผ๋กœ use ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: useCounter, useFetchData). Vue 2์˜ ๋ฏน์Šค์ธ๊ณผ ๋‹ฌ๋ฆฌ ์ปดํฌ์ €๋ธ”์€ ๋ช…์‹œ์ ์ธ ํƒ€์ž… ์ง€์ •, ์ด๋ฆ„ ์ถฉ๋Œ ๋ฐฉ์ง€, ์˜์กด์„ฑ์˜ ํˆฌ๋ช…์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์กฐํ™”๋œ ์ปดํฌ์ €๋ธ”์˜ ์„ค๊ณ„ ์›์น™

ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ ์ปดํฌ์ €๋ธ”์€ ๋ช‡ ๊ฐ€์ง€ ์•„ํ‚คํ…์ฒ˜ ์›์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ž…์ถœ๋ ฅ์— ๋Œ€ํ•œ ์—„๊ฒฉํ•œ ํƒ€์ž… ์ •์˜, ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ, ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๊ทธ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์˜ useCounter ์ปดํฌ์ €๋ธ”์€ ์ด๋Ÿฌํ•œ ๊ธฐ๋ณธ ๊ทœ์น™์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

useCounter.tstypescript
import { ref, computed, type Ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
  initialValue?: number
}

interface UseCounterReturn {
  count: Ref<number>
  doubled: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
  const { min = 0, max = Infinity, initialValue = 0 } = options

  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment() {
    if (count.value < max) count.value++
  }

  function decrement() {
    if (count.value > min) count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return { count, doubled, increment, decrement, reset }
}

์ด ๊ตฌํ˜„์—์„œ ์ฃผ๋ชฉํ•  ์„ค๊ณ„ ์š”์†Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. UseCounterOptions ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์„ค์ •์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๊ณ , UseCounterReturn์€ ์ปดํฌ์ €๋ธ”์ด ๋…ธ์ถœํ•˜๋Š” ๋‚ด์šฉ์„ ์ •ํ™•ํ•˜๊ฒŒ ๋ฌธ์„œํ™”ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ ๋ณธ๋ฌธ์—์„œ ๊ธฐ๋ณธ๊ฐ’์„ ํฌํ•จํ•œ ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹น์„ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, ๋ช…์‹œ์  ์„ค์ • ์—†์ด๋„ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

๋ช…๋ช…๋œ ๊ฐ์ฒด(๋ฐฐ์—ด์ด ์•„๋‹Œ)๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ˜ธ์ถœ ์ธก์—์„œ ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹น์„ ํ†ตํ•ด ํ•„์š”ํ•œ ์†์„ฑ๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ทœ์น™์€ Vue ์ƒํƒœ๊ณ„ ์ „๋ฐ˜์— ๊ฑธ์ณ ์ฑ„ํƒ๋˜์–ด ํ˜ธ์ถœ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

min๊ณผ max ๊ฒฝ๊ณ„๊ฐ’์€ ํ”„๋กœ๋•์…˜ ์ปดํฌ์ €๋ธ”์—์„œ ์ž์ฃผ ๋‚˜ํƒ€๋‚˜๋Š” ํŒจํ„ด์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ์ œ์•ฝ ์กฐ๊ฑด์„ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋กœ์ง ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ๊ฒ€์ฆํ•จ์œผ๋กœ์จ, ์†Œ๋น„์ž ์ปดํฌ๋„ŒํŠธ์— ๊ทœ์น™์ด ๋ถ„์‚ฐ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ์ปดํฌ์ €๋ธ”๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

HTTP ์š”์ฒญ ๊ด€๋ฆฌ๋Š” ์ปดํฌ์ €๋ธ”์˜ ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค. ์ž˜ ์„ค๊ณ„๋œ ๋น„๋™๊ธฐ ์ปดํฌ์ €๋ธ”์€ ์š”์ฒญ์˜ ์ „์ฒด ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋กœ๋”ฉ ์ƒํƒœ, ์„ฑ๊ณต, ์—๋Ÿฌ, ์ทจ์†Œ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ํŒจํ„ด์€ ์ž๋™ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์œ„ํ•œ watchEffect์™€ ์š”์ฒญ์˜ ์ ์ ˆํ•œ ์ทจ์†Œ๋ฅผ ์œ„ํ•œ AbortController๋ฅผ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

useFetchData.tstypescript
import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  isLoading: Ref<boolean>
  refresh: () => Promise<void>
}

export function useFetchData<T>(
  url: MaybeRefOrGetter<string>
): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const isLoading = ref(false)
  let abortController: AbortController | null = null

  async function fetchData() {
    // Cancel any in-flight request
    abortController?.abort()
    abortController = new AbortController()

    isLoading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url), {
        signal: abortController.signal
      })
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      isLoading.value = false
    }
  }

  // Re-fetch when URL changes reactively
  watchEffect(() => {
    fetchData()
  })

  onUnmounted(() => abortController?.abort())

  return { data, error, isLoading, refresh: fetchData }
}

url ๋งค๊ฐœ๋ณ€์ˆ˜์— MaybeRefOrGetter<string>์„ ์‚ฌ์šฉํ•˜๋ฉด ์ตœ๋Œ€ํ•œ์˜ ์œ ์—ฐ์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜ธ์ถœ ์ธก์€ ์ •์  ๋ฌธ์ž์—ด, ref, ๋˜๋Š” getter ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. toValue() ํ•จ์ˆ˜๋Š” ์›์‹œ ๊ฐ’์ด๋“  ๋ฆฌ์•กํ‹ฐ๋ธŒ ๊ฐ’์ด๋“  ๊ธฐ์ € ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ํ•ด์„ํ•ฉ๋‹ˆ๋‹ค.

AbortController๋ฅผ ํ™œ์šฉํ•œ ์ทจ์†Œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ์ด์ „ ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ์ƒˆ ์š”์ฒญ์ด ๋ฐœ์ƒํ•  ๋•Œ ๊ฒฝ์Ÿ ์กฐ๊ฑด(race condition)์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. onUnmounted ํ›…์€ ์ปดํฌ๋„ŒํŠธ ํŒŒ๊ดด ์‹œ ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์„ ํ™•์‹คํ•˜๊ฒŒ ์ •๋ฆฌํ•˜์—ฌ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜์™€ ์–ธ๋งˆ์šดํŠธ๋œ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’์— ๋…ธ์ถœ๋œ refresh ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์†Œ๋น„์ž ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆ˜๋™์œผ๋กœ ๋ฐ์ดํ„ฐ ์žฌ๋กœ๋”ฉ์„ ํŠธ๋ฆฌ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "์ƒˆ๋กœ๊ณ ์นจ" ๋ฒ„ํŠผ๊ณผ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์•ก์…˜์— ํ•„์ˆ˜์ ์ธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ” ๋‚ด์˜ ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…

๋ผ์ดํ”„์‚ฌ์ดํด ํ›…(onMounted, onUnmounted ๋“ฑ)์€ ์ปดํฌ์ €๋ธ” ๋ณธ๋ฌธ์—์„œ ๋™๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ์ด๋‚˜ setTimeout ์•ˆ์—์„œ๋Š” ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. Vue๋Š” ํ˜ธ์ถœ ์‹œ์ ์— ํ™œ์„ฑํ™”๋œ ์ปดํฌ๋„ŒํŠธ ์ธ์Šคํ„ด์Šค์— ์ด๋Ÿฌํ•œ ํ›…์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ง€์—ฐ๋œ ํ˜ธ์ถœ์€ ๋ฌด์Œ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ์ปดํฌ๋„ŒํŠธ์— ํ›…์„ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ”์˜ ํ•ฉ์„ฑ

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

usePaginatedSearch.tstypescript
import { ref, computed, watch, type Ref } from 'vue'
import { useFetchData } from './useFetchData'
import { useDebouncedRef } from './useDebouncedRef'

interface UsePaginatedSearchReturn<T> {
  query: Ref<string>
  page: Ref<number>
  results: Ref<T[] | null>
  totalPages: Ref<number>
  isLoading: Ref<boolean>
  error: Ref<string | null>
  nextPage: () => void
  prevPage: () => void
}

export function usePaginatedSearch<T>(
  baseUrl: string,
  perPage = 20
): UsePaginatedSearchReturn<T> {
  const query = useDebouncedRef('', 300)
  const page = ref(1)
  const totalPages = ref(1)

  const apiUrl = computed(
    () => `${baseUrl}?q=${encodeURIComponent(query.value)}&page=${page.value}&limit=${perPage}`
  )

  const { data, error, isLoading } = useFetchData<{ items: T[]; total: number }>(apiUrl)

  const results = computed(() => data.value?.items ?? null)

  watch(data, (response) => {
    if (response) {
      totalPages.value = Math.ceil(response.total / perPage)
    }
  })

  // Reset to page 1 when query changes
  watch(query, () => { page.value = 1 })

  function nextPage() {
    if (page.value < totalPages.value) page.value++
  }

  function prevPage() {
    if (page.value > 1) page.value--
  }

  return { query, page, results, totalPages, isLoading, error, nextPage, prevPage }
}

์ด ์ปดํฌ์ €๋ธ”์€ ์—ฌ๋Ÿฌ ๊ณ ๊ธ‰ ํ•ฉ์„ฑ ๊ธฐ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. useDebouncedRef๋Š” ํƒ€์ด๋ฐ ์ œ์–ด ๋กœ์ง์„ ์บก์Аํ™”ํ•˜์—ฌ, ํ‚ค ์ž…๋ ฅ๋งˆ๋‹ค API์— ๊ณผ๋„ํ•œ ์š”์ฒญ์ด ์ „์†ก๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. computed์ธ apiUrl์€ ์ฟผ๋ฆฌ ๋˜๋Š” ํŽ˜์ด์ง€๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ž๋™์œผ๋กœ URL์„ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ, useFetchData๋ฅผ ํ†ตํ•ด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ํŽ˜์นญ์„ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

query์— ๋Œ€ํ•œ watch๋Š” ์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰์ด ์ด๋ฃจ์–ด์งˆ ๋•Œ๋งˆ๋‹ค ํŽ˜์ด์ง€๋ฅผ 1๋กœ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๋™์ž‘์ด์ง€๋งŒ, ๋‹จ์ˆœํ•œ ๊ตฌํ˜„์—์„œ๋Š” ์ข…์ข… ๊ฐ„๊ณผ๋˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์„ธ๋ถ€ ์‚ฌํ•ญ์€ ๋กœ์ง์„ ์ปดํฌ๋„ŒํŠธ์— ๋ถ„์‚ฐ์‹œํ‚ค์ง€ ์•Š๊ณ  ์ปดํฌ์ €๋ธ”์— ์บก์Аํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•œ ์ด์œ ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ”์˜ ํ•ฉ์„ฑ์€ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. useDebouncedRef๋Š” ๋””๋ฐ”์šด์‹ฑ์„ ๊ด€๋ฆฌํ•˜๊ณ , useFetchData๋Š” ์š”์ฒญ-์‘๋‹ต ์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, usePaginatedSearch๊ฐ€ ์ „์ฒด๋ฅผ ์กฐ์œจํ•ฉ๋‹ˆ๋‹ค. ์ด ์•„ํ‚คํ…์ฒ˜ ๋•๋ถ„์— ๊ฐ ๋ ˆ์ด์–ด์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Vue.js / Nuxt.js ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

provide/inject๋ฅผ ํ™œ์šฉํ•œ ์˜์กด์„ฑ ์ฃผ์ž…

์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ์—์„œ ๋ฉ€๋ฆฌ ๋–จ์–ด์ง„ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์— ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, Vue์˜ provide/inject ์‹œ์Šคํ…œ์€ props ๋“œ๋ฆด๋ง์— ๋Œ€ํ•œ ์šฐ์•„ํ•œ ๋Œ€์•ˆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ์ €๋ธ”์€ ์ด ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์บก์Аํ™”ํ•˜์—ฌ ๊น”๋”ํ•˜๊ณ  ํƒ€์ž… ์•ˆ์ „ํ•œ API๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

useTheme.tstypescript
import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue'

type Theme = 'light' | 'dark' | 'system'

interface ThemeContext {
  theme: Readonly<Ref<Theme>>
  setTheme: (t: Theme) => void
  resolvedTheme: Readonly<Ref<'light' | 'dark'>>
}

const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

export function provideTheme(initial: Theme = 'system') {
  const theme = ref<Theme>(initial)

  const resolvedTheme = computed<'light' | 'dark'>(() => {
    if (theme.value !== 'system') return theme.value
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  })

  function setTheme(t: Theme) {
    theme.value = t
  }

  const context: ThemeContext = {
    theme: readonly(theme),
    setTheme,
    resolvedTheme: readonly(resolvedTheme)
  }

  provide(ThemeKey, context)
  return context
}

export function useTheme(): ThemeContext {
  const context = inject(ThemeKey)
  if (!context) {
    throw new Error('useTheme() requires a parent component to call provideTheme()')
  }
  return context
}

Symbol๊ณผ ํ•จ๊ป˜ ํƒ€์ž…์ด ์ง€์ •๋œ InjectionKey๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฃผ์ž… ํ‚ค์˜ ๊ณ ์œ ์„ฑ์ด ๋ณด์žฅ๋˜๊ณ , inject ํ˜ธ์ถœ ์‹œ ์ž๋™์œผ๋กœ ํƒ€์ž…์ด ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜ ํ‚ค์—์„œ ๋ฐœ์ƒํ•˜๋˜ ํ‚ค ์ถฉ๋Œ ์œ„ํ—˜์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

provideTheme๊ณผ useTheme์˜ ๋ถ„๋ฆฌ๋Š” ๋ช…ํ™•ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฝ๊ณ„๋ฅผ ํ™•๋ฆฝํ•ฉ๋‹ˆ๋‹ค. ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ(๋˜๋Š” ๋ ˆ์ด์•„์›ƒ)๊ฐ€ provideTheme์„ ํ˜ธ์ถœํ•˜์—ฌ ์ปจํ…์ŠคํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ , ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ useTheme์„ ํ˜ธ์ถœํ•˜์—ฌ ์†Œ๋น„ํ•ฉ๋‹ˆ๋‹ค. ๋…ธ์ถœ๋˜๋Š” ref์— readonly๋ฅผ ์ ์šฉํ•˜๋ฉด ์†Œ๋น„์ž ์ธก์—์„œ์˜ ์˜๋„์น˜ ์•Š์€ ๋ณ€์ด๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

resolvedTheme computed๋Š” ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ํŒจํ„ด์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ถ”์ƒ์ ์ธ ์„ค์ • ๊ฐ’('system')์„ ์‹œ์Šคํ…œ ํ™˜๊ฒฝ ์„ค์ •์— ๋”ฐ๋ฅธ ๊ตฌ์ฒด์ ์ธ ๊ฐ’('light' ๋˜๋Š” 'dark')์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ์ˆ˜์ค€์˜ ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด ์†Œ๋น„์ž ์ปดํฌ๋„ŒํŠธ์˜ ๋กœ์ง์ด ์ƒ๋‹นํžˆ ๋‹จ์ˆœํ™”๋ฉ๋‹ˆ๋‹ค.

ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ปดํฌ์ €๋ธ”

ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ์ปดํฌ์ €๋ธ”๋กœ์˜ ์บก์Аํ™”์—์„œ ํฐ ์ด์ ์„ ์–ป๋Š” ๋ณต์žกํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ํŒจํ„ด์€ ๊ทœ์น™ ๊ธฐ๋ฐ˜์˜ ์„ ์–ธ์  ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์—๋Ÿฌ์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๊ด€๋ฆฌ, ์ „์ฒด ์œ ํšจ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

useFormValidation.tstypescript
import { reactive, computed, type UnwrapNestedRefs } from 'vue'

type ValidationRule<T> = (value: T) => string | true
type FieldRules<T> = { [K in keyof T]?: ValidationRule<T[K]>[] }

interface UseFormReturn<T extends Record<string, any>> {
  fields: UnwrapNestedRefs<T>
  errors: Record<keyof T, string>
  isValid: Ref<boolean>
  validate: () => boolean
  resetErrors: () => void
}

export function useFormValidation<T extends Record<string, any>>(
  initialValues: T,
  rules: FieldRules<T>
): UseFormReturn<T> {
  const fields = reactive({ ...initialValues }) as UnwrapNestedRefs<T>

  const errors = reactive(
    Object.keys(initialValues).reduce(
      (acc, key) => ({ ...acc, [key]: '' }),
      {} as Record<keyof T, string>
    )
  )

  function validate(): boolean {
    let valid = true
    for (const key of Object.keys(rules) as (keyof T)[]) {
      const fieldRules = rules[key] || []
      errors[key] = '' as any
      for (const rule of fieldRules) {
        const result = rule(fields[key])
        if (result !== true) {
          errors[key] = result as any
          valid = false
          break // Stop at first error per field
        }
      }
    }
    return valid
  }

  function resetErrors() {
    for (const key of Object.keys(errors)) {
      (errors as any)[key] = ''
    }
  }

  const isValid = computed(() =>
    Object.values(errors).every((e) => e === '')
  )

  return { fields, errors, isValid, validate, resetErrors }
}

์ด ์ปดํฌ์ €๋ธ”์€ fields ๊ฐ์ฒด์— ref ๋Œ€์‹  reactive๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด .value ์—†์ด ์†์„ฑ์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™ ์‹œ์Šคํ…œ์€ ๊ฐ„๋‹จํ•œ ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๊ฐ ๊ทœ์น™์€ ์„ฑ๊ณต ์‹œ true๋ฅผ, ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. break ๋ฌธ์€ ํ•„๋“œ๋‹น ์ฒซ ๋ฒˆ์งธ ์—๋Ÿฌ์—์„œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ค‘๋‹จํ•˜์—ฌ, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ˆ„์ ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

FieldRules<T> ํƒ€์ž…์€ TypeScript์˜ ๋งคํ•‘๋œ ํƒ€์ž…(mapped types)์„ ํ™œ์šฉํ•˜์—ฌ ๊ทœ์น™์ด ํผ ํ•„๋“œ์— ์ •ํ™•ํ•˜๊ฒŒ ๋Œ€์‘ํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ์ปดํŒŒ์ผ ํƒ€์ž„์— ํƒ€์ž… ์—๋Ÿฌ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ํ”„๋กœ๋•์…˜ ๋ฒ„๊ทธ๋ฅผ ์ค„์ž…๋‹ˆ๋‹ค.

isValid computed๋Š” ํผ ์ „์ฒด ์ƒํƒœ์— ๋Œ€ํ•œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ œ์ถœ ๋ฒ„ํŠผ์˜ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”์— ์ปดํฌ๋„ŒํŠธ ์ธก์˜ ์ถ”๊ฐ€ ๋กœ์ง์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ” ํ…Œ์ŠคํŠธ

์ปดํฌ์ €๋ธ”์€ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ ์‹œ์Šคํ…œ๊ณผ ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์— ์˜์กดํ•˜๋ฏ€๋กœ, ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋ ค๋ฉด ํ™œ์„ฑํ™”๋œ Vue ์ปจํ…์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ‘œ์ค€ ๊ธฐ๋ฒ•์€ setup ์•ˆ์—์„œ ์ปดํฌ์ €๋ธ”์„ ์ธ์Šคํ„ด์Šคํ™”ํ•˜๋Š” ์ตœ์†Œํ•œ์˜ ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. Vitest์™€ Vue Test Utils๊ฐ€ ํ•„์š”ํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

useCounter.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useCounter } from './useCounter'

function withSetup<T>(composable: () => T): { result: T; unmount: () => void } {
  let result!: T
  const wrapper = mount(
    defineComponent({
      setup() {
        result = composable()
        return () => h('div')
      }
    })
  )
  return { result, unmount: () => wrapper.unmount() }
}

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = withSetup(() => useCounter())
    expect(result.count.value).toBe(0)
  })

  it('respects min and max boundaries', () => {
    const { result } = withSetup(() =>
      useCounter({ min: 0, max: 3, initialValue: 3 })
    )
    result.increment()
    expect(result.count.value).toBe(3) // Capped at max

    result.count.value = 0
    result.decrement()
    expect(result.count.value).toBe(0) // Capped at min
  })

  it('computes doubled value reactively', () => {
    const { result } = withSetup(() => useCounter({ initialValue: 5 }))
    expect(result.doubled.value).toBe(10)
    result.increment()
    expect(result.doubled.value).toBe(12)
  })
})

์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ withSetup์€ ๋ชจ๋“  ์ปดํฌ์ €๋ธ”์˜ ํ…Œ์ŠคํŠธ์— ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์œ ํšจํ•œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ ์ปดํฌ์ €๋ธ”์„ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์œ ์ผํ•œ ์—ญํ• ์ธ ์ตœ์†Œํ•œ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค. unmount ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜์„ ํ†ตํ•ด onUnmounted ํ›…์˜ ์ •๋ฆฌ ๋™์ž‘๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋Š” ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์ธก๋ฉด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ์˜ ์ดˆ๊ธฐํ™”, ๋น„์ฆˆ๋‹ˆ์Šค ์ œ์•ฝ ์กฐ๊ฑด(min/max ๊ฒฝ๊ณ„)์˜ ์ค€์ˆ˜, ๊ณ„์‚ฐ๋œ ๊ฐ’์˜ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ์ž…๋‹ˆ๋‹ค. ์ด ์ˆ˜์ค€์˜ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ํ”„๋กœ๋•์…˜ ์ปดํฌ์ €๋ธ”์— ๊ธฐ๋Œ€๋˜๋Š” ์ตœ์†Œํ•œ์˜ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

useFetchData์™€ ๊ฐ™์€ ๋น„๋™๊ธฐ ์ปดํฌ์ €๋ธ”์˜ ๊ฒฝ์šฐ, fetch์˜ ๋ชจํ‚น๊ณผ flushPromises()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ด์„œ์…˜ ์ „์— Promise ํ•ด๊ฒฐ์„ ๋Œ€๊ธฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ฃผ์ œ๋Š” Vue์—์„œ์˜ ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ ์ˆ™๋ จ๋„๋ฅผ ํ‰๊ฐ€ํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ๋ฉด์ ‘ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค.

VueUse: ์ฐธ์กฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

VueUse๋Š” ๋ธŒ๋ผ์šฐ์ € ์ธํ„ฐ๋ž™์…˜, ์„ผ์„œ, ์• ๋‹ˆ๋ฉ”์ด์…˜, ๋ฆฌ์•กํ‹ฐ๋ธŒ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํฌ๊ด„ํ•˜๋Š” 200๊ฐœ ์ด์ƒ์˜ ์ปดํฌ์ €๋ธ”์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ปค์Šคํ…€ ์ปดํฌ์ €๋ธ”์„ ๊ฐœ๋ฐœํ•˜๊ธฐ ์ „์— VueUse๊ฐ€ ์ด๋ฏธ ํ•ด๊ฒฐ์ฑ…์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์ด๋Ÿฌํ•œ ์ปดํฌ์ €๋ธ”์„ ์ ์‘, ํ™•์žฅ, ๋””๋ฒ„๊น…ํ•˜๋ ค๋ฉด ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํŒจํ„ด์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

๊ธฐ์ˆ  ๋ฉด์ ‘ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ์งˆ๋ฌธ

Vue 3 ์ปดํฌ์ €๋ธ”์€ 2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ๋น ์งˆ ์ˆ˜ ์—†๋Š” ์ฃผ์ œ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ์งˆ๋ฌธ๋“ค์€ ์ค‘๊ธ‰๋ถ€ํ„ฐ ์‹œ๋‹ˆ์–ด ๋ ˆ๋ฒจ๊นŒ์ง€ ๋ฉด์ ‘๊ด€์ด ํ‰๊ฐ€ํ•˜๋Š” ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ”๊ณผ ๋ฏน์Šค์ธ์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? Vue 2์˜ ๋ฏน์Šค์ธ์€ ์˜ต์…˜์„ ์ปดํฌ๋„ŒํŠธ์— ๋ณ‘ํ•ฉํ•˜์—ฌ ์ด๋ฆ„ ์ถฉ๋Œ, ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜์˜ ๋ถˆ๋ช…ํ™•ํ•จ, ํƒ€์ž… ์ง€์ •์˜ ๋ถ€์žฌ๋ฅผ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ์ €๋ธ”์€ ํƒ€์ž…์ด ์ง€์ •๋œ ๋ฐ˜ํ™˜๊ฐ’์„ ํ†ตํ•ด ๋ช…์‹œ์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋…ธ์ถœํ•˜๊ณ , ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹น์œผ๋กœ ์ด๋ฆ„ ๋ณ€๊ฒฝ์ด ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์˜์กด์„ฑ์„ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์œ ์ง€๋ณด์ˆ˜์„ฑ, ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ, TypeScript ํ˜ธํ™˜์„ฑ ๋ชจ๋“  ์ธก๋ฉด์—์„œ ์ปดํฌ์ €๋ธ”์ด ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ” ๋‚ด์˜ ๋ถ€์ˆ˜ ํšจ๊ณผ(side effect)๋Š” ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ๋ชจ๋“  ๋ถ€์ˆ˜ ํšจ๊ณผ(์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ, ํƒ€์ด๋จธ, WebSocket ๊ตฌ๋…)๋Š” onUnmounted ํ›… ๋˜๋Š” watchEffect์˜ ํด๋ฆฐ์—… ๋ฉ”์ปค๋‹ˆ์ฆ˜์—์„œ ์ •๋ฆฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ •๋ฆฌ๋ฅผ ์ƒ๋žตํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜์™€ ์ปดํฌ๋„ŒํŠธ ์žฌ๋งˆ์šดํŠธ ์‹œ ์˜ˆ์ธกํ•  ์ˆ˜ ์—†๋Š” ๋™์ž‘์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

setup() ์™ธ๋ถ€์—์„œ ์ปดํฌ์ €๋ธ”์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ์ €๋ธ”์€ ์ปดํฌ๋„ŒํŠธ์˜ setup() ํ•จ์ˆ˜ ๋‚ด๋ถ€ ๋˜๋Š” ๋‹ค๋ฅธ ์ปดํฌ์ €๋ธ” ๋‚ด๋ถ€์—์„œ ๋™๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ปจํ…์ŠคํŠธ ์™ธ๋ถ€์—์„œ์˜ ํ˜ธ์ถœ์€ Vue๊ฐ€ ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…๊ณผ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ธ์Šคํ„ด์Šค์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉํ•ดํ•ฉ๋‹ˆ๋‹ค.

์ปดํฌ์ €๋ธ”๋กœ ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ์ฃผ๋กœ ๋‘ ๊ฐ€์ง€ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์€ ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜ ์™ธ๋ถ€(๋ชจ๋“ˆ ์ˆ˜์ค€)์—์„œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ชจ๋“  ์†Œ๋น„์ž ๊ฐ„์— ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. provide/inject ํŒจํ„ด์€ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ์ปจํ…์ŠคํŠธ์ ์œผ๋กœ ์ „ํŒŒํ•ฉ๋‹ˆ๋‹ค. ์ง„์ •ํ•œ ์ „์—ญ ์ƒํƒœ์—๋Š” ์‹ฑ๊ธ€ํ†ค์„, ํ•˜์œ„ ํŠธ๋ฆฌ์— ๋ฒ”์œ„๊ฐ€ ์ง€์ •๋œ ์ƒํƒœ์—๋Š” provide/inject๊ฐ€ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์„ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ์ €๋ธ”์€ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๊นŒ? ์•ž์„œ ์†Œ๊ฐœํ•œ withSetup ๊ธฐ๋ฒ•์€ ํ•„์š”ํ•œ Vue ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ตœ์†Œํ•œ์˜ ๋ž˜ํผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. onMounted์™€ ๊ฐ™์€ ํ›…์˜ ๊ฒฝ์šฐ Vue Test Utils๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ๋˜๋Š” ๊ฐ€์ƒ DOM์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งˆ์šดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ˆœ์ˆ˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปดํฌ์ €๋ธ”์€ Vue์˜ effectScope๋กœ ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SSR ํ™˜๊ฒฝ์—์„œ ๋น„๋™๊ธฐ ์ปดํฌ์ €๋ธ”์—๋Š” ์–ด๋–ค ์ „๋žต์ด ํ•„์š”ํ•ฉ๋‹ˆ๊นŒ? SSR ์ปจํ…์ŠคํŠธ(Nuxt ๋˜๋Š” Vue ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง)์—์„œ๋Š” ๋น„๋™๊ธฐ ์ปดํฌ์ €๋ธ”์— ํŠน๋ณ„ํ•œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. onMounted๋Š” ์„œ๋ฒ„ ์ธก์—์„œ ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋ฉฐ, fetch ํ˜ธ์ถœ์€ ํ”„๋ ˆ์ž„์›Œํฌ์˜ SSR ๋ฉ”์ปค๋‹ˆ์ฆ˜(Nuxt์˜ useAsyncData ๋“ฑ)์„ ํ†ตํ•ด ๊ด€๋ฆฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ์ €๋ธ”์€ ์‹คํ–‰ ํ™˜๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ ๋™์ž‘์„ ์กฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

๊ฒฐ๋ก 

Vue 3์˜ ์ปดํฌ์ €๋ธ”์€ ๋ฏน์Šค์ธ์—์„œ์˜ ๋‹จ์ˆœํ•œ ๊ตฌ๋ฌธ์  ์ง„ํ™”๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋กœ์ง์„ ๊ตฌ์กฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๊ด€ํ•œ ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ์ „ํ™˜์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ˆ™๋‹ฌ์€ ํ”„๋กœ๋•์…˜ ๊ฐœ๋ฐœ๊ณผ ๊ธฐ์ˆ  ๋ฉด์ ‘ ๋ชจ๋‘์—์„œ ์ƒ๋‹นํ•œ ๊ฒฝ์Ÿ๋ ฅ์ด ๋ฉ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ์—„๊ฒฉํ•œ ํƒ€์ž… ์ •์˜: ์˜ต์…˜๊ณผ ๋ฐ˜ํ™˜๊ฐ’์— ๋ช…์‹œ์  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•˜๋ฉด ์‹ ๋ขฐ์„ฑ๊ณผ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค
  • ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ: ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ถ€์ˆ˜ ํšจ๊ณผ๋Š” ํ•ญ์ƒ onUnmounted์—์„œ ์ •๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
  • ํ•ฉ์„ฑ: ๋ชจ๋†€๋ฆฌ์‹ ์ปดํฌ์ €๋ธ”์ด ์•„๋‹Œ, ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ ์ „๋ฌธํ™”๋œ ์ปดํฌ์ €๋ธ”์„ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค
  • ์ •๋ฐ€ํ•œ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ: MaybeRefOrGetter, toValue, watchEffect๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์œ ์—ฐ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค
  • ์˜์กด์„ฑ ์ฃผ์ž…: ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ๋‚ด ์ƒํƒœ ๊ณต์œ ์—๋Š” ํƒ€์ž…์ด ์ง€์ •๋œ InjectionKey์™€ provide/inject๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ: withSetup ๊ธฐ๋ฒ•์€ ๊ฐ ์ปดํฌ์ €๋ธ”์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์†Œํ•œ์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค
  • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ: ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ปดํฌ์ €๋ธ”์— ์บก์Аํ™”ํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์˜ ์ผ๊ด€์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค

๊ฒฌ๊ณ ํ•œ ์ปดํฌ์ €๋ธ”์„ ์„ค๊ณ„ํ•˜๊ณ , ํ•ฉ์„ฑํ•˜๋ฉฐ, ํ…Œ์ŠคํŠธํ•˜๋Š” ๋Šฅ๋ ฅ์€ 2026๋…„ Vue ์‹œ์žฅ์—์„œ ์‹œ๋‹ˆ์–ด ๋ ˆ๋ฒจ์˜ ์ธ์žฌ๋ฅผ ๊ตฌ๋ณ„ํ•˜๋Š” ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํŒจํ„ด์€ Vue 3 ๋ฐ Nuxt 3 ํ”„๋กœ๋•์…˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ์ˆ ์  ํ† ๋Œ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

ํƒœ๊ทธ

#vue
#composables
#composition-api
#interview
#deep-dive

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

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 ์ปจํ…์ŠคํŠธ ๋ถ„๋ฆฌ๋ฅผ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ƒ์„ธํžˆ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ๋ธ”๋ก๊ณผ Vue ๋กœ๊ณ ๊ฐ€ ๋“ฑ์žฅํ•˜๋Š” Vue.js ๊ธฐ์ˆ  ๋ฉด์ ‘ ์ผ๋Ÿฌ์ŠคํŠธ

Vue.js ๋ฉด์ ‘ ํ•ต์‹ฌ ์งˆ๋ฌธ: ํ•ฉ๊ฒฉ์„ ์œ„ํ•œ 25๋ฌธํ•ญ

Vue.js ๋ฉด์ ‘์„ ์œ„ํ•œ 25๊ฐœ์˜ ํ•ต์‹ฌ ์งˆ๋ฌธ. ๋ฐ˜์‘์„ฑ๋ถ€ํ„ฐ composables๊นŒ์ง€, ๋‹ค์Œ ๋ฉด์ ‘์—์„œ ๋น›๋‚  ํ•ต์‹ฌ ๊ฐœ๋…์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.