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는 <SomeMyComponent>가 아니라 <MyComponent>로 사용할 수 있게 됩니다.
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 컴포넌트는 앱의 청크 크기를 제어하는 데는 좋지만, 조건부 렌더링되지 않는 한 여전히 적극적으로 로드되기 때문에 항상 런타임 성능을 향상시키지는 않습니다. 실제 애플리케이션에서는 일부 페이지에 많은 콘텐츠와 많은 컴포넌트가 포함될 수 있으며, 대부분의 경우 페이지가 로드되자마자 모든 컴포넌트가 상호작용 가능할 필요는 없습니다. 이들을 모두 적극적으로 로드하면 성능에 부정적인 영향을 줄 수 있습니다.
앱을 최적화하기 위해, 일부 컴포넌트의 hydration을 화면에 보일 때까지 또는 브라우저가 더 중요한 작업을 마칠 때까지 지연시키고 싶을 수 있습니다.
Nuxt는 lazy(또는 지연) hydration을 사용하여, 컴포넌트가 언제 상호작용 가능해질지 제어할 수 있도록 지원합니다.
Nuxt는 다양한 내장 hydration 전략을 제공합니다. lazy 컴포넌트당 하나의 전략만 사용할 수 있습니다.
hydrate-never가 설정된 컴포넌트의 prop을 변경하면 hydration이 발생합니다)v-bind로 prop 객체를 펼쳐 전달하는 방식은 사용할 수 없습니다). 또한 #components에서 직접 임포트하는 경우에는 작동하지 않습니다.hydrate-on-visible컴포넌트가 뷰포트에 보이게 될 때 hydration을 수행합니다.
<template>
<div>
<LazyMyComponent hydrate-on-visible />
</div>
</template>
hydrateOnVisible 전략을 사용합니다.hydrate-on-idle브라우저가 idle 상태일 때 컴포넌트를 hydration합니다. 컴포넌트를 가능한 한 빨리 로드해야 하지만, 크리티컬 렌더링 경로를 차단하고 싶지 않을 때 적합합니다.
최대 타임아웃으로 사용할 숫자를 전달할 수도 있습니다.
<template>
<div>
<LazyMyComponent hydrate-on-idle />
</div>
</template>
hydrateOnIdle 전략을 사용합니다.hydrate-on-interaction지정된 상호작용(예: click, mouseover) 이후에 컴포넌트를 hydration합니다.
<template>
<div>
<LazyMyComponent hydrate-on-interaction="mouseover" />
</div>
</template>
이벤트나 이벤트 목록을 전달하지 않으면 기본적으로 pointerenter, click, focus 시에 hydration됩니다.
hydrateOnInteraction 전략을 사용합니다.hydrate-on-media-query윈도우가 특정 미디어 쿼리와 일치할 때 컴포넌트를 hydration합니다.
<template>
<div>
<LazyMyComponent hydrate-on-media-query="(max-width: 768px)" />
</div>
</template>
hydrateOnMediaQuery 전략을 사용합니다.hydrate-after지정된 지연 시간(밀리초) 이후에 컴포넌트를 hydration합니다.
<template>
<div>
<LazyMyComponent :hydrate-after="2000" />
</div>
</template>
hydrate-when불리언 조건에 따라 컴포넌트를 hydration합니다.
<template>
<div>
<LazyMyComponent :hydrate-when="isReady" />
</div>
</template>
<script setup lang="ts">
const isReady = ref(false)
function myFunction () {
// 커스텀 hydration 전략 트리거...
isReady.value = true
}
</script>
hydrate-never컴포넌트를 절대 hydration하지 않습니다.
<template>
<div>
<LazyMyComponent hydrate-never />
</div>
</template>
모든 지연 hydration 컴포넌트는 hydration이 완료되면 @hydrated 이벤트를 발생시킵니다.
<template>
<div>
<LazyMyComponent
hydrate-on-visible
@hydrated="onHydrate"
/>
</div>
</template>
<script setup lang="ts">
function onHydrate () {
console.log('Component has been hydrated!')
}
</script>
지연 hydration은 성능상의 이점을 제공할 수 있지만, 올바르게 사용하는 것이 중요합니다:
v-if="false"를 사용하는 경우, 지연 hydration이 필요하지 않을 수 있습니다. 일반적인 lazy 컴포넌트만 사용해도 됩니다.v-model)에 유의하세요. 한 컴포넌트에서 모델을 업데이트하면 해당 모델에 바인딩된 모든 컴포넌트에서 hydration이 트리거될 수 있습니다.hydrate-when은 항상 hydration이 필요하지 않을 수 있는 컴포넌트에 가장 적합합니다.hydrate-after는 일정 시간 동안 기다릴 수 있는 컴포넌트에 적합합니다.hydrate-on-idle은 브라우저가 idle 상태일 때 hydration해도 되는 컴포넌트에 적합합니다.hydrate-never를 사용하지 마세요: 사용자 상호작용이 필요한 컴포넌트는 절대 hydration되지 않도록 설정해서는 안 됩니다.Nuxt의 자동 임포트 기능을 우회하고 싶거나 그럴 필요가 있는 경우, #components에서 컴포넌트를 명시적으로 임포트할 수도 있습니다.
<script setup lang="ts">
import { LazyMountainsList, NuxtLink } 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 패키지에서 컴포넌트를 자동 임포트하고 싶다면, 로컬 모듈에서 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 속성을 설정하여 컴포넌트를 부분적으로 hydration할 수 있습니다.
<template>
<div>
<HighlightedMarkdown markdown="# Headline" />
<!-- Counter는 클라이언트 측에서 로드 및 hydration됩니다 -->
<Counter
nuxt-client
:count="5"
/>
</div>
</template>
experimental.componentIsland.selectiveClient가 'deep'으로 설정된 경우에만 작동하며, 서버 측에서 렌더링되기 때문에 클라이언트 측에서는 상호작용이 불가능합니다.서버 전용 또는 island 컴포넌트를 렌더링할 때, <NuxtIsland>는 NuxtIslandResponse를 반환하는 fetch 요청을 수행합니다. (서버에서 렌더링되는 경우 내부 요청이며, 클라이언트 측 내비게이션에서 렌더링되는 경우 네트워크 탭에서 확인할 수 있는 요청입니다.)
이는 다음을 의미합니다:
NuxtIslandResponse를 생성하기 위해 서버 측에서 새로운 Vue 앱이 생성됩니다.env: { islands: false }가 설정된 경우(객체 문법 플러그인에서 설정 가능)를 제외하고, island를 렌더링할 때 플러그인이 다시 실행됩니다.island 컴포넌트 내부에서는 nuxtApp.ssrContext.islandContext를 통해 해당 island 컨텍스트에 접근할 수 있습니다. island 컴포넌트가 여전히 실험적 기능인 동안에는 이 컨텍스트의 형식이 변경될 수 있다는 점에 유의하세요.
display: contents;가 설정된 <div>로 래핑됩니다.이 경우 .server + .client 컴포넌트는 하나의 컴포넌트를 이루는 두 '절반'이며, 서버와 클라이언트 측에서 각각 다른 구현을 사용하는 고급 사용 사례에 활용할 수 있습니다.
-| components/
---| Comments.client.vue
---| Comments.server.vue
<template>
<div>
<!-- 이 컴포넌트는 서버에서는 Comments.server를 렌더링하고, 브라우저에서 마운트된 후에는 Comments.client를 렌더링합니다 -->
<Comments />
</div>
</template>
Nuxt는 <ClientOnly>와 <DevOnly>를 포함해 여러 컴포넌트를 제공합니다. 이에 대해서는 API 문서에서 더 자세히 읽어볼 수 있습니다.
자동 트리 셰이킹과 컴포넌트 등록이 가능한 Vue 컴포넌트 라이브러리를 만드는 것은 매우 쉽습니다. ✨
Nuxt 모듈에서 @nuxt/kit이 제공하는 addComponentsDir 메서드를 사용해 컴포넌트 디렉터리를 등록할 수 있습니다.
다음과 같은 디렉터리 구조를 가정해 봅시다:
-| node_modules/
---| awesome-ui/
-----| components/
-------| Alert.vue
-------| Button.vue
-----| nuxt.ts
-| pages/
---| index.vue
-| nuxt.config.ts
그런 다음 awesome-ui/nuxt.ts에서 addComponentsDir 훅을 사용할 수 있습니다:
import { addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup () {
const resolver = createResolver(import.meta.url)
// ./components 디렉터리를 목록에 추가
addComponentsDir({
path: resolver.resolve('./components'),
prefix: 'awesome',
})
},
})
이게 전부입니다! 이제 프로젝트의 nuxt.config 파일에서 UI 라이브러리를 Nuxt 모듈로 임포트할 수 있습니다:
export default defineNuxtConfig({
modules: ['awesome-ui/nuxt'],
})
... 그리고 app/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도 지원됩니다.