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

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

Nuxt 4 directory structure and migration guide

Nuxt 4๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์™€ ์„ค์ • ํŒŒ์ผ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•˜๋Š” ์ƒˆ๋กœ์šด ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. 2025๋…„ 7์›”์— ์ถœ์‹œ๋˜์–ด 2026๋…„ 4์›” ๊ธฐ์ค€ ๋ฒ„์ „ 4.4์— ๋„๋‹ฌํ•œ ์ด ๋ฉ”์ด์ € ์—…๋ฐ์ดํŠธ๋Š”, ์‹ฑ๊ธ€ํ†ค ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ ˆ์ด์–ด, shallow reactivity ๊ธฐ๋ณธ ์ ์šฉ, TypeScript ์ปจํ…์ŠคํŠธ ๋ถ„ํ•  ๋“ฑ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Nuxt 2์—์„œ Nuxt 3์œผ๋กœ์˜ ์ „ํ™˜๊ณผ ๋น„๊ตํ•˜๋ฉด, ์ด๋ฒˆ ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฒฝ๋กœ๋Š” ํ›จ์”ฌ ๋งค๋„๋Ÿฝ์Šต๋‹ˆ๋‹ค.

์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ ์ œ๊ณต

Nuxt ํŒ€์€ Codemod์™€ ํ˜‘๋ ฅํ•˜์—ฌ ๋Œ€๋ถ€๋ถ„์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‹จ๊ณ„๋ฅผ ์ž๋™ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. npx codemod@latest nuxt/4/migration-recipe๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ตฌ์„ฑ, ๋ฐ์ดํ„ฐ ํŒจ์นญ ์—…๋ฐ์ดํŠธ, ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” API ๊ต์ฒด๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

Nuxt 4์˜ app/ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ

Nuxt 4์—์„œ๋Š” ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ app/ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค. ์ด ๋ณ€๊ฒฝ์€ ์‹ค์ œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์˜ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. Linux์™€ Windows์˜ ํŒŒ์ผ ๊ฐ์‹œ์ž(file watcher)๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ๊ฐ€ node_modules/, .git/, ์„ค์ • ํŒŒ์ผ๊ณผ ํ˜ผ์žฌ๋˜์ง€ ์•Š๊ณ  ์ „์šฉ ํ•˜์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ €์žฅ๋  ๋•Œ ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ ˆ์ด์•„์›ƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

text
my-nuxt-app/
โ”œโ”€ app/
โ”‚  โ”œโ”€ assets/
โ”‚  โ”œโ”€ components/
โ”‚  โ”œโ”€ composables/
โ”‚  โ”œโ”€ layouts/
โ”‚  โ”œโ”€ middleware/
โ”‚  โ”œโ”€ pages/
โ”‚  โ”œโ”€ plugins/
โ”‚  โ”œโ”€ utils/
โ”‚  โ”œโ”€ app.vue
โ”‚  โ”œโ”€ app.config.ts
โ”‚  โ””โ”€ error.vue
โ”œโ”€ content/
โ”œโ”€ public/
โ”œโ”€ shared/         # ์‹ ๊ทœ: app๊ณผ server ๊ฐ„ ๊ณต์œ  ์ฝ”๋“œ
โ”œโ”€ server/
โ””โ”€ nuxt.config.ts

shared/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” ์ฃผ๋ชฉํ•  ๋งŒํ•œ ์ถ”๊ฐ€ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. shared/์— ๋ฐฐ์น˜๋œ composable์ด๋‚˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋Š” Vue ์•ฑ๊ณผ Nitro ์„œ๋ฒ„ ์–‘์ชฝ์—์„œ ์ž๋™ ์ž„ํฌํŠธ๋ฉ๋‹ˆ๋‹ค. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์Šคํ‚ค๋งˆ, ํƒ€์ž… ์ •์˜, ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์ปจํ…์ŠคํŠธ ๊ฐ„์— ๊ณต์œ ํ•  ๋•Œ ์ˆ˜๋™์œผ๋กœ import ๋ฌธ์„ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค.

