Files
nuxt4-deep/server
hyeonggil fc7d3d14cf Add initial Nuxt 4 project setup with essential configurations
- Created .gitignore to exclude build outputs, logs, and environment files.
- Added nuxt.config.ts for project configuration, enabling Tailwind CSS and Pinia modules.
- Initialized package.json with scripts for development and production, and added necessary dependencies.
- Generated pnpm-lock.yaml for dependency management.
- Created README.md with setup instructions and development guidelines.
- Implemented server API examples in server/api/ directory, demonstrating various use cases.
- Added middleware for logging requests and responses.
- Included example Vue components and pages for server API interaction.
- Established basic project structure for Nuxt 4 application development.
2026-04-08 23:59:29 +09:00
..

Nuxt 4 서버 API (Nitro) 학습 정리

목차

  1. 디렉토리 구조
  2. server/api vs server/routes
  3. 케이스별 학습 예제
  4. 핵심 유틸 함수
  5. 클라이언트 연동
  6. 성능 고려사항
  7. 서버 API 사용 기준

디렉토리 구조

server/
├── api/           # /api/* 접두사 자동 부여
├── routes/        # 접두사 없음 (URL 그대로)
├── middleware/    # 모든 요청에 자동 실행
└── utils/         # 서버 전용 auto-import 헬퍼

server/api vs server/routes

server/api/ server/routes/
URL 접두사 /api/ 자동 추가 없음 (파일 경로 그대로)
예시 api/hello.ts/api/hello routes/hello.ts/hello
사용 케이스 일반 API Webhook, RSS, sitemap.xml

파일명 메서드 접미사

hello.get.ts    → GET    /api/hello
hello.post.ts   → POST   /api/hello
hello.ts        → 모든 메서드 수신 (내부에서 분기)
[id].get.ts     → GET    /api/[id] (동적 라우트)

케이스별 학습 예제

케이스 1: 기본 GET (01-hello.get.ts)

export default defineEventHandler(() => {
  return {
    message: "안녕하세요!",
    timestamp: new Date().toISOString(),
  }
})
  • defineEventHandler() 로 핸들러 정의
  • 반환값은 자동으로 JSON 직렬화

케이스 2: POST + readBody (02-users.post.ts)

export default defineEventHandler(async (event) => {
  const body = await readBody<{ name: string; email: string }>(event)

  if (!body.name || !body.email) {
    throw createError({ statusCode: 400, statusMessage: "필수 항목 누락" })
  }

  setResponseStatus(event, 201)
  return { id: crypto.randomUUID(), ...body }
})
  • readBody(event) : 요청 본문 파싱 (JSON → 객체)
  • setResponseStatus(event, 201) : 응답 상태 코드 변경

케이스 3: getQuery 쿼리 파라미터 (03-search.get.ts)

// GET /api/03-search?keyword=nuxt&page=2&limit=10
export default defineEventHandler((event) => {
  const query = getQuery(event)

  const keyword = String(query.keyword ?? "")
  const page    = Number(query.page ?? 1)
  const limit   = Number(query.limit ?? 10)

  // ...필터링 및 페이지네이션 처리
})
  • getQuery(event) : URL 쿼리 파라미터를 객체로 반환
  • 모든 값이 string 타입이므로 Number(), String() 변환 필요

케이스 4: 동적 라우트 파라미터 (04-users/[id].get.ts)

// GET /api/04-users/123
export default defineEventHandler((event) => {
  const id = getRouterParam(event, "id")
  // 중첩: server/api/users/[userId]/posts/[postId].get.ts
  // const { userId, postId } = getRouterParams(event)
})
  • 파일명의 [id] → URL 동적 세그먼트
  • getRouterParam(event, 'key') : 단일 파라미터
  • getRouterParams(event) : 전체 파라미터 객체

케이스 5: HTTP 메서드 분기 (05-items/[id].ts)

export default defineEventHandler(async (event) => {
  const method = event.method // 'GET' | 'PUT' | 'DELETE' ...

  if (method === "GET")    { /* 조회 */ }
  if (method === "PUT")    { /* 수정 */ }
  if (method === "DELETE") { /* 삭제 */ }

  throw createError({ statusCode: 405, statusMessage: "허용되지 않는 메서드" })
})
  • 파일명에 메서드 접미사 없음 → 모든 메서드 수신
  • event.method 로 분기

케이스 6: createError 에러 처리 (06-error-handling.get.ts)

// 기본 에러
throw createError({ statusCode: 404, statusMessage: "리소스 없음" })

// data 필드로 추가 정보 전달
throw createError({
  statusCode: 422,
  statusMessage: "유효성 검사 실패",
  data: { fields: { email: "형식 오류" } }
})
  • 클라이언트에서 error.value.statusCode, error.value.data 로 접근
  • try/catch 내부 예외는 500으로 래핑

케이스 7: server/utils 헬퍼 (server/utils/response.ts)

// server/utils/response.ts 정의
export function apiSuccess<T>(data: T): ApiResponse<T> { ... }
export function extractBearerToken(event): string | null { ... }

