# Nuxt 4 서버 API (Nitro) 학습 정리 ## 목차 1. [디렉토리 구조](#디렉토리-구조) 2. [server/api vs server/routes](#serverapi-vs-serverroutes) 3. [케이스별 학습 예제](#케이스별-학습-예제) 4. [핵심 유틸 함수](#핵심-유틸-함수) 5. [클라이언트 연동](#클라이언트-연동) 6. [성능 고려사항](#성능-고려사항) 7. [서버 API 사용 기준](#서버-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`) ```ts export default defineEventHandler(() => { return { message: "안녕하세요!", timestamp: new Date().toISOString(), } }) ``` - `defineEventHandler()` 로 핸들러 정의 - 반환값은 자동으로 JSON 직렬화 --- ### 케이스 2: POST + readBody (`02-users.post.ts`) ```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`) ```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`) ```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`) ```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`) ```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`) ```ts // server/utils/response.ts 정의 export function apiSuccess(data: T): ApiResponse { ... } 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`) ```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`) ```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 액션 | ```ts // 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' } }) } ``` --- ## 성능 고려사항 ### 이벤트 루프 블로킹 주의 ```ts // ❌ 동기 처리 → 이벤트 루프 블로킹 const data = fs.readFileSync('./file.json') // ✅ 비동기 처리 const data = await fs.promises.readFile('./file.json') ``` ### 무거운 CPU 작업 - 이미지 처리, 암호화 등 CPU 집약 작업 → `worker_threads` 분리 고려 - Node.js 단일 스레드 특성상 해당 작업 중 다른 요청 처리 지연 발생 ### 캐싱 활용 ```ts // 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 로 충분 |