Nuxt 3์—์„œ Nuxt 4๋กœ์˜ ๋‹จ๊ณ„๋ณ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

์—…๊ทธ๋ ˆ์ด๋“œ ๊ณผ์ •์€ ํ•˜๋‚˜์˜ ๋ช…๋ น์–ด์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. Nuxt๋Š” ๊ธฐ์กด์˜ ํ”Œ๋žซ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ๋ณ€๊ฒฝ ์—†์ด ๊ณ„์† ๋™์ž‘ํ•˜๋ฏ€๋กœ, ์ ์ง„์ ์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

bash
# Nuxt ์—…๊ทธ๋ ˆ์ด๋“œ ๋ฐ ์˜์กด์„ฑ ์ค‘๋ณต ์ œ๊ฑฐ
npx nuxt upgrade --dedupe

ํŒจํ‚ค์ง€ ์—…๊ทธ๋ ˆ์ด๋“œ ํ›„, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ์„ app/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

bash
# ๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ตฌ์„ฑ ์ž๋™ํ™”
npx codemod@latest nuxt/4/file-structure

์ด codemod๋Š” assets/, components/, composables/, layouts/, middleware/, pages/, plugins/, utils/, app.vue, error.vue, app.config.ts๋ฅผ app/์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ฃจํŠธ์— ๋‚จ์•„์•ผ ํ•˜๋Š” ํŒŒ์ผ๋“ค(nuxt.config.ts, server/, public/, content/)์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ตฌ์„ฑ์„ ์ฆ‰์‹œ ์ง„ํ–‰ํ•˜์ง€ ์•Š์œผ๋ ค๋Š” ๊ฒฝ์šฐ, ์†Œ์Šค ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

nuxt.config.tstypescript
export default defineNuxtConfig({
  srcDir: '.',
  dir: { app: 'app' },
})

์ด ์„ค์ •์„ ํ†ตํ•ด Nuxt 4๊ฐ€ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ฒŒ ๋˜์–ด, Nuxt 3๊ณผ ๋™์ผํ•œ ๋™์ž‘์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์‹ฑ๊ธ€ํ†ค ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ ˆ์ด์–ด์™€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ‚ค

Nuxt 4์—์„œ๋Š” useAsyncData์™€ useFetch์˜ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๋ฐฉ์‹์ด ๊ทผ๋ณธ์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…๋ฆฝ์ ์ธ ๋ณต์‚ฌ๋ณธ์„ ์œ ์ง€ํ•˜๋Š” ๋Œ€์‹ , ํ•˜๋‚˜์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ฐธ์กฐ๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

app/composables/useProductData.tstypescript
export function useProductData(productId: string) {
  return useAsyncData(
    `product-${productId}`,
    () => $fetch(`/api/products/${productId}`),
    {
      getCachedData: (key, nuxtApp, ctx) => {
        // ctx.cause๋กœ ํŒจ์นญ ์ด์œ ๋ฅผ ํŒ๋ณ„ ๊ฐ€๋Šฅ
        if (ctx.cause === 'refresh:manual') return undefined
        return nuxtApp.payload.data[key]
      },
    },
  )
}

์ด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด์—๋Š” ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๊ณต์œ  ref: ๋‘ ์ปดํฌ๋„ŒํŠธ์—์„œ useProductData('abc')๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋™์ผํ•œ data, error, status ref๊ฐ€ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. ํ•œ์ชฝ์„ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋‹ค๋ฅธ ์ชฝ์—๋„ ์ฆ‰์‹œ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
  • ์ž๋™ ์ •๋ฆฌ: ํŠน์ • ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋งˆ์ง€๋ง‰ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋˜๋ฉด, ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค.
  • ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ‚ค: ํ‚ค๋ฅผ computed๋‚˜ ref๋กœ ๋ž˜ํ•‘ํ•˜๋ฉด, ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค์‹œ ํŒจ์นญ๋ฉ๋‹ˆ๋‹ค.