// server/api/07-products.get.ts 에서 import 없이 사용
export default defineEventHandler((event) => {
  const token = extractBearerToken(event) // auto-import
  return apiSuccess(products)
})
  • server/utils/ 하위 파일은 서버 전체에서 auto-import
  • 클라이언트에 노출되지 않는 서버 전용 헬퍼

케이스 8: server/routes Webhook (08-webhook.post.ts)

// POST /08-webhook  (/api 접두사 없음)
export default defineEventHandler(async (event) => {
  const secret = getHeader(event, "x-webhook-secret")
  if (secret !== "my-secret") {
    throw createError({ statusCode: 401 })
  }
  const payload = await readBody(event)
  return { received: true }
})
  • getHeader(event, key) : 요청 헤더 읽기
  • setHeader(event, key, value) : 응답 헤더 설정

케이스 9: 서버 미들웨어 (server/middleware/09-logger.ts)

export default defineEventHandler((event) => {
  // event.context에 데이터 저장 → 이후 핸들러에서 접근
  event.context.requestedAt = new Date().toISOString()

  // return 없음 → 다음 핸들러로 계속 진행
  // 값 반환 시 → 요청 처리 중단 (인증 게이트 패턴)
})
  • server/middleware/ 하위 파일은 모든 요청에 자동 실행
  • 실행 순서: 파일명 알파벳 순
  • 특정 경로만 처리: getRequestURL(event).pathname 으로 필터링

핵심 유틸 함수

함수 역할
readBody(event) POST 요청 본문 파싱 (JSON → 객체)
getQuery(event) URL 쿼리 파라미터 객체 반환
getRouterParam(event, 'key') 동적 라우트 파라미터 단일 읽기
getRouterParams(event) 동적 라우트 파라미터 전체 반환
getHeader(event, 'key') 요청 헤더 읽기
setHeader(event, key, val) 응답 헤더 설정
setResponseStatus(event, 201) 응답 상태 코드 설정
createError({ statusCode, statusMessage }) HTTP 에러 던지기
getRequestURL(event) 요청 URL 객체 반환

클라이언트 연동

useFetch vs $fetch

useFetch $fetch
SSR 지원 O (서버에서 미리 호출) X
자동 캐싱 O X
반응형 O (ref 전달 시 자동 재호출) X
사용 위치 컴포넌트 최상위 (setup) 이벤트 핸들러, Pinia 액션
// useFetch: 반응형 쿼리
const keyword = ref('Nuxt')
const { data } = await useFetch('/api/03-search', {
  query: { keyword } // keyword 변경 시 자동 재호출
})

// useFetch: 동적 URL
const id = ref('1')
const { data } = await useFetch(() => `/api/04-users/${id.value}`)

// $fetch: 이벤트 핸들러 내부
async function submit() {
  const result = await $fetch('/api/02-users', {
    method: 'POST',
    body: { name: '홍길동', email: 'hong@example.com' }
  })
}

성능 고려사항

이벤트 루프 블로킹 주의

// ❌ 동기 처리 → 이벤트 루프 블로킹
const data = fs.readFileSync('./file.json')

// ✅ 비동기 처리
const data = await fs.promises.readFile('./file.json')

무거운 CPU 작업

  • 이미지 처리, 암호화 등 CPU 집약 작업 → worker_threads 분리 고려
  • Node.js 단일 스레드 특성상 해당 작업 중 다른 요청 처리 지연 발생

캐싱 활용

// nuxt.config.ts: 라우트 레벨 캐싱
routeRules: {
  '/api/products': { cache: { maxAge: 60 } }
}

// 핸들러 레벨 캐싱
export default cachedEventHandler(async () => {
  return await fetchHeavyData()
}, { maxAge: 60, name: 'heavy-data' })

트래픽이 높을 때

  • Nitro는 기본 단일 프로세스 → PM2 클러스터 모드 또는 컨테이너 수평 확장
  • Vercel / Cloudflare Workers 배포 시 단일 스레드 제약 없음

서버 API 사용 기준

클라이언트에 노출되면 안 되는 것이 있거나, 클라이언트가 직접 접근할 수 없는 리소스를 다룰 때 사용

반드시 서버에서 처리

케이스 이유
DB 직접 접근 (Prisma, Drizzle) 연결 정보 노출 방지
외부 API 시크릿 키 사용 환경변수 클라이언트 노출 방지
인증 / 권한 검증 클라이언트 조작 불가 보장
민감한 비즈니스 로직 역공학 방지

있으면 좋은 경우

케이스 이유
여러 외부 API 집계 클라이언트 요청 수 감소
응답 데이터 가공/필터링 불필요한 데이터 노출 방지
서버 사이드 캐싱 DB/외부 API 부하 감소

서버 API 불필요

케이스 이유
공개 외부 API 직접 호출 숨길 키 없으면 클라이언트에서 직접 호출 가능
정적 데이터 assets/ 또는 public/ 에서 처리
클라이언트 상태 관리 Pinia 로 충분