Nuxt 모듈 작성, 플러그인 실행 순서, 빌드/런타임 훅으로 프레임워크 확장.
모듈·플러그인·런타임 훅으로 확장하기
입문편에서 자동 임포트와 composable을 통해 애플리케이션 코드를 구조화하는 방법을 배웠다면, 이제는 한 단계 더 깊이 들어가 프레임워크 자체를 확장하는 방법을 다룰 차례입니다. Nuxt의 진정한 강점은 @nuxt/kit이 제공하는 빌드 타임 훅, 모듈 시스템, 플러그인 레이어를 통해 개발자가 프레임워크 동작을 선언적으로 제어할 수 있다는 점에 있습니다.
이 강에서는 플러그인의 실행 흐름부터 커스텀 모듈 작성, 훅 시스템의 구조, 그리고 런타임 설정 관리까지 — 실무에서 Nuxt 기반 라이브러리를 개발하거나 팀 내 공통 인프라를 구축할 때 반드시 알아야 할 심화 내용을 체계적으로 살펴봅니다.
학습 목표
- 플러그인의 실행 순서와
dependsOn,provide/inject패턴을 이해하고 올바르게 활용할 수 있다. - 서버/클라이언트 전용 플러그인의 파일 접미사 규칙과 실행 환경 분리 전략을 설명할 수 있다.
@nuxt/kit의addPlugin,addComponent,addImports등을 사용해 커스텀 Nuxt 모듈을 작성할 수 있다.- 빌드 타임 훅과 런타임 훅의 차이를 이해하고 적절한 상황에 각각을 적용할 수 있다.
runtimeConfig와app.config의 사용 구분 기준과 보안 고려 사항을 설명할 수 있다.
플러그인의 실행 순서, dependsOn, provide/inject
Nuxt 플러그인은 plugins/ 디렉터리에 위치하며, 파일명의 알파벳 순서에 따라 자동으로 등록됩니다. 그러나 알파벳 순서에만 의존하면 플러그인 간 의존 관계를 명확하게 표현하기 어렵습니다. 이를 해결하기 위해 Nuxt 3는 defineNuxtPlugin의 dependsOn 옵션을 제공합니다.
// plugins/01.logger.ts
export default defineNuxtPlugin({
name: 'logger',
setup(nuxtApp) {
const logger = {
log: (msg: string) => console.log(`[APP] ${msg}`),
error: (msg: string) => console.error(`[APP ERROR] ${msg}`),
}
// provide로 등록 → useNuxtApp().$logger 로 접근 가능
return { provide: { logger } }
},
})
// plugins/02.api.ts
export default defineNuxtPlugin({
name: 'api',
// 'logger' 플러그인이 먼저 초기화되어야 함을 명시
dependsOn: ['logger'],
setup(nuxtApp) {
const { $logger } = nuxtApp
const api = {
get: async (url: string) => {
$logger.log(`GET ${url}`)
return $fetch(url)
},
}
return { provide: { api } }
},
})
dependsOn에 나열된 플러그인 이름들은 반드시 해당 플러그인의 name 필드와 일치해야 합니다. 이름이 없는 플러그인은 dependsOn으로 참조할 수 없습니다.
provide로 반환한 값은 Vue 애플리케이션 전체에 inject되어, 컴포넌트 내에서 useNuxtApp() 헬퍼나 $ 접두어를 통해 접근할 수 있습니다.
<script setup lang="ts">
// composable 스타일로 접근
const { $logger, $api } = useNuxtApp()
// 또는 컴포넌트 Options API에서는 this.$logger
const data = await $api.get('/api/users')
$logger.log('데이터 로드 완료')
</script>
⚠️ 주의
provide로 노출된 값은 서버 렌더링 시 요청마다 새로운 인스턴스를 공유합니다. 요청 간 상태가 섞이지 않도록 플러그인 내부에서 전역 가변 상태(싱글턴 패턴)를 사용할 때는 서버 환경을 고려해야 합니다.
서버/클라이언트 전용 플러그인과 파일 접미사
Nuxt는 파일명 접미사를 통해 플러그인의 실행 환경을 선택적으로 제한할 수 있습니다.
| 파일명 패턴 | 실행 환경 |
|---|---|
plugins/foo.ts | 서버 + 클라이언트 양쪽 |
plugins/foo.server.ts | 서버(SSR) 전용 |
plugins/foo.client.ts | 클라이언트(브라우저) 전용 |
이 메커니즘을 활용하면 브라우저 전용 API(예: localStorage, window)나 서버 전용 리소스(예: 데이터베이스 연결)에 안전하게 접근할 수 있습니다.
// plugins/analytics.client.ts — 브라우저에서만 실행
export default defineNuxtPlugin(() => {
// window 객체 접근이 안전함
window.addEventListener('unhandledrejection', (event) => {
console.error('미처리 Promise 오류:', event.reason)
})
// 서드파티 분석 스크립트 초기화
const analytics = {
track: (event: string, props?: Record<string, unknown>) => {
// 실제 환경에서는 gtag, mixpanel 등을 호출
console.log('[Analytics]', event, props)
},
}
return { provide: { analytics } }
})
// plugins/db-health.server.ts — 서버에서만 실행
export default defineNuxtPlugin(async () => {
// 서버 사이드에서 DB 연결 상태 확인
try {
// 가상의 DB ping
await $fetch('/api/_health/db')
console.log('[Server] DB 연결 정상')
} catch {
console.error('[Server] DB 연결 실패 — 애플리케이션을 점검하세요.')
}
})
💡 TIP 클라이언트 전용 플러그인에서
provide한 값은 서버에서는provide자체가 실행되지 않아 해당 키가 주입되지 않으므로, 서버 측 컴포넌트 코드에서nuxtApp.$xxx접근 시undefined입니다. 따라서 서버에서도 호출될 수 있는 composable이나 컴포넌트에서 이를 사용할 때는 반드시if (process.client)가드나 optional chaining($analytics?.track(...))을 사용해야 합니다.
// ✅ 클라이언트 전용 provide 값의 안전한 사용
const { $analytics } = useNuxtApp()
if (import.meta.client) {
$analytics.track('page_view', { path: route.path })
}
// ❌ SSR 중 런타임 오류 발생 가능
$analytics.track('page_view') // 서버에서 $analytics가 undefined
@nuxt/kit으로 커스텀 모듈 만들기
Nuxt 모듈은 defineNuxtModule로 정의하는 함수로, 빌드 타임에 한 번 실행됩니다. 모듈 안에서 @nuxt/kit의 헬퍼들을 사용해 플러그인, 컴포넌트, 자동 임포트 등을 프로그래밍 방식으로 등록할 수 있습니다.
// modules/my-ui/index.ts
import {
defineNuxtModule,
addPlugin,
addComponent,
addImports,
createResolver,
} from '@nuxt/kit'
export interface ModuleOptions {
prefix: string
theme: 'light' | 'dark'
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-ui',
configKey: 'myUi', // nuxt.config.ts에서 myUi: { ... }로 설정
compatibility: { nuxt: '^3.0.0' },
},
defaults: {
prefix: 'My',
theme: 'light',
},
setup(options, nuxt) {
// 모듈 자신의 디렉터리를 기준으로 절대 경로를 생성
const resolver = createResolver(import.meta.url)
// 1) 플러그인 등록
addPlugin(resolver.resolve('./runtime/plugin'))
// 2) 컴포넌트 등록 (접두어 적용)
addComponent({
name: `${options.prefix}Button`, // MyButton
filePath: resolver.resolve('./runtime/components/Button.vue'),
})
addComponent({
name: `${options.prefix}Modal`, // MyModal
filePath: resolver.resolve('./runtime/components/Modal.vue'),
})
// 3) Composable 자동 임포트 등록
addImports({
name: 'useMyUi',
as: 'useMyUi',
from: resolver.resolve('./runtime/composables/useMyUi'),
})
// 4) 모듈 옵션을 런타임에서도 접근 가능하도록 runtimeConfig에 노출
nuxt.options.runtimeConfig.public.myUiTheme = options.theme
},
})
createResolver는 import.meta.url 기준의 절대 경로 해석기를 반환합니다. 이를 사용하지 않으면 빌드 환경에 따라 경로가 달라지는 문제가 발생할 수 있습니다.
모듈의 런타임 플러그인은 빌드 후에도 최종 번들에 포함됩니다. 플러그인 파일도 일반 플러그인과 동일한 방식으로 작성합니다.
// modules/my-ui/runtime/plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
const theme = useRuntimeConfig().public.myUiTheme
console.log(`[my-ui] 테마 초기화: ${theme}`)
})
nuxt.config.ts에서 로컬 모듈은 경로로, npm 게시 모듈은 패키지명으로 등록합니다.
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'./modules/my-ui', // 로컬 모듈
'@nuxtjs/tailwindcss', // npm 모듈
],
myUi: {
prefix: 'Ds', // DsButton, DsModal
theme: 'dark',
},
})
빌드 타임 훅과 런타임 훅의 차이
Nuxt는 두 가지 서로 다른 훅 시스템을 제공합니다. 이 둘을 혼동하면 코드가 예상치 못한 시점에 실행되거나 전혀 실행되지 않는 문제가 생깁니다.
**빌드 타임 훅 (Nuxt hooks)**은 nuxt.config.ts 또는 모듈의 setup() 안에서 nuxt.hook()으로 등록하며, nuxi build / nuxi dev 실행 중 Node.js 프로세스에서 한 번만 호출됩니다.
// nuxt.config.ts — 빌드 훅 등록
export default defineNuxtConfig({
hooks: {
// Vite 설정을 프로그래밍 방식으로 수정
'vite:extendConfig'(config, { isClient }) {
if (isClient) {
config.resolve!.alias = {
...config.resolve!.alias,
'lodash': 'lodash-es', // 트리 쉐이킹을 위해 교체
}
}
},
// 빌드 완료 후 훅
'build:done'(builder) {
console.log('빌드 완료:', builder.nuxt.options.buildDir)
},
// 라우트 생성 시 훅 — 동적으로 라우트 추가/수정 가능
'pages:extend'(pages) {
// 페이지 라우트를 프로그래밍 방식으로 추가
pages.push({
name: 'docs-catch-all',
path: '/docs/:slug(.*)*',
file: '~/pages/docs/[...slug].vue',
})
},
},
// 리다이렉트는 meta가 아니라 routeRules로 처리해야 동작합니다.
// (meta는 임의 데이터 저장소일 뿐, Nuxt/Vue Router가 meta.redirect를 보고 리다이렉트하지 않습니다.)
routeRules: {
'/old-path': { redirect: '/' },
},
})
**런타임 훅 (Nuxt App hooks / Nitro hooks)**은 브라우저나 서버(Nitro) 런타임에서 실행되며, 플러그인 안에서 nuxtApp.hook() 또는 서버 플러그인에서 Nitro의 훅 시스템으로 등록합니다.
// plugins/runtime-hooks.ts — 런타임 훅
export default defineNuxtPlugin((nuxtApp) => {
// 페이지 전환 전 호출
nuxtApp.hook('page:start', () => {
console.log('페이지 전환 시작')
})
// Vue 앱 에러 캐처
nuxtApp.hook('vue:error', (error, instance, info) => {
console.error('Vue 오류:', error, info)
// Sentry 등 에러 트래킹 서비스로 전송 가능
})
// SSR payload가 클라이언트에 도착했을 때
nuxtApp.hook('app:mounted', () => {
console.log('앱이 마운트되었습니다')
})
})
// server/plugins/nitro-hook.ts — Nitro 런타임 훅
export default defineNitroPlugin((nitroApp) => {
// 모든 서버 응답 후처리
nitroApp.hooks.hook('render:response', (response) => {
response.headers['X-Powered-By'] = 'MyApp/1.0'
})
// 에러 핸들링
nitroApp.hooks.hook('error', (error) => {
console.error('[Nitro] 처리되지 않은 오류:', error)
})
})
| 구분 | Nuxt 빌드 훅 | Nuxt 앱 런타임 훅 | Nitro 훅 |
|---|---|---|---|
| 실행 시점 | nuxi dev/build 중 | 브라우저·SSR 런타임 | Nitro 서버 런타임 |
| 등록 위치 | nuxt.config.ts, 모듈 setup() | plugins/ | server/plugins/ |
| API | nuxt.hook() | nuxtApp.hook() | nitroApp.hooks.hook() |
| 재실행 | 빌드 시 1회 | 요청/이벤트마다 | 요청/이벤트마다 |
⚠️ 주의 빌드 훅 안에서
import한 런타임 의존성(예: 클라이언트 라이브러리)은 번들에 포함되지 않습니다. 빌드 훅은 순수하게 Nuxt/Vite 설정을 조작하는 용도로만 사용해야 합니다.
runtimeConfig와 app.config의 사용 구분 및 보안
이 두 가지는 모두 "설정을 런타임에 노출"하는 메커니즘이지만, 목적과 특성이 명확히 다릅니다.
runtimeConfig 는 환경 변수로 재정의할 수 있는 서버/클라이언트 설정 공간입니다. public 하위에 넣은 값만 클라이언트(브라우저)에 노출되며, 루트 레벨 값은 서버에서만 접근 가능합니다.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// ✅ 서버에서만 접근 가능 (클라이언트 번들에 포함 안 됨)
databaseUrl: process.env.DATABASE_URL ?? '',
apiSecret: process.env.API_SECRET ?? '',
public: {
// ✅ 클라이언트와 서버 양쪽에서 접근 가능
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL ?? 'https://api.example.com',
appVersion: '1.2.0',
},
},
})
환경 변수 재정의 규칙: NUXT_ 접두어 후 대문자+언더스코어로 변환한 키로 .env 파일 또는 프로세스 환경 변수에서 재정의됩니다.
# .env
NUXT_DATABASE_URL=postgresql://prod-server/mydb
NUXT_API_SECRET=super-secret-key
NUXT_PUBLIC_API_BASE_URL=https://api.prod.example.com
app.config 는 빌드 타임에 고정되는 공개 설정 공간으로, 환경 변수로 재정의할 수 없습니다. 테마 토큰, 기능 플래그, UI 설정처럼 배포 후 변경되지 않는 값에 적합합니다.
// app.config.ts
export default defineAppConfig({
ui: {
primary: 'indigo',
gray: 'slate',
},
featureFlags: {
newDashboard: true,
betaEditor: false,
},
})
컴포넌트나 composable에서는 useAppConfig()로 접근하며, 반응형으로 동작합니다(개발 중 HMR 지원).
<script setup lang="ts">
const appConfig = useAppConfig()
// appConfig.ui.primary → 'indigo'
const config = useRuntimeConfig()
// config.public.apiBaseUrl → 서버+클라이언트 가능
// config.apiSecret → 서버에서만 접근 가능, 클라이언트에서는 undefined
</script>
⚠️ 주의
runtimeConfig의 루트 레벨(non-public) 값은 절대로 컴포넌트에서 직접 사용하면 안 됩니다. 이 값들은 서버 API 라우트(server/api/)나 서버 플러그인(server/plugins/)에서만 사용해야 합니다. Nuxt는 SSR 페이로드에 이 값을 포함하지 않지만, 개발자가 실수로provide나useState에 담아 클라이언트로 노출시킬 위험이 있습니다.
두 설정의 주요 차이를 정리하면 다음과 같습니다.
| 항목 | runtimeConfig | app.config |
|---|---|---|
| 변경 가능 시점 | 런타임(환경 변수) | 빌드 타임 고정 |
| 클라이언트 노출 | public 키만 | 전체 (민감 정보 금지) |
| 서버 전용 값 | 루트 레벨 가능 | 불가 |
| 주요 용도 | API 키, DB URL, 엔드포인트 | 테마, 기능 플래그, UI 설정 |
| 반응형 | 아니오 | 예 (HMR) |
모듈에서 타입 자동 생성 — addTypeTemplate
@nuxt/kit의 addTypeTemplate을 사용하면 모듈이 등록한 플러그인, composable, 설정 등에 대한 TypeScript 타입을 빌드 타임에 자동으로 생성할 수 있습니다. 이는 라이브러리 소비자의 DX(개발자 경험)를 크게 향상시킵니다.
// modules/my-ui/index.ts (위 예제에서 이어서)
import {
defineNuxtModule,
addPlugin,
addTypeTemplate,
createResolver,
} from '@nuxt/kit'
export default defineNuxtModule<ModuleOptions>({
meta: { name: 'my-ui', configKey: 'myUi' },
defaults: { prefix: 'My', theme: 'light' },
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
addPlugin(resolver.resolve('./runtime/plugin'))
// $myUi 플러그인 헬퍼의 타입을 NuxtApp 인터페이스에 병합
addTypeTemplate({
filename: 'types/my-ui.d.ts',
getContents: () => `
// 자동 생성된 파일 — 직접 수정하지 마세요
import type { MyUiInstance } from '${resolver.resolve('./runtime/types')}'
declare module '#app' {
interface NuxtApp {
$myUi: MyUiInstance
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$myUi: MyUiInstance
}
}
export {}
`,
})
// runtimeConfig 타입 보강
addTypeTemplate({
filename: 'types/my-ui-config.d.ts',
getContents: () => `
declare module 'nuxt/schema' {
interface PublicRuntimeConfig {
myUiTheme: 'light' | 'dark'
}
}
export {}
`,
})
},
})
생성된 타입 파일은 .nuxt/types/ 디렉터리에 저장되며, tsconfig.json이 .nuxt/tsconfig.json을 확장하는 Nuxt 기본 설정 덕분에 자동으로 TypeScript 프로젝트에 포함됩니다.
// modules/my-ui/runtime/types.ts
export interface MyUiInstance {
showToast: (message: string, type?: 'info' | 'error' | 'success') => void
setTheme: (theme: 'light' | 'dark') => void
}
이제 소비자 코드에서 타입 추론이 완벽하게 작동합니다.
// 사용하는 프로젝트의 어느 파일에서든
const { $myUi } = useNuxtApp()
$myUi.showToast('저장되었습니다', 'success') // ✅ 타입 자동완성 지원
$myUi.showToast('오류', 'warning') // ❌ 타입 오류: 'warning'은 허용되지 않음
💡 TIP
addTypeTemplate의getContents는 함수이므로, 모듈 옵션(options)을 클로저로 캡처하여 동적으로 타입을 생성할 수 있습니다. 예를 들어prefix옵션에 따라DsButton,DsModal같은 컴포넌트 이름을 타입으로 생성할 수 있습니다.
요약
- 플러그인은
name+dependsOn으로 실행 순서를 명시적으로 제어하고,provide로 앱 전체에 서비스를 주입할 수 있습니다. .server.ts/.client.ts접미사로 플러그인 실행 환경을 분리하며, 클라이언트 전용provide값은 SSR 환경에서undefined이므로 반드시 가드가 필요합니다.@nuxt/kit의addPlugin,addComponent,addImports를defineNuxtModule안에서 사용해 재사용 가능한 Nuxt 모듈을 작성할 수 있습니다.- 빌드 훅(
nuxt.hook)은 Nuxt/Vite 설정을 조작하는 빌드 타임 도구이고, 런타임 훅(nuxtApp.hook, Nitrohooks.hook)은 요청/이벤트 단위로 실행되는 런타임 도구입니다. runtimeConfig는 환경 변수 재정의가 가능한 서버/클라이언트 설정 공간이며, 민감 정보는 반드시public외부(루트 레벨)에 두어야 합니다.app.config는 빌드 타임 고정 공개 설정입니다.addTypeTemplate으로 모듈이 등록한 플러그인·설정의 타입을 자동 생성하면 소비자의 TypeScript 개발 경험이 크게 향상됩니다.
연습문제
-
plugins/디렉터리에auth플러그인과http플러그인을 작성하되,http플러그인이auth에 의존하도록dependsOn을 설정하고,auth가provide한 토큰을http플러그인에서 활용하세요. -
브라우저의
localStorage를 이용해 사용자 설정을 저장/불러오는 기능을 클라이언트 전용 플러그인으로 구현하고, SSR 중 안전하게 동작하도록 처리하세요. -
@nuxt/kit을 사용하여 간단한analytics모듈을 작성하세요. 이 모듈은trackingId: string옵션을 받아, 클라이언트 전용 플러그인을 자동으로 등록하고,useAnalytics()composable을 자동 임포트로 등록해야 합니다. -
runtimeConfig와app.config를 활용하여 다음 두 가지를 올바른 위치에 설정하세요: (a) 서드파티 API의 시크릿 키, (b) 앱의 UI 테마 색상(primary,secondary). 그리고 각각이 왜 그 위치에 있어야 하는지 코드 주석으로 설명하세요.
힌트 클라이언트 번들에 포함되면 안 되는 값은
runtimeConfig의 루트 레벨에, 배포 후에도 재정의되어야 하는 공개 값은public에, 빌드 타임 고정 UI 설정은app.config에 놓는 원칙을 기억하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“Nuxt.js 심화” 강좌에 대한 댓글입니다.