getCachedData ์ฝœ๋ฐฑ์€ ์ด์ œ cause ์†์„ฑ('initial', 'refresh:hook', 'refresh:manual', 'watch')์„ ๊ฐ€์ง„ ์ปจํ…์ŠคํŠธ ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์บ์‹œ ๋ฐ์ดํ„ฐ ์ œ๊ณต๊ณผ ์ƒˆ๋กœ์šด ํŒจ์นญ ๊ฐ„์˜ ์ „ํ™˜์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ๊ฐ’ ๋ณ€๊ฒฝ ์‚ฌํ•ญ

useAsyncData/useFetch์˜ data์™€ error ๊ธฐ๋ณธ๊ฐ’์ด null์—์„œ undefined๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. === null ๊ฒ€์‚ฌ๋ฅผ === undefined๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ฑฐ๋‚˜ ๋А์Šจํ•œ ๋™๋“ฑ ๋น„๊ต๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Shallow Reactivity ๊ธฐ๋ณธ ์ ์šฉ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ

Nuxt 4์—์„œ๋Š” useAsyncData์™€ useFetch์˜ data๊ฐ€ ref ๋Œ€์‹  shallowRef๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Vue๊ฐ€ ๋” ์ด์ƒ ์ค‘์ฒฉ๋œ ๋ชจ๋“  ์†์„ฑ์„ ์žฌ๊ท€์ ์œผ๋กœ ์ถ”์ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊นŠ์ด ์ค‘์ฒฉ๋œ ๊ฐ์ฒด๋‚˜ ๋Œ€๊ทœ๋ชจ ๋ฐฐ์—ด์„ ํฌํ•จํ•˜๋Š” API ์‘๋‹ต์— ๋Œ€ํ•ด ์ธก์ • ๊ฐ€๋Šฅํ•œ ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

app/pages/dashboard.vuetypescript
<script setup lang="ts">
// data๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ shallowRef๋กœ ๋™์ž‘
const { data: metrics } = await useFetch('/api/dashboard/metrics')

// ์ง์ ‘์ ์ธ ์†์„ฑ ๋ณ€๊ฒฝ์€ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜์ง€ ์•Š์Œ
// metrics.value.visits = 100  // ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Œ

// ์ „์ฒด ๊ฐ’์„ ๊ต์ฒดํ•˜๋ฉด ์—…๋ฐ์ดํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋จ
metrics.value = { ...metrics.value, visits: 100 }

// ํŠน์ • ํ˜ธ์ถœ์—์„œ ๋”ฅ ๋ฆฌ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ํ™œ์„ฑํ™”
const { data: settings } = await useFetch('/api/settings', {
  deep: true,
})
</script>

๋Œ€์‹œ๋ณด๋“œ, ์ƒํ’ˆ ๋ชฉ๋ก, ๊ธฐ์‚ฌ ํŽ˜์ด์ง€ ๋“ฑ ๋Œ€๋ถ€๋ถ„์˜ ์ฝ๊ธฐ ์ „์šฉ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ์—์„œ๋Š” shallow reactivity๊ฐ€ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํผ์ด๋‚˜ ์ค‘์ฒฉ๋œ ์†์„ฑ์„ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI์—๋Š” deep: true ์˜ต์…˜์„ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

TypeScript ์ปจํ…์ŠคํŠธ ๋ถ„ํ• ๊ณผ ํƒ€์ž… ์•ˆ์ „์„ฑ ๊ฐ•ํ™”

Nuxt 4์—์„œ๋Š” ํ”„๋กœ์ ํŠธ ๋‚ด ๊ฐ ์ปจํ…์ŠคํŠธ์— ๋Œ€ํ•ด ๋ณ„๋„์˜ TypeScript ์„ค์ •์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • .nuxt/tsconfig.app.json โ€” Vue ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์šฉ
  • .nuxt/tsconfig.server.json โ€” Nitro ์„œ๋ฒ„ ์ฝ”๋“œ์šฉ
  • .nuxt/tsconfig.shared.json โ€” ๊ณต์œ  ์œ ํ‹ธ๋ฆฌํ‹ฐ์šฉ
  • .nuxt/tsconfig.node.json โ€” ๋นŒ๋“œ ์‹œ์  ์„ค์ •์šฉ

