# 02. API 통신 패턴 > **대상**: 프론트엔드/백엔드 개발자 > **버전**: 1.0.0 > **최종 수정**: 2026-03-09 --- ## 1. 전체 흐름 ``` 클라이언트(브라우저) ↓ fetch('/api/proxy/items?page=1') ← 토큰 없이 Next.js API Proxy (/api/proxy/[...path]) ↓ HttpOnly 쿠키에서 access_token 읽기 ↓ Authorization: Bearer {token} 헤더 추가 PHP Laravel Backend (https://api.xxx.com/api/v1/items?page=1) ↓ 응답 Next.js → 클라이언트 (응답 전달) ``` **왜 프록시?** - HttpOnly 쿠키는 JavaScript에서 읽을 수 없음 (XSS 방지) - 서버(Next.js)에서만 쿠키 읽어서 백엔드에 전달 가능 - 토큰 갱신(refresh)도 프록시에서 자동 처리 --- ## 2. API 호출 방법 2가지 ### 방법 A: Server Action (대부분의 경우) Server Action에서 `serverFetch` / `executeServerAction` 사용. ```typescript // components/accounting/Bills/actions.ts 'use server'; import { buildApiUrl } from '@/lib/api/query-params'; import { executePaginatedAction } from '@/lib/api'; export async function getBills(params: BillSearchParams) { return executePaginatedAction({ url: buildApiUrl('/api/v1/bills', { search: params.search, bill_type: params.billType !== 'all' ? params.billType : undefined, page: params.page, }), transform: transformBillApiToFrontend, errorMessage: '어음 목록 조회에 실패했습니다.', }); } ``` 컴포넌트에서 호출: ```typescript // 컴포넌트 내부 const result = await getBills({ search: '', page: 1 }); if (result.success) { setData(result.data); setPagination(result.pagination); } ``` ### 방법 B: 프록시 직접 호출 (특수한 경우) 대시보드 훅, 파일 다운로드 등 Server Action이 부적합한 경우에만 사용. ```typescript // hooks/useCEODashboard.ts const response = await fetch('/api/proxy/daily-report/summary'); const result = await response.json(); ``` --- ## 3. 핵심 유틸리티 ### 3.1 buildApiUrl — URL 빌더 (필수) ```typescript import { buildApiUrl } from '@/lib/api/query-params'; // 기본 사용 buildApiUrl('/api/v1/items') // → "https://api.xxx.com/api/v1/items" // 쿼리 파라미터 (undefined는 자동 제외) buildApiUrl('/api/v1/items', { search: '볼트', status: undefined, // ← 자동 제외됨 page: 1, // ← 숫자 → 문자 자동 변환 }) // → "https://api.xxx.com/api/v1/items?search=볼트&page=1" // 동적 경로 + 파라미터 buildApiUrl(`/api/v1/items/${id}`, { with_details: true }) ``` **금지 패턴:** ```typescript // ❌ 직접 URLSearchParams 사용 금지 const params = new URLSearchParams(); params.set('search', value); url: `${API_URL}/api/v1/items?${params.toString()}` ``` ### 3.2 executeServerAction — 단건 조회/CUD ```typescript import { executeServerAction } from '@/lib/api'; // 단건 조회 return executeServerAction({ url: buildApiUrl(`/api/v1/items/${id}`), transform: transformItemApiToFrontend, errorMessage: '품목 조회에 실패했습니다.', }); // 생성 (POST) return executeServerAction({ url: buildApiUrl('/api/v1/items'), method: 'POST', body: JSON.stringify(payload), errorMessage: '등록에 실패했습니다.', }); // 삭제 (DELETE) return executeServerAction({ url: buildApiUrl(`/api/v1/items/${id}`), method: 'DELETE', errorMessage: '삭제에 실패했습니다.', }); ``` **반환 구조:** ```typescript { success: boolean; data?: T; error?: string; fieldErrors?: Record; // Laravel validation 에러 } ``` ### 3.3 executePaginatedAction — 페이지네이션 목록 ```typescript import { executePaginatedAction } from '@/lib/api'; return executePaginatedAction({ url: buildApiUrl('/api/v1/items', { search: params.search, page: params.page, }), transform: transformItemApiToFrontend, errorMessage: '목록 조회에 실패했습니다.', }); ``` **반환 구조:** ```typescript { success: boolean; data: T[]; pagination: { currentPage: number; lastPage: number; perPage: number; total: number; }; error?: string; } ``` --- ## 4. Server Action 규칙 ### 파일 위치 각 도메인 컴포넌트 폴더 내 `actions.ts`: ``` src/components/accounting/Bills/actions.ts src/components/hr/EmployeeList/actions.ts ``` ### 필수 규칙 ```typescript 'use server'; // 첫 줄 필수 // ✅ 타입은 인라인 정의 (export interface/type 허용) export interface BillSearchParams { ... } // ❌ 타입 re-export 금지 (Next.js Turbopack 제한) // export type { BillType } from './types'; ← 컴파일 에러 // → 컴포넌트에서 원본 파일에서 직접 import할 것 ``` ### actions.ts 패턴 요약 | 작업 | 유틸리티 | HTTP | |------|---------|------| | 목록 조회 (페이지네이션) | `executePaginatedAction` | GET | | 단건 조회 | `executeServerAction` | GET | | 등록 | `executeServerAction` | POST | | 수정 | `executeServerAction` | PUT/PATCH | | 삭제 | `executeServerAction` | DELETE | --- ## 5. 인증 토큰 흐름 ``` [로그인] ↓ POST /api/proxy/auth/login ↓ 백엔드 → access_token + refresh_token 반환 ↓ 프록시에서 HttpOnly 쿠키로 설정 - access_token (HttpOnly, Max-Age=2h) - refresh_token (HttpOnly, Max-Age=7d) - is_authenticated (non-HttpOnly, 프론트 상태 확인용) [API 호출] ↓ 프록시가 쿠키에서 토큰 읽어 헤더 주입 [401 발생 시] ↓ authenticatedFetch가 자동 감지 ↓ refresh_token으로 새 access_token 발급 ↓ 재시도 (1회) ↓ 실패 시 → 쿠키 삭제 → 로그인 페이지 이동 ``` --- ## 6. 백엔드 개발자 참고 ### API 응답 규격 프론트엔드는 Laravel 표준 응답 구조를 기대합니다: ```json // 단건 { "success": true, "data": { ... } } // 페이지네이션 목록 { "success": true, "data": [ ... ], "current_page": 1, "last_page": 5, "per_page": 20, "total": 93 } // 에러 { "success": false, "message": "에러 메시지" } // Validation 에러 { "message": "The given data was invalid.", "errors": { "name": ["이름은 필수입니다."], "amount": ["금액은 0보다 커야 합니다."] } } ``` ### API 엔드포인트 기본 규칙 - 기본 경로: `/api/v1/{resource}` - RESTful: GET(조회), POST(생성), PUT/PATCH(수정), DELETE(삭제) - 페이지네이션: `?page=1&per_page=20` - 검색: `?search=키워드` - 개별 기능 API 스펙은 `sam-docs/frontend/api-specs/` 참조