Files
nuxt4-deep/server/README.md
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

331 lines
9.1 KiB
Markdown

# 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<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`)
```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 로 충분 |