์ด ๋ถ„๋ฆฌ ๋•๋ถ„์— IDE๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ์„œ๋ฒ„ ์ „์šฉ API๋ฅผ ์ œ์•ˆํ•˜๊ฑฐ๋‚˜ ๊ทธ ๋ฐ˜๋Œ€์˜ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์˜ tsconfig.json์€ ๋„ค ๊ฐ€์ง€ ์„ค์ •์„ ๋ชจ๋‘ ์ฐธ์กฐํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

json
{
  "files": [],
  "references": [
    { "path": "./.nuxt/tsconfig.app.json" },
    { "path": "./.nuxt/tsconfig.server.json" },
    { "path": "./.nuxt/tsconfig.shared.json" },
    { "path": "./.nuxt/tsconfig.node.json" }
  ]
}

CI์—์„œ์˜ ํƒ€์ž… ๊ฒ€์‚ฌ๋„ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. vue-tsc ๋ช…๋ น์–ด๋Š” ํ”„๋กœ์ ํŠธ ์ฐธ์กฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด -b ํ”Œ๋ž˜๊ทธ(๋นŒ๋“œ ๋ชจ๋“œ)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

bash
# ๋ณ€๊ฒฝ ์ „ (Nuxt 3)
nuxt prepare && vue-tsc --noEmit

# ๋ณ€๊ฒฝ ํ›„ (Nuxt 4)
nuxt prepare && vue-tsc -b --noEmit

๋˜ ๋‹ค๋ฅธ TypeScript ๋ณ€๊ฒฝ ์‚ฌํ•ญ์œผ๋กœ, compilerOptions.noUncheckedIndexedAccess๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ true๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ๋ฐฐ์—ด ์š”์†Œ๋‚˜ ๊ฐ์ฒด ์†์„ฑ์— ์ธ๋ฑ์Šค๋กœ ์ ‘๊ทผํ•˜๋ฉด T | undefined๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋˜์–ด, ์ปดํŒŒ์ผ ์‹œ์ ์— ์ž ์žฌ์ ์ธ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„ ์ •๊ทœํ™”์™€ Vue Router v5

Nuxt 4.3์—์„œ๋Š” Vue Router v5๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ๋˜์—ˆ์œผ๋ฉฐ, unplugin-vue-router์— ๋Œ€ํ•œ ์˜์กด์„ฑ์ด ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ด ์—…๊ทธ๋ ˆ์ด๋“œ๋Š” ํˆฌ๋ช…ํ•˜๊ฒŒ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ ๋ช…๋ช… ๊ทœ์น™์ด ํ‘œ์ค€ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. components/dashboard/MetricsCard.vue์— ์œ„์น˜ํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” <KeepAlive>, Vue DevTools, ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ์ปจํ…์ŠคํŠธ์—์„œ ์ผ๊ด€๋˜๊ฒŒ DashboardMetricsCard๋ผ๋Š” ์ด๋ฆ„์„ ๊ฐ–์Šต๋‹ˆ๋‹ค.

vue
<!-- app/pages/dashboard.vue -->
<template>
  <NuxtPage :keepalive="{
    include: ['DashboardMetricsCard', 'DashboardRecentActivity'],
  }" />
</template>

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

Head ๊ด€๋ฆฌ์˜ ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋Œ€์‘

Nuxt 4์—๋Š” Unhead v2๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, useHead์™€ useSeoMeta์—์„œ ์—ฌ๋Ÿฌ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์†์„ฑ์ด ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

app/pages/product/[id].vuetypescript
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)

// Unhead v2: vmid, hid, children, body ์†์„ฑ ์ œ๊ฑฐ
useSeoMeta({
  title: () => product.value?.name ?? 'Product',
  ogTitle: () => product.value?.name ?? 'Product',
  description: () => product.value?.description ?? '',
  ogImage: () => product.value?.imageUrl ?? '',
})
</script>

