- 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.
9.1 KiB
9.1 KiB
Nuxt 4 서버 API (Nitro) 학습 정리
목차
디렉토리 구조
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 로 충분 |