components
Nuxt는 이 디렉토리(및 사용 중인 모듈에서 등록된 컴포넌트)의 모든 컴포넌트를 자동으로 가져옵니다.
-| components/
---| AppHeader.vue
---| AppFooter.vue
<template>
<div>
<AppHeader />
<NuxtPage />
<AppFooter />
</div>
</template>
컴포넌트 이름
다음과 같이 중첩된 디렉토리에 컴포넌트가 있는 경우:
-| components/
---| base/
-----| foo/
-------| Button.vue
... 그러면 컴포넌트의 이름은 자신의 경로 디렉토리와 파일명을 기반으로 하며, 중복된 세그먼트는 제거됩니다. 따라서 컴포넌트의 이름은 다음과 같습니다:
<BaseFooButton />
Button.vue를 BaseFooButton.vue로 이름을 바꿀 수 있습니다.컴포넌트를 경로가 아닌 이름만으로 자동 임포트하고 싶다면, 구성 객체의 확장형을 사용하여 pathPrefix 옵션을 false로 설정해야 합니다:
export default defineNuxtConfig({
components: [
{
path: '~/components',
pathPrefix: false, },
],
});
이렇게 하면 Nuxt 2에서 사용하던 것과 동일한 전략으로 컴포넌트가 등록됩니다. 예를 들어, ~/components/Some/MyComponent.vue는 <MyComponent>로 사용할 수 있고, <SomeMyComponent>로는 사용할 수 없습니다.
동적 컴포넌트
Vue의 <component :is="someComputedComponent"> 문법을 사용하려면, Vue에서 제공하는 resolveComponent 헬퍼를 사용하거나 #components에서 컴포넌트를 직접 임포트하여 is prop에 전달해야 합니다.
예시:
<script setup lang="ts">
import { SomeComponent } from '#components'
const MyButton = resolveComponent('MyButton')
</script>
<template>
<component :is="clickable ? MyButton : 'div'" />
<component :is="SomeComponent" />
</template>
resolveComponent를 사용하는 경우, 반드시 컴포넌트의 이름만을 삽입해야 하며, 반드시 리터럴 문자열이어야 하고 변수이거나 변수를 포함해서는 안 됩니다. 이 문자열은 컴파일 단계에서 정적으로 분석됩니다.또한 권장되지는 않지만, 모든 컴포넌트를 전역으로 등록할 수도 있습니다. 이렇게 하면 모든 컴포넌트에 대해 비동기 청크가 생성되어 애플리케이션 전체에서 사용할 수 있게 됩니다.
export default defineNuxtConfig({
components: {
+ global: true,
+ dirs: ['~/components']
},
})
또한 ~/components/global 디렉토리에 컴포넌트를 배치하거나 파일명에 .global.vue 접미사를 사용하여 일부 컴포넌트만 선택적으로 전역 등록할 수도 있습니다. 위에서 언급했듯이, 각 전역 컴포넌트는 별도의 청크로 렌더링되므로 이 기능을 과도하게 사용하지 않도록 주의하세요.
global 옵션은 컴포넌트 디렉토리별로도 설정할 수 있습니다.동적 임포트
컴포넌트를 동적으로 임포트(지연 로딩이라고도 함)하려면, 컴포넌트 이름 앞에 Lazy 접두사를 붙이기만 하면 됩니다. 이 방법은 컴포넌트가 항상 필요한 것이 아닐 때 특히 유용합니다.
Lazy 접두사를 사용하면 컴포넌트 코드를 적절한 시점까지 로딩을 지연시킬 수 있어, JavaScript 번들 크기 최적화에 도움이 됩니다.
<script setup lang="ts">
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">Show List</button>
</div>
</template>
지연(또는 Lazy) 하이드레이션
Lazy 컴포넌트는 앱의 청크 크기를 제어하는 데 유용하지만, 조건부 렌더링되지 않는 한 런타임 성능을 항상 향상시키는 것은 아닙니다. 실제 애플리케이션에서는 일부 페이지에 많은 콘텐츠와 컴포넌트가 포함될 수 있으며, 대부분의 경우 모든 컴포넌트가 페이지가 로드되자마자 상호작용 가능할 필요는 없습니다. 모든 컴포넌트를 즉시 로드하면 성능에 부정적인 영향을 줄 수 있습니다.
앱을 최적화하기 위해, 일부 컴포넌트의 하이드레이션을 해당 컴포넌트가 보이거나 브라우저가 더 중요한 작업을 마칠 때까지 지연시키고 싶을 수 있습니다.
Nuxt는 lazy(또는 지연) 하이드레이션을 지원하여 컴포넌트가 언제 상호작용 가능해질지 제어할 수 있습니다.
하이드레이션 전략
Nuxt는 다양한 내장 하이드레이션 전략을 제공합니다. lazy 컴포넌트마다 한 가지 전략만 사용할 수 있습니다.
v-bind로 prop 객체를 펼치는 방식은 지원하지 않음). 또한 #components에서 직접 임포트하는 경우에는 동작하지 않습니다.hydrate-on-visible
컴포넌트가 뷰포트에 보일 때 하이드레이션합니다.
<template>
<div>
<LazyMyComponent hydrate-on-visible />
</div>
</template>
hydrateOnVisible 전략을 사용합니다.hydrate-on-idle
브라우저가 유휴 상태일 때 컴포넌트를 하이드레이션합니다. 컴포넌트를 가능한 빨리 로드해야 하지만, 중요한 렌더링 경로를 차단하지 않아야 할 때 적합합니다.
최대 타임아웃 역할을 하는 숫자를 전달할 수도 있습니다.
<template>
<div>
<LazyMyComponent hydrate-on-idle />
</div>
</template>
hydrateOnIdle 전략을 사용합니다.hydrate-on-interaction
지정된 상호작용(예: 클릭, 마우스오버) 후에 컴포넌트를 하이드레이션합니다.
<template>
<div>
<LazyMyComponent hydrate-on-interaction="mouseover" />
</div>
</template>
이벤트나 이벤트 목록을 전달하지 않으면 기본적으로 pointerenter와 focus에서 하이드레이션됩니다.
hydrateOnInteraction 전략을 사용합니다.hydrate-on-media-query
윈도우가 미디어 쿼리와 일치할 때 컴포넌트를 하이드레이션합니다.
<template>
<div>
<LazyMyComponent hydrate-on-media-query="(max-width: 768px)" />
</div>
</template>
hydrateOnMediaQuery 전략을 사용합니다.hydrate-after
지정된 지연 시간(밀리초) 후에 컴포넌트를 하이드레이션합니다.
<template>
<div>
<LazyMyComponent :hydrate-after="2000" />
</div>
</template>
hydrate-when
불리언 조건에 따라 컴포넌트를 하이드레이션합니다.
<template>
<div>
<LazyMyComponent :hydrate-when="isReady" />
</div>
</template>
<script setup lang="ts">
const isReady = ref(false)
function myFunction() {
// 커스텀 하이드레이션 전략 트리거...
isReady.value = true
}
</script>
hydrate-never
컴포넌트를 절대 하이드레이션하지 않습니다.
<template>
<div>
<LazyMyComponent hydrate-never />
</div>
</template>
하이드레이션 이벤트 리스닝
모든 지연 하이드레이션 컴포넌트는 하이드레이션 시 @hydrated 이벤트를 발생시킵니다.
<template>
<div>
<LazyMyComponent hydrate-on-visible @hydrated="onHydrate" />
</div>
</template>
<script setup lang="ts">
function onHydrate() {
console.log("컴포넌트가 하이드레이션되었습니다!")
}
</script>
주의사항 및 모범 사례
지연 하이드레이션은 성능상의 이점을 제공할 수 있지만, 올바르게 사용하는 것이 중요합니다:
- 뷰포트 내 콘텐츠 우선순위 지정: 중요한, 화면 상단(above-the-fold) 콘텐츠에는 지연 하이드레이션을 피하세요. 즉시 필요하지 않은 콘텐츠에 가장 적합합니다.
- 조건부 렌더링: lazy 컴포넌트에
v-if="false"를 사용하는 경우, 지연 하이드레이션이 필요하지 않을 수 있습니다. 일반 lazy 컴포넌트만 사용하면 됩니다. - 공유 상태: 여러 컴포넌트 간에 공유 상태(
v-model)를 사용할 때 주의하세요. 한 컴포넌트에서 모델을 업데이트하면 해당 모델에 바인딩된 모든 컴포넌트의 하이드레이션이 트리거될 수 있습니다. - 각 전략의 용도에 맞게 사용: 각 전략은 특정 목적에 최적화되어 있습니다.
hydrate-when은 항상 하이드레이션이 필요하지 않을 수 있는 컴포넌트에 가장 적합합니다.hydrate-after는 특정 시간만큼 기다릴 수 있는 컴포넌트에 적합합니다.hydrate-on-idle은 브라우저가 유휴 상태일 때 하이드레이션해도 되는 컴포넌트에 적합합니다.
- 상호작용이 필요한 컴포넌트에
hydrate-never사용 금지: 컴포넌트가 사용자 상호작용을 필요로 한다면, 절대 하이드레이션하지 않도록 설정해서는 안 됩니다.
직접 임포트
Nuxt의 자동 임포트 기능을 우회하고 싶거나 필요하다면, #components에서 컴포넌트를 명시적으로 임포트할 수도 있습니다.
<script setup lang="ts">
import { NuxtLink, LazyMountainsList } from '#components'
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">Show List</button>
<NuxtLink to="/">Home</NuxtLink>
</div>
</template>
커스텀 디렉토리
기본적으로 ~/components 디렉토리만 스캔됩니다. 다른 디렉토리를 추가하거나, 이 디렉토리의 하위 폴더 내에서 컴포넌트가 스캔되는 방식을 변경하려면, 구성에 추가 디렉토리를 추가할 수 있습니다:
export default defineNuxtConfig({
components: [
// ~/calendar-module/components/event/Update.vue => <EventUpdate />
{ path: '~/calendar-module/components' },
// ~/user-module/components/account/UserDeleteDialog.vue => <UserDeleteDialog />
{ path: '~/user-module/components', pathPrefix: false },
// ~/components/special-components/Btn.vue => <SpecialBtn />
{ path: '~/components/special-components', prefix: 'Special' },
// `~/components` 하위 디렉터리에 적용하려는 오버라이드(재정의)가 있다면,
// 이 항목은 마지막에 위치해야 합니다.
//
// ~/components/Btn.vue => <Btn />
// ~/components/base/Btn.vue => <BaseBtn />
'~/components'
]
})
npm 패키지
npm 패키지에서 컴포넌트를 자동 임포트하고 싶다면, 로컬 모듈에서 addComponent를 사용하여 등록할 수 있습니다.
import { addComponent, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
// import { MyComponent as MyAutoImportedComponent } from 'my-npm-package'
addComponent({
name: 'MyAutoImportedComponent',
export: 'MyComponent',
filePath: 'my-npm-package',
})
},
})
<template>
<div>
<!-- 지정한 이름으로 컴포넌트가 자동 임포트됩니다 -->
<MyAutoImportedComponent />
</div>
</template>
컴포넌트 확장자
기본적으로, nuxt.config.ts의 extensions 키에 지정된 확장자를 가진 모든 파일이 컴포넌트로 처리됩니다.
컴포넌트로 등록할 파일 확장자를 제한해야 하는 경우, 컴포넌트 디렉토리 선언의 확장형과 extensions 키를 사용할 수 있습니다:
export default defineNuxtConfig({
components: [
{
path: '~/components',
extensions: ['.vue'], }
]
})
클라이언트 컴포넌트
컴포넌트가 클라이언트 사이드에서만 렌더링되어야 한다면, 컴포넌트에 .client 접미사를 추가할 수 있습니다.
| components/
--| Comments.client.vue
<template>
<div>
<!-- 이 컴포넌트는 클라이언트 사이드에서만 렌더링됩니다 -->
<Comments />
</div>
</template>
#components 임포트에서만 동작합니다. 실제 경로에서 명시적으로 임포트하면 클라이언트 전용 컴포넌트로 변환되지 않습니다..client 컴포넌트는 마운트된 후에만 렌더링됩니다. onMounted()를 사용하여 렌더링된 템플릿에 접근하려면, onMounted() 훅의 콜백에서 await nextTick()을 추가하세요.서버 컴포넌트
서버 컴포넌트를 사용하면 클라이언트 사이드 앱 내에서 개별 컴포넌트를 서버 렌더링할 수 있습니다. 정적 사이트를 생성하는 경우에도 Nuxt에서 서버 컴포넌트를 사용할 수 있습니다. 이를 통해 동적 컴포넌트, 서버 렌더링된 HTML, 심지어 정적 마크업 청크가 혼합된 복잡한 사이트를 구축할 수 있습니다.
서버 컴포넌트는 단독으로 사용하거나 클라이언트 컴포넌트와 함께 사용할 수 있습니다.
독립형 서버 컴포넌트
독립형 서버 컴포넌트는 항상 서버에서 렌더링되며, Islands 컴포넌트라고도 합니다.
props가 업데이트되면 네트워크 요청이 발생하여 렌더링된 HTML이 즉시 업데이트됩니다.
서버 컴포넌트는 현재 실험적 기능이며, 사용하려면 nuxt.config에서 'component islands' 기능을 활성화해야 합니다:
export default defineNuxtConfig({
experimental: {
componentIslands: true
}
})
이제 .server 접미사가 붙은 서버 전용 컴포넌트를 등록하고, 애플리케이션 어디에서나 자동으로 사용할 수 있습니다.
-| components/
---| HighlightedMarkdown.server.vue
<template>
<div>
<!--
이 컴포넌트는 자동으로 서버에서 렌더링되므로, 마크다운 파싱 + 하이라이팅
라이브러리가 클라이언트 번들에 포함되지 않습니다.
-->
<HighlightedMarkdown markdown="# Headline" />
</div>
</template>
서버 전용 컴포넌트는 내부적으로 <NuxtIsland>를 사용하므로, lazy prop과 #fallback 슬롯이 모두 전달됩니다.
서버 컴포넌트 내 클라이언트 컴포넌트
experimental.componentIslands.selectiveClient가 true여야 합니다.클라이언트 사이드에서 로드할 컴포넌트에 nuxt-client 속성을 설정하여 컴포넌트를 부분적으로 하이드레이션할 수 있습니다.
<template>
<div>
<HighlightedMarkdown markdown="# Headline" />
<!-- Counter는 클라이언트 사이드에서 로드 및 하이드레이션됩니다 -->
<Counter nuxt-client :count="5" />
</div>
</template>
experimental.componentIsland.selectiveClient가 'deep'으로 설정된 경우에만 동작하며, 서버 사이드에서 렌더링되므로 클라이언트 사이드에서는 상호작용할 수 없습니다.서버 컴포넌트 컨텍스트
서버 전용 또는 아일랜드 컴포넌트를 렌더링할 때, <NuxtIsland>는 NuxtIslandResponse를 반환하는 fetch 요청을 만듭니다. (서버에서 렌더링되는 경우 내부 요청이며, 클라이언트 사이드 내비게이션에서 렌더링되는 경우 네트워크 탭에서 요청을 볼 수 있습니다.)
이것은 다음을 의미합니다:
NuxtIslandResponse를 생성하기 위해 서버 사이드에서 새로운 Vue 앱이 생성됩니다.- 컴포넌트를 렌더링하는 동안 새로운 '아일랜드 컨텍스트'가 생성됩니다.
- 앱의 나머지 부분에서 '아일랜드 컨텍스트'에 접근할 수 없으며, 아일랜드 컴포넌트에서도 앱의 나머지 컨텍스트에 접근할 수 없습니다. 즉, 서버 컴포넌트 또는 아일랜드는 앱의 나머지 부분과 _격리_되어 있습니다.
- 플러그인은 아일랜드를 렌더링할 때 다시 실행되며,
env: { islands: false }가 설정된 경우(객체 문법 플러그인에서 설정 가능)에는 실행되지 않습니다.
아일랜드 컴포넌트 내에서는 nuxtApp.ssrContext.islandContext를 통해 해당 아일랜드 컨텍스트에 접근할 수 있습니다. 아일랜드 컴포넌트가 아직 실험적이므로, 이 컨텍스트의 형식은 변경될 수 있습니다.
display: contents;가 적용된 <div>로 감싸집니다.클라이언트 컴포넌트와 함께 사용
이 경우, .server + .client 컴포넌트는 컴포넌트의 두 '절반'으로, 서버와 클라이언트에서 각각 별도의 구현을 사용하는 고급 사용 사례에 사용할 수 있습니다.
-| components/
---| Comments.client.vue
---| Comments.server.vue
<template>
<div>
<!-- 이 컴포넌트는 서버에서는 Comments.server, 브라우저에서 마운트되면 Comments.client를 렌더링합니다 -->
<Comments />
</div>
</template>
내장 Nuxt 컴포넌트
Nuxt가 제공하는 <ClientOnly>, <DevOnly> 등 여러 컴포넌트가 있습니다. 자세한 내용은 API 문서를 참고하세요.
라이브러리 저자
자동 트리 셰이킹 및 컴포넌트 등록이 가능한 Vue 컴포넌트 라이브러리를 만드는 것은 매우 쉽습니다. ✨
@nuxt/kit에서 제공하는 addComponentsDir 메서드를 사용하여 Nuxt 모듈에서 컴포넌트 디렉토리를 등록할 수 있습니다.
다음과 같은 디렉토리 구조를 상상해보세요:
-| node_modules/
---| awesome-ui/
-----| components/
-------| Alert.vue
-------| Button.vue
-----| nuxt.ts
-| pages/
---| index.vue
-| nuxt.config.ts
그런 다음 awesome-ui/nuxt.ts에서 addComponentsDir 훅을 사용할 수 있습니다:
import { createResolver, defineNuxtModule, addComponentsDir } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const resolver = createResolver(import.meta.url)
// ./components 디렉토리를 목록에 추가
addComponentsDir({
path: resolver.resolve('./components'),
prefix: 'awesome',
})
},
})
이게 전부입니다! 이제 프로젝트의 nuxt.config 파일에서 Nuxt 모듈로 UI 라이브러리를 임포트할 수 있습니다:
export default defineNuxtConfig({
modules: ['awesome-ui/nuxt']
})
... 그리고 pages/index.vue에서 모듈 컴포넌트(awesome- 접두사)를 바로 사용할 수 있습니다:
<template>
<div>
My <AwesomeButton>UI button</AwesomeButton>!
<awesome-alert>Here's an alert!</awesome-alert>
</div>
</template>
이렇게 하면 사용된 경우에만 컴포넌트가 자동으로 임포트되고, node_modules/awesome-ui/components/에서 컴포넌트를 업데이트할 때 HMR도 지원됩니다.