ํ…œํ”Œ๋ฆฟ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ๋ณ„์นญ ์ •๋ ฌ์— ์˜์กดํ•˜๋Š” ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด๋ฅผ ๋ช…์‹œ์  ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์„ค์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

app/plugins/unhead.tstypescript
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'

export default defineNuxtPlugin({
  setup() {
    const unhead = injectHead()
    unhead.use(TemplateParamsPlugin)
    unhead.use(AliasSortingPlugin)
  },
})
Nuxt 3 ์ง€์› ๊ธฐ๊ฐ„

Nuxt 3์€ 2026๋…„ 7์›” 31์ผ๊นŒ์ง€ ๋ณด์•ˆ ์—…๋ฐ์ดํŠธ์™€ ์ค‘์š”ํ•œ ๋ฒ„๊ทธ ์ˆ˜์ •์„ ๊ณ„์† ๋ฐ›์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋‚ ์งœ ์ดํ›„ Nuxt 3์€ ์ง€์› ๋Œ€์ƒ์—์„œ ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค. EOL ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ํ”„๋กœ๋•์…˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์šด์˜ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ง€๊ธˆ๋ถ€ํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš์„ ์„ธ์šฐ๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ์™€ ์ฃผ์˜ ์‚ฌํ•ญ

์ž๋™ codemod๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ์ˆ˜๋™ ๋Œ€์‘์ด ํ•„์š”ํ•œ ํ•ญ๋ชฉ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

  • window.__NUXT__ ์ œ๊ฑฐ: useNuxtApp().payload๋กœ ๊ต์ฒดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ „์—ญ ๊ฐ์ฒด๋Š” Nuxt 4์—์„œ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ํ›„ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.
  • pages:extend ํ›…: ํŽ˜์ด์ง€ ๋ฉ”ํƒ€ ์Šค์บ” ํ›„์— ์‹คํ–‰๋˜๋Š” ์ƒˆ๋กœ์šด pages:resolved ํ›…์œผ๋กœ ์ „ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • dedupe ๋ถˆ๋ฆฌ์–ธ: refresh({ dedupe: true })๋ฅผ refresh({ dedupe: 'cancel' })๋กœ, false๋ฅผ 'defer'๋กœ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค.
  • ์ธ๋ผ์ธ ์Šคํƒ€์ผ: ๊ธฐ๋ณธ์ ์œผ๋กœ Vue ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋งŒ ์ธ๋ผ์ธํ™”๋˜๋ฉฐ, ๊ธ€๋กœ๋ฒŒ CSS๋Š” ๋ณ„๋„ ํŒŒ์ผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. Nuxt 3 ๋™์ž‘์„ ๋ณต์›ํ•˜๋ ค๋ฉด features: { inlineStyles: true }๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • clearNuxtState: undefined ๋Œ€์‹  ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋ฆฌ์…‹๋ฉ๋‹ˆ๋‹ค. ์ด์ „ ๋™์ž‘์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ clearNuxtState('key', { reset: false })๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ๋•์…˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์‹ค์šฉ์ ์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. npx nuxt upgrade --dedupe๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋นŒ๋“œ๊ฐ€ ์ •์ƒ์ธ์ง€ ํ™•์ธ
  2. codemod ์‹คํ–‰: npx codemod@latest nuxt/4/migration-recipe
  3. ํŒŒ์ผ์„ app/์œผ๋กœ ์ด๋™ (file-structure codemod๋กœ ์ž๋™ํ™”)
  4. ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋กœ์ง์—์„œ null ๊ฒ€์‚ฌ๋ฅผ undefined๋กœ ์—…๋ฐ์ดํŠธ
  5. ์ •๊ทœํ™”๋œ ์ด๋ฆ„์œผ๋กœ <KeepAlive> ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ
  6. CI ํƒ€์ž… ๊ฒ€์‚ฌ๋ฅผ vue-tsc -b --noEmit์œผ๋กœ ์—…๋ฐ์ดํŠธ
  7. ์ „์ฒด ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  noUncheckedIndexedAccess๋กœ ์ธํ•ด ๋ฐœ๊ฒฌ๋œ TypeScript ์˜ค๋ฅ˜ ์ˆ˜์ •

