업그레이드 가이드
Nuxt 업그레이드
최신 릴리스
Nuxt를 최신 릴리스로 업그레이드하려면 nuxt upgrade 명령어를 사용하세요.
npx nuxt upgrade
yarn nuxt upgrade
pnpm nuxt upgrade
bun x nuxt upgrade
Nightly 릴리스 채널
최신 Nuxt 빌드를 사용하고 릴리스 전에 기능을 테스트하려면 nightly 릴리스 채널 가이드를 참고하세요.
latest 태그는 현재 Nuxt v4 브랜치를 추적하고 있으므로, 지금은 특히 파괴적인 변경 사항이 있을 가능성이 높습니다 — 주의하세요! 3.x 브랜치 nightly 릴리스를 사용하려면 "nuxt": "npm:nuxt-nightly@3x"로 설정할 수 있습니다.Nuxt 4 테스트하기
Nuxt 4는 2025년 2분기 출시 예정입니다. 현재 compatibilityVersion: 4를 통해 제공되는 모든 기능이 포함될 예정입니다.
출시 전까지는 Nuxt 3.12+ 버전에서 Nuxt 4의 주요 변경 사항을 미리 테스트할 수 있습니다.
Nuxt 4 적용하기
먼저, Nuxt를 최신 릴리스로 업그레이드하세요.
그런 다음 compatibilityVersion을 Nuxt 4 동작에 맞게 설정할 수 있습니다:
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
// _모든_ Nuxt v3 동작을 다시 활성화하려면 다음 옵션을 설정하세요:
// srcDir: '.',
// dir: {
// app: 'app'
// },
// experimental: {
// scanPageMeta: 'after-resolve',
// sharedPrerenderData: false,
// compileTemplate: true,
// resetAsyncDataToUndefined: true,
// templateUtils: true,
// relativeWatchPaths: true,
// normalizeComponentNames: false,
// spaLoadingTemplateLocation: 'within',
// parseErrorData: false,
// pendingWhenIdle: true,
// alwaysRunFetchOnKeyChange: true,
// defaults: {
// useAsyncData: {
// deep: true
// }
// }
// },
// features: {
// inlineStyles: true
// },
// unhead: {
// renderSSRHeadOptions: {
// omitLineBreaks: false
// }
// }
})
compatibilityVersion을 4로 설정하면 Nuxt 설정 전반의 기본값이 Nuxt v4 동작에 맞게 변경되지만, 테스트 시 위에 주석 처리된 라인처럼 Nuxt v3 동작을 세밀하게 다시 활성화할 수 있습니다. 문제가 있다면 이슈를 등록해 주세요. Nuxt 또는 생태계에서 해결할 수 있도록 하겠습니다.
파괴적이거나 중요한 변경 사항은 이곳에 이주 단계와 함께 안내될 예정입니다.
compatibilityVersion: 4로 Nuxt 4를 테스트하는 경우 정기적으로 이곳을 확인하세요.Codemods를 이용한 마이그레이션
업그레이드 과정을 쉽게 하기 위해 Codemod 팀과 협력하여 여러 마이그레이션 단계를 오픈소스 codemod로 자동화했습니다.
npx codemod feedback으로 Codemod 팀에 보고해 주세요 🙏Nuxt 4 codemod 전체 목록, 각 codemod의 상세 정보, 소스, 실행 방법 등은 Codemod Registry에서 확인할 수 있습니다.
이 가이드에서 언급된 모든 codemod는 다음 codemod 레시피로 실행할 수 있습니다:
npx codemod@latest nuxt/4/migration-recipe
yarn dlx codemod@latest nuxt/4/migration-recipe
pnpm dlx codemod@latest nuxt/4/migration-recipe
bun x codemod@latest nuxt/4/migration-recipe
이 명령어는 모든 codemod를 순차적으로 실행하며, 실행을 원하지 않는 codemod는 선택 해제할 수 있습니다. 각 codemod는 아래에 해당 변경 사항과 함께 나열되어 있으며, 개별적으로 실행할 수도 있습니다.
새 디렉터리 구조
🚦 영향 수준: 중요
Nuxt는 이제 새로운 디렉터리 구조를 기본값으로 사용하며, 하위 호환성을 제공합니다(예: Nuxt가 상위 pages/ 디렉터리 등 기존 구조를 감지하면 이 새로운 구조가 적용되지 않음).
무엇이 변경되었나요
- 새로운 Nuxt 기본
srcDir는 기본적으로app/이며, 대부분의 항목이 이곳에서 해석됩니다. serverDir는 이제 기본값이<rootDir>/server이며,<srcDir>/server가 아닙니다.layers/,modules/,public/는 기본적으로<rootDir>기준으로 해석됩니다.- Nuxt Content v2.13+를 사용하는 경우,
content/도<rootDir>기준으로 해석됩니다. - 새로운
dir.app이 추가되어,router.options.ts와spa-loading-template.html을 찾는 디렉터리로 사용됩니다. 기본값은<srcDir>/입니다.
v4 폴더 구조 예시입니다.
.output/
.nuxt/
app/
assets/
components/
composables/
layouts/
middleware/
pages/
plugins/
utils/
app.config.ts
app.vue
router.options.ts
content/
layers/
modules/
node_modules/
public/
server/
api/
middleware/
plugins/
routes/
utils/
nuxt.config.ts
👉 자세한 내용은 이 변경을 구현한 PR을 참고하세요.
변경 이유
- 성능 - 모든 코드를 저장소 루트에 두면
.git/및node_modules/폴더가 FS 감시자에 의해 스캔/포함되어 Mac OS가 아닌 환경에서 시작이 크게 지연될 수 있습니다. - IDE 타입 안전성 -
server/와 앱의 나머지 부분은 완전히 다른 컨텍스트에서 실행되며, 전역 import도 다릅니다.server/가 앱의 나머지와 같은 폴더 안에 있지 않도록 하는 것이 IDE에서 자동 완성 기능을 제대로 받는 첫걸음입니다.
마이그레이션 단계
app/이라는 새 디렉터리를 만드세요.assets/,components/,composables/,layouts/,middleware/,pages/,plugins/,utils/폴더와app.vue,error.vue,app.config.ts를 그 아래로 옮기세요.app/router-options.ts나app/spa-loading-template.html이 있다면 경로는 그대로 유지됩니다.nuxt.config.ts,content/,layers/,modules/,public/,server/폴더는app/폴더 밖, 프로젝트 루트에 남겨두세요.tailwindcss나eslint설정 등 서드파티 설정 파일도 새 디렉터리 구조에 맞게 업데이트해야 할 수 있습니다(필요한 경우 -@nuxtjs/tailwindcss는 자동으로tailwindcss를 올바르게 설정합니다).
npx codemod@latest nuxt/4/file-structure를 실행하여 이 마이그레이션을 자동화할 수 있습니다.하지만 마이그레이션은 _필수_가 아닙니다. 현재 폴더 구조를 유지하고 싶다면 Nuxt가 자동으로 감지합니다. (감지하지 못하면 이슈를 등록해 주세요.) 단, 이미 커스텀 srcDir를 사용 중이라면 modules/, public/, server/ 폴더가 커스텀 srcDir이 아닌 rootDir 기준으로 해석된다는 점을 유의하세요. 필요하다면 dir.modules, dir.public, serverDir로 오버라이드할 수 있습니다.
다음 설정으로 v3 폴더 구조를 강제로 사용할 수도 있습니다:
export default defineNuxtConfig({
// 새 srcDir 기본값을 `app`에서 루트 디렉터리로 되돌립니다
srcDir: '.',
// `app/router.options.ts`와 `app/spa-loading-template.html`의 디렉터리 접두사를 지정합니다
dir: {
app: 'app'
}
})
싱글턴 데이터 패칭 레이어
🚦 영향 수준: 중간
무엇이 변경되었나요
Nuxt의 데이터 패칭 시스템(useAsyncData 및 useFetch)이 성능과 일관성을 위해 크게 재구성되었습니다:
- 동일한 키에 대한 공유 ref: 동일한 키로
useAsyncData또는useFetch를 호출하면 이제 동일한data,error,statusref를 공유합니다. 즉, 명시적 키를 사용하는 모든 호출에서deep,transform,pick,getCachedData,default옵션이 충돌하지 않도록 주의해야 합니다. getCachedData에 대한 더 많은 제어: 이제 데이터가 패칭될 때마다(감시자에 의해 또는refreshNuxtData호출로 인해)getCachedData함수가 호출됩니다. (이전에는 항상 새 데이터를 패칭했고 이 함수는 호출되지 않았습니다.) 캐시 데이터를 사용할지, 다시 패칭할지 더 세밀하게 제어할 수 있도록 함수에 요청 원인을 담은 컨텍스트 객체가 전달됩니다.- 반응형 키 지원: 이제 계산 ref, 일반 ref, getter 함수를 키로 사용할 수 있어 자동 데이터 재패칭(및 데이터 별도 저장)이 가능합니다.
- 데이터 정리:
useAsyncData로 패칭한 데이터를 사용하는 마지막 컴포넌트가 언마운트되면 Nuxt가 해당 데이터를 제거하여 메모리 사용이 계속 증가하는 것을 방지합니다.
변경 이유
이 변경은 메모리 사용을 개선하고, useAsyncData 호출 간 로딩 상태의 일관성을 높이기 위해 도입되었습니다.
마이그레이션 단계
- 옵션 불일치 확인: 동일한 키로 다른 옵션이나 패칭 함수를 사용하는 컴포넌트를 점검하세요.
// 이제 경고가 발생합니다 const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false }) const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
명시적 키를 공유하면서 커스텀 옵션을 사용하는useAsyncData호출은 별도의 composable로 추출하는 것이 좋습니다:composables/useUserData.tsexport function useUserData(userId: string) { return useAsyncData( `user-${userId}`, () => fetchUser(userId), { deep: true, transform: (user) => ({ ...user, lastAccessed: new Date() }) } ) } getCachedData구현 업데이트:useAsyncData('key', fetchFunction, { - getCachedData: (key, nuxtApp) => { - return cachedData[key] - } + getCachedData: (key, nuxtApp, ctx) => { + // ctx.cause - 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch' 중 하나입니다 + + // 예시: 수동 새로고침 시에는 캐시를 사용하지 않음 + if (ctx.cause === 'refresh:manual') return undefined + + return cachedData[key] + } })
또는, 당분간 이 동작을 비활성화할 수 있습니다:
export default defineNuxtConfig({
experimental: {
granularCachedData: false,
purgeCachedData: false
}
})
라우트 메타데이터 중복 제거
🚦 영향 수준: 최소
무엇이 변경되었나요
definePageMeta를 사용해 라우트 메타데이터(예: name, path 등)를 설정할 수 있습니다. 이전에는 이 값들이 라우트와 라우트 메타데이터 모두에서 접근 가능했습니다(예: route.name, route.meta.name).
이제는 라우트 객체에서만 접근할 수 있습니다.
변경 이유
이는 experimental.scanPageMeta를 기본값으로 활성화한 결과이며, 성능 최적화 목적입니다.
마이그레이션 단계
마이그레이션은 간단합니다:
const route = useRoute()
- console.log(route.meta.name)
+ console.log(route.name)
컴포넌트 이름 정규화
🚦 영향 수준: 중간
Vue는 이제 Nuxt의 컴포넌트 네이밍 패턴과 일치하는 컴포넌트 이름을 생성합니다.
무엇이 변경되었나요
기본적으로 수동으로 설정하지 않았다면, Vue는 컴포넌트의 파일명을 컴포넌트 이름으로 할당합니다.
├─ components/
├─── SomeFolder/
├───── MyComponent.vue
이 경우, Vue 입장에서는 컴포넌트 이름이 MyComponent입니다. <KeepAlive>에서 사용하거나 Vue DevTools에서 식별하려면 이 이름을 사용해야 합니다.
하지만 자동 import를 하려면 SomeFolderMyComponent를 사용해야 했습니다.
이 변경으로 두 값이 일치하게 되며, Vue가 Nuxt의 컴포넌트 네이밍 패턴과 일치하는 컴포넌트 이름을 생성합니다.
마이그레이션 단계
@vue/test-utils의 findComponent를 사용하는 테스트나, 컴포넌트 이름에 의존하는 <KeepAlive>에서 업데이트된 이름을 사용하세요.
또는, 당분간 이 동작을 비활성화할 수 있습니다:
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: false
}
})
Unhead v2
🚦 영향 수준: 최소
무엇이 변경되었나요
Unhead는 <head> 태그 생성을 담당하며, 버전 2로 업데이트되었습니다.
대부분 호환되지만, 저수준 API에 몇 가지 파괴적 변경이 포함되어 있습니다.
- 제거된 props:
vmid,hid,children,body. - Promise 입력 지원 중단.
- 태그가 기본적으로 Capo.js로 정렬됩니다.
마이그레이션 단계
위 변경 사항은 앱에 미치는 영향이 거의 없습니다.
문제가 있다면 다음을 확인하세요:
- 제거된 props를 사용하지 않는지 확인하세요.
useHead({
meta: [{
name: 'description',
// meta 태그에는 vmid나 key가 필요하지 않습니다
- vmid: 'description'
- hid: 'description'
}]
})
- Template Params나 Alias Tag Sorting을 사용 중이라면, 이제 명시적으로 해당 기능을 활성화해야 합니다.
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'
export default defineNuxtPlugin({
setup() {
const unhead = injectHead()
unhead.use(TemplateParamsPlugin)
unhead.use(AliasSortingPlugin)
}
})
필수는 아니지만, @unhead/vue에서 #imports 또는 nuxt/app으로 import를 업데이트하는 것이 권장됩니다.
-import { useHead } from '@unhead/vue'
+import { useHead } from '#imports'
문제가 계속된다면 head.legacy 설정을 활성화하여 v1 동작으로 되돌릴 수 있습니다.
export default defineNuxtConfig({
unhead: {
legacy: true,
}
})
SPA 로딩 화면의 새로운 DOM 위치
🚦 영향 수준: 최소
무엇이 변경되었나요
클라이언트 전용 페이지(ssr: false)를 렌더링할 때, Nuxt 앱 루트 내에 로딩 화면(app/spa-loading-template.html)을 선택적으로 렌더링합니다:
<div id="__nuxt">
<!-- spa loading template -->
</div>
이제 기본적으로 Nuxt 앱 루트 옆에 템플릿을 렌더링합니다:
<div id="__nuxt"></div>
<!-- spa loading template -->
변경 이유
SPA 로딩 템플릿이 Vue 앱 suspense가 해제될 때까지 DOM에 남아 있어 흰 화면이 깜빡이는 현상을 방지합니다.
마이그레이션 단계
CSS나 document.queryElement로 spa 로딩 템플릿을 타겟팅했다면 셀렉터를 업데이트해야 합니다. 이 목적을 위해 새로운 app.spaLoaderTag 및 app.spaLoaderAttrs 설정 옵션을 사용할 수 있습니다.
또는, 이전 동작으로 되돌릴 수 있습니다:
export default defineNuxtConfig({
experimental: {
spaLoadingTemplateLocation: 'within',
}
})
파싱된 error.data
🚦 영향 수준: 최소
data 속성이 있는 에러를 throw할 수 있었지만, 이전에는 파싱되지 않았습니다. 이제 파싱되어 error 객체에서 사용할 수 있습니다. 이는 버그 수정이지만, 이전 동작에 의존해 수동으로 파싱했다면 파괴적 변경입니다.
마이그레이션 단계
커스텀 error.vue에서 error.data의 추가 파싱을 제거하세요:
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
- const data = JSON.parse(error.data)
+ const data = error.data
</script>
또는, 이 변경을 비활성화할 수 있습니다:
export default defineNuxtConfig({
experimental: {
parseErrorData: false
},
})
더 세밀한 인라인 스타일
🚦 영향 수준: 중간
Nuxt는 이제 Vue 컴포넌트에 대해서만 스타일을 인라인하며, 글로벌 CSS는 인라인하지 않습니다.
무엇이 변경되었나요
이전에는 Nuxt가 모든 CSS(글로벌 스타일 포함)를 인라인하고, 별도의 CSS 파일에 대한 <link> 요소를 제거했습니다. 이제 Nuxt는 Vue 컴포넌트에 대해서만 이 작업을 수행합니다(이전에는 별도의 CSS 청크를 생성). 이는 별도의 네트워크 요청을 줄이면서(초기 로드 시 페이지별/컴포넌트별 .css 파일에 대한 별도 요청이 없음), 단일 글로벌 CSS 파일의 캐싱을 허용하고, 초기 요청의 문서 다운로드 크기를 줄이는 더 나은 균형이라고 생각합니다.
마이그레이션 단계
이 기능은 완전히 설정 가능하며, inlineStyles: true로 글로벌 CSS와 컴포넌트별 CSS 모두 인라인하도록 이전 동작으로 되돌릴 수 있습니다.
export default defineNuxtConfig({
features: {
inlineStyles: true
}
})
페이지 메타 스캔 시점 변경
🚦 영향 수준: 최소
무엇이 변경되었나요
이제 definePageMeta에 정의된 페이지 메타데이터를 pages:extend 훅 호출 _이후_에 스캔합니다.
변경 이유
사용자가 pages:extend에서 추가하고자 하는 페이지의 메타데이터도 스캔할 수 있도록 하기 위함입니다. 이제 새로운 pages:resolved 훅에서 페이지 메타데이터를 변경하거나 오버라이드할 수 있습니다.
마이그레이션 단계
페이지 메타데이터를 오버라이드하려면 pages:extend가 아니라 pages:resolved에서 처리하세요.
export default defineNuxtConfig({
hooks: {
- 'pages:extend'(pages) {
+ 'pages:resolved'(pages) {
const myPage = pages.find(page => page.path === '/')
myPage.meta ||= {}
myPage.meta.layout = 'overridden-layout'
}
}
})
또는, 이전 동작으로 되돌릴 수 있습니다:
export default defineNuxtConfig({
experimental: {
scanPageMeta: true
}
})
공유 프리렌더 데이터
🚦 영향 수준: 중간
무엇이 변경되었나요
이전에는 실험적이었던 기능을 활성화하여, useAsyncData 및 useFetch 호출의 데이터를 서로 다른 페이지 간에 공유합니다. 원본 PR 참고.
변경 이유
이 기능은 프리렌더링된 페이지 간에 payload _data_를 자동으로 공유합니다. 여러 페이지에서 동일한 데이터를 패칭하는 경우, 첫 번째 페이지에서만 데이터를 패칭하고 이후에는 캐시를 사용하므로 프리렌더링 성능이 크게 향상될 수 있습니다.
예를 들어, 모든 페이지에서 메뉴용 네비게이션 데이터나 CMS에서 사이트 설정을 가져오는 useFetch 호출이 필요한 경우, 해당 데이터는 처음 한 번만 패칭되고 이후에는 캐시로 사용됩니다.
마이그레이션 단계
데이터의 고유 키가 항상 동일한 데이터를 가리키도록 해야 합니다. 예를 들어, 특정 페이지와 관련된 데이터를 패칭할 때는 해당 데이터를 고유하게 식별할 수 있는 키를 제공해야 합니다. (useFetch는 자동으로 처리합니다.)
// 동적 페이지(예: `[slug].vue`)에서 route slug에 따라 패칭 데이터가 달라지므로,
// 키에 반영하지 않으면 안전하지 않습니다.
const route = useRoute()
const { data } = await useAsyncData(async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
// 대신, 패칭 데이터에 고유하게 매칭되는 키를 사용해야 합니다.
const { data } = await useAsyncData(route.params.slug, async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
또는, 이 기능을 비활성화할 수 있습니다:
export default defineNuxtConfig({
experimental: {
sharedPrerenderData: false
}
})
useAsyncData 및 useFetch의 기본 data 및 error 값
🚦 영향 수준: 최소
무엇이 변경되었나요
useAsyncData에서 반환되는 data 및 error 객체의 기본값이 이제 undefined입니다.
변경 이유
이전에는 data가 null로 초기화되었지만, clearNuxtData에서는 undefined로 리셋되었습니다. error는 null로 초기화되었습니다. 이 변경은 일관성을 높이기 위함입니다.
마이그레이션 단계
data.value나 error.value가 null인지 확인했다면, 이제 undefined를 확인하도록 업데이트하세요.
npx codemod@latest nuxt/4/default-data-error-value를 실행하여 이 단계를 자동화할 수 있습니다.문제가 있다면 이전 동작으로 되돌릴 수 있습니다:
export default defineNuxtConfig({
experimental: {
defaults: {
useAsyncData: {
value: 'null',
errorValue: 'null'
}
}
}
})
이렇게 하는 경우 이슈를 등록해 주세요. 이 설정은 계속 유지할 계획이 없습니다.
useAsyncData 및 useFetch에서 refresh 호출 시 dedupe 옵션의 deprecated boolean 값 제거
🚦 영향 수준: 최소
무엇이 변경되었나요
이전에는 refresh에 dedupe: boolean을 전달할 수 있었습니다. 이는 각각 cancel(true)과 defer(false)의 별칭이었습니다.
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt!' }))
async function refreshData () {
await refresh({ dedupe: true })
}
변경 이유
더 명확하게 하기 위해 이 별칭이 제거되었습니다.
useAsyncData에 dedupe 옵션을 추가하면서, boolean 값이 반대 의미가 되어 혼란을 야기했습니다.
refresh({ dedupe: false })는 기존 요청을 취소하지 않고 새 요청을 실행하는 의미였고, useAsyncData 옵션의 dedupe: true는 기존 대기 중인 요청이 있으면 새 요청을 만들지 않음을 의미했습니다. (PR 참고.)
마이그레이션 단계
마이그레이션은 간단합니다:
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
async function refreshData () {
- await refresh({ dedupe: true })
+ await refresh({ dedupe: 'cancel' })
- await refresh({ dedupe: false })
+ await refresh({ dedupe: 'defer' })
}
npx codemod@latest nuxt/4/deprecated-dedupe-value를 실행하여 이 단계를 자동화할 수 있습니다.useAsyncData 및 useFetch에서 data를 clear할 때 기본값 존중
🚦 영향 수준: 최소
무엇이 변경되었나요
useAsyncData에 커스텀 default 값을 제공하면, 이제 clear 또는 clearNuxtData 호출 시 해당 기본값으로 리셋됩니다(단순히 해제되지 않음).
변경 이유
사용자는 종종 빈 배열 등 적절한 빈 값을 설정하여 반복 시 null/undefined 체크를 피하고자 합니다. 이 값이 리셋/clear 시에도 존중되어야 합니다.
마이그레이션 단계
문제가 있다면, 당분간 이전 동작으로 되돌릴 수 있습니다:
export default defineNuxtConfig({
experimental: {
resetAsyncDataToUndefined: true,
}
})
이렇게 하는 경우 이슈를 등록해 주세요. 이 설정은 계속 유지할 계획이 없습니다.
useAsyncData 및 useFetch의 pending 값 정렬
🚦 영향 수준: 중간
useAsyncData, useFetch, useLazyAsyncData, useLazyFetch에서 반환되는 pending 객체는 이제 status가 pending일 때만 true인 계산 속성입니다.
무엇이 변경되었나요
이제 immediate: false를 전달하면, 첫 요청이 실행될 때까지 pending은 false입니다. 이전에는 첫 요청 전까지 항상 pending이 true였습니다.
변경 이유
pending의 의미를 status 속성과 일치시키기 위함입니다. status도 요청이 진행 중일 때만 pending입니다.
마이그레이션 단계
pending 속성에 의존한다면, 이제 pending이 status가 pending일 때만 true임을 고려해 로직을 수정하세요.
<template>
- <div v-if="!pending">
+ <div v-if="status === 'success'">
<p>Data: {{ data }}</p>
</div>
<div v-else>
<p>Loading...</p>
</div>
</template>
<script setup lang="ts">
const { data, pending, execute, status } = await useAsyncData(() => fetch('/api/data'), {
immediate: false
})
onMounted(() => execute())
</script>
또는, 이전 동작으로 임시 복귀할 수 있습니다:
export default defineNuxtConfig({
experimental: {
pendingWhenIdle: true
}
})
useAsyncData 및 useFetch의 키 변경 동작
🚦 영향 수준: 중간
무엇이 변경되었나요
useAsyncData나 useFetch에서 반응형 키를 사용할 때, 키가 변경되면 Nuxt가 자동으로 데이터를 재패칭합니다. immediate: false가 설정된 경우, 데이터가 한 번이라도 패칭된 후에만 키 변경 시 데이터를 패칭합니다.
이전에는 useFetch가 약간 다르게 동작하여, 키가 변경될 때마다 항상 데이터를 패칭했습니다.
이제 useFetch와 useAsyncData가 일관되게 동작합니다 - 데이터가 한 번이라도 패칭된 후에만 키 변경 시 데이터를 패칭합니다.
변경 이유
useAsyncData와 useFetch의 동작을 일관되게 하여, 예기치 않은 패칭을 방지합니다. immediate: false를 설정했다면, 반드시 refresh나 execute를 호출해야 useFetch나 useAsyncData에서 데이터가 패칭됩니다.
마이그레이션 단계
이 변경은 일반적으로 기대 동작을 개선하지만, non-immediate useFetch에서 키나 옵션 변경 시 자동 패칭을 기대했다면, 이제는 첫 실행을 수동으로 트리거해야 합니다.
const id = ref('123')
const { data, execute } = await useFetch('/api/test', {
query: { id },
immediate: false
)
+ watch(id, execute, { once: true })
이 동작을 비활성화하려면:
// 또는 Nuxt 설정에서 전역적으로
export default defineNuxtConfig({
experimental: {
alwaysRunFetchOnKeyChange: true
}
})
useAsyncData 및 useFetch의 얕은 데이터 반응성
🚦 영향 수준: 최소
useAsyncData, useFetch, useLazyAsyncData, useLazyFetch에서 반환되는 data 객체는 이제 ref가 아닌 shallowRef입니다.
무엇이 변경되었나요
새 데이터를 패칭하면, data에 의존하는 모든 항목이 여전히 반응성을 가집니다(전체 객체가 교체됨). 하지만 데이터 구조 내의 속성만 변경하면 앱에서 반응성이 트리거되지 않습니다.
변경 이유
깊게 중첩된 객체와 배열에 대해 Vue가 모든 속성/배열을 감시하지 않아도 되므로 성능이 크게 향상됩니다. 대부분의 경우, data는 불변이어야 합니다.
마이그레이션 단계
대부분의 경우 별도 마이그레이션이 필요 없지만, 데이터 객체의 반응성에 의존한다면 두 가지 방법이 있습니다:
- 컴포저블 단위로 깊은 반응성을 선택적으로 활성화할 수 있습니다:
- const { data } = useFetch('/api/test') + const { data } = useFetch('/api/test', { deep: true }) - 프로젝트 전체에서 기본 동작을 변경할 수 있습니다(권장하지 않음):
nuxt.config.ts
export default defineNuxtConfig({ experimental: { defaults: { useAsyncData: { deep: true } } } })
npx codemod@latest nuxt/4/shallow-function-reactivity를 실행하여 이 단계를 자동화할 수 있습니다.builder:watch의 절대 경로 감시
🚦 영향 수준: 최소
무엇이 변경되었나요
Nuxt의 builder:watch 훅이 이제 프로젝트 srcDir 기준 상대 경로가 아닌 절대 경로를 내보냅니다.
변경 이유
srcDir 외부의 경로 감시를 지원하고, 레이어 등 더 복잡한 패턴을 더 잘 지원하기 위함입니다.
마이그레이션 단계
이 훅을 사용하는 공개 Nuxt 모듈은 이미 사전 마이그레이션되었습니다. 이슈 #25339 참고.
하지만, builder:watch 훅을 사용하는 모듈 작성자라면, Nuxt v3/v4 모두에서 동일하게 동작하도록 다음 코드를 사용할 수 있습니다:
+ import { relative, resolve } from 'node:fs'
// ...
nuxt.hook('builder:watch', async (event, path) => {
+ path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
// ...
})
npx codemod@latest nuxt/4/absolute-watch-path를 실행하여 이 단계를 자동화할 수 있습니다.window.__NUXT__ 객체 제거
무엇이 변경되었나요
앱이 hydration을 마치면 전역 window.__NUXT__ 객체를 제거합니다.
변경 이유
이는 멀티 앱 패턴(#21635)을 지원하고, Nuxt 앱 데이터를 접근하는 단일 방법인 useNuxtApp()에 집중하기 위함입니다.
마이그레이션 단계
데이터는 여전히 사용할 수 있으며, useNuxtApp().payload로 접근할 수 있습니다:
- console.log(window.__NUXT__)
+ console.log(useNuxtApp().payload)
디렉터리 인덱스 스캐닝
🚦 영향 수준: 중간
무엇이 변경되었나요
middleware/ 폴더의 하위 폴더도 index 파일을 스캔하며, 이제 이들도 프로젝트의 미들웨어로 등록됩니다.
변경 이유
Nuxt는 middleware/, plugins/ 등 여러 폴더를 자동으로 스캔합니다.
plugins/의 하위 폴더도 index 파일을 스캔하므로, 스캔되는 디렉터리 간 동작을 일관되게 만들고자 했습니다.
마이그레이션 단계
대부분 별도 마이그레이션이 필요 없지만, 이전 동작으로 되돌리고 싶다면 다음 훅을 추가해 해당 미들웨어를 필터링할 수 있습니다:
export default defineNuxtConfig({
hooks: {
'app:resolve'(app) {
app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
}
}
})
템플릿 컴파일 변경
🚦 영향 수준: 최소
무엇이 변경되었나요
이전에는 Nuxt가 파일 시스템에 있는 .ejs 파일 포맷/문법의 템플릿을 컴파일할 때 lodash/template를 사용했습니다.
또한, 코드 생성에 사용할 수 있는 일부 템플릿 유틸리티(serialize, importName, importSources)를 제공했으나, 이제 제거됩니다.
변경 이유
Nuxt v3에서는 getContents() 함수가 있는 '가상' 문법으로 전환하여 훨씬 더 유연하고 성능이 좋아졌습니다.
또한, lodash/template는 보안 이슈가 연이어 발생했습니다. Nuxt 프로젝트에서는 빌드 타임에 신뢰할 수 있는 코드로만 사용되므로 큰 문제는 아니지만, 보안 감사에 계속 나타납니다. 게다가 lodash는 무거운 의존성이며 대부분의 프로젝트에서 사용되지 않습니다.
마지막으로, Nuxt에서 직접 코드 직렬화 함수를 제공하는 것은 이상적이지 않습니다. 대신 unjs/knitwork와 같은 프로젝트를 유지 관리하며, 보안 이슈가 발생하면 Nuxt 업그레이드 없이도 직접 해결할 수 있습니다.
마이그레이션 단계
EJS 문법을 사용하는 모듈은 PR을 통해 업데이트했지만, 직접 해야 한다면 다음 세 가지 방법 중 하나를 사용할 수 있습니다:
- 문자열 치환 로직을 직접
getContents()로 이동 - https://github.com/nuxt-modules/color-mode/pull/240와 같이 커스텀 함수를 사용해 치환 처리
- Nuxt가 아닌 본인 프로젝트의 의존성으로
es-toolkit/compat(lodash template의 대체품) 사용:
+ import { readFileSync } from 'node:fs'
+ import { template } from 'es-toolkit/compat'
// ...
addTemplate({
fileName: 'appinsights-vue.js'
options: { /* some options */ },
- src: resolver.resolve('./runtime/plugin.ejs'),
+ getContents({ options }) {
+ const contents = readFileSync(resolver.resolve('./runtime/plugin.ejs'), 'utf-8')
+ return template(contents)({ options })
+ },
})
마지막으로, 템플릿 유틸리티(serialize, importName, importSources)를 사용 중이라면, 다음과 같이 knitwork의 유틸리티로 대체할 수 있습니다:
import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"(?=,?$)/gm, r => JSON.parse(r).replace(/^{(.*)}$/, '$1'))
const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => {
if (lazy) {
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
}
return genImport(src, genSafeVariableName(src))
}).join('\n')
}
const importName = genSafeVariableName
npx codemod@latest nuxt/4/template-compilation-changes를 실행하여 이 단계를 자동화할 수 있습니다.실험적 기능 제거
🚦 영향 수준: 최소
무엇이 변경되었나요
Nuxt 4에서는 네 가지 실험적 기능이 더 이상 설정할 수 없습니다:
experimental.treeshakeClientOnly는true입니다(v3.0부터 기본값)experimental.configSchema는true입니다(v3.3부터 기본값)experimental.polyfillVueUseHead는false입니다(v3.4부터 기본값)experimental.respectNoSSRHeader는false입니다(v3.4부터 기본값)vite.devBundler는 더 이상 설정할 수 없으며, 기본적으로vite-node를 사용합니다
변경 이유
이 옵션들은 오랫동안 현재 값으로 설정되어 있었으며, 계속 설정 가능할 필요가 없다고 판단했습니다.
마이그레이션 단계
Nuxt 2 vs. Nuxt 3+
아래 표는 Nuxt의 3가지 버전을 간단히 비교한 것입니다:
| 기능 / 버전 | Nuxt 2 | Nuxt Bridge | Nuxt 3+ |
|---|---|---|---|
| Vue | 2 | 2 | 3 |
| 안정성 | 😊 안정적 | 😊 안정적 | 😊 안정적 |
| 성능 | 🏎 빠름 | ✈️ 더 빠름 | 🚀 가장 빠름 |
| Nitro 엔진 | ❌ | ✅ | ✅ |
| ESM 지원 | 🌙 부분적 | 👍 더 나음 | ✅ |
| TypeScript | ☑️ 선택적 | 🚧 부분적 | ✅ |
| Composition API | ❌ | 🚧 부분적 | ✅ |
| Options API | ✅ | ✅ | ✅ |
| 컴포넌트 자동 import | ✅ | ✅ | ✅ |
<script setup> 문법 | ❌ | 🚧 부분적 | ✅ |
| 자동 import | ❌ | ✅ | ✅ |
| webpack | 4 | 4 | 5 |
| Vite | ⚠️ 부분적 | 🚧 부분적 | ✅ |
| Nuxt CLI | ❌ 구버전 | ✅ nuxt | ✅ nuxt |
| 정적 사이트 | ✅ | ✅ | ✅ |
Nuxt 2에서 Nuxt 3+로
마이그레이션 가이드는 Nuxt 2의 기능과 Nuxt 3+의 기능을 단계별로 비교하고, 현재 애플리케이션을 적응시키는 방법을 안내합니다.
Nuxt 2에서 Nuxt Bridge로
Nuxt 2 애플리케이션을 점진적으로 Nuxt 3로 마이그레이션하고 싶다면 Nuxt Bridge를 사용할 수 있습니다. Nuxt Bridge는 Nuxt 2에서 Nuxt 3+ 기능을 opt-in 방식으로 사용할 수 있게 해주는 호환성 레이어입니다.