Vue์™€ Nuxt ๊ฐœ๋…์„ ๋” ๊นŠ์ด ์ดํ•ดํ•˜๋ ค๋ฉด SharpSkill์˜ Vue/Nuxt ๋ฉด์ ‘ ์งˆ๋ฌธ์„ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜, Nuxt 4์—๋„ ์ด์–ด์ง€๋Š” ๋ Œ๋”๋ง ์ „๋žต์˜ ๋ฐฐ๊ฒฝ์œผ๋กœ SSR ๋ฐ ์ •์  ์ƒ์„ฑ ๊ฐ€์ด๋“œ๋ฅผ ํ™•์ธํ•ด ๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

  • Nuxt 4.4(2026๋…„ 4์›” ๊ธฐ์ค€ ์ตœ์‹  ๋ฒ„์ „)๋Š” app/ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ, ์‹ฑ๊ธ€ํ†ค ๋ฐ์ดํ„ฐ ํŒจ์นญ, ๋ถ„ํ•  TypeScript ์ปจํ…์ŠคํŠธ๋ฅผ ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์™„๋ฃŒ๋œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ํ™•๋ฆฝํ–ˆ์Šต๋‹ˆ๋‹ค
  • npx codemod@latest nuxt/4/migration-recipe ๋ช…๋ น์–ด๋กœ ๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ตฌ์„ฑ, ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” API ๊ต์ฒด, ๋ฐ์ดํ„ฐ ํŒจ์นญ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ž๋™ํ™”๋ฉ๋‹ˆ๋‹ค
  • shallowRef ๊ธฐ๋ฐ˜์˜ shallow reactivity๋Š” ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์ฝ๊ธฐ ์ค‘์‹ฌ ํŽ˜์ด์ง€์˜ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค
  • ์ปจํ…์ŠคํŠธ๋ณ„ ๊ฐœ๋ณ„ TypeScript ์„ค์ •(app, server, shared, node)์€ ์ปจํ…์ŠคํŠธ ๊ฐ„ ํƒ€์ž… ๋ˆ„์ถœ์„ ์ œ๊ฑฐํ•˜๊ณ  IDE ์ž๋™ ์™„์„ฑ ์ •ํ™•๋„๋ฅผ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค
  • Nuxt 3์€ 2026๋…„ 7์›” 31์ผ์— ์ง€์›์ด ์ข…๋ฃŒ๋˜๋ฏ€๋กœ, ๊ทธ ์ด์ „์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์™„๋ฃŒํ•ด์•ผ ์ง€์›๋˜๋Š” ๋ฒ„์ „์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • Vue Router v5์™€ Unhead v2๋Š” ๋” ์ •๋ˆ๋œ API๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์†์„ฑ์˜ ์ œ๊ฑฐ๋กœ ์ธํ•ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ ์ ๊ฒ€์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

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

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

ํƒœ๊ทธ

#nuxt
#vue
#migration
#typescript
#tutorial

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Vue 3 TypeScript ํƒ€์ž… ์•ˆ์ „ํ•œ props, emits, ์ปดํฌ์ €๋ธ”

2026๋…„ Vue 3์™€ TypeScript: ํƒ€์ž… ์•ˆ์ „ํ•œ Props, Emits, ์ปดํฌ์ €๋ธ”

TypeScript๋กœ ํƒ€์ž… ์•ˆ์ „ํ•œ Vue 3 ์ปดํฌ๋„ŒํŠธ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ: ์ œ๋„ค๋ฆญ defineProps, ํŠœํ”Œ defineEmits, ํƒ€์ž… ์ง€์ • ์ปดํฌ์ €๋ธ”, defineModel, InjectionKey์™€ ๋ฉด์ ‘ ์งˆ๋ฌธ๊นŒ์ง€.

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

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

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

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

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

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