이 글에서는 API 스펙을 받았지만, 실제 서버가 개발되지 않았을 때 쉽게 전환 할 수 있는 구조를 실제 프로젝트(Virgin Road)를 기반으로 설명한다.
문제 상황
- API 문서는 완성 되었지만 구현을 안되었을때
전체 구조도

1. 타입 정의
API에서 정의한 타입을 미리 작성해 둔다.
src/types/room.ts
export type Recipient = 'GROOM' | 'BRIDE' | 'BOTH';
export interface LetterSummary {
writerName: string;
recipient: Recipient;
}
export interface Room {
roomCode: string;
groomFirstName: string;
groomLastName: string;
brideFirstName: string;
brideLastName: string;
weddingDate: string;
dday: number;
letterCount: number;
letterList: LetterSummary[]; // ← Swagger와 정확히 일치
}
export interface CreateRoomRequest {
groomFirstName: string;
groomLastName: string;
brideFirstName: string;
brideLastName: string;
weddingDate: string;
email: string;
}
export interface VerifyCodeRequest {
roomCode: string;
authCode: string;
}
export interface RoomVerificationResponse {
roomCode: string;
email: string;
}
export interface RoomCreatedResponse {
roomCode: string;
shareUrl: string;
}
2. API 레이어 분리 - Mock/Real 분기점
이 패턴의 장점은 Mock과 Real이 같은 인터페이스 구현되어 환경변수 하나로 전환이 가능하여 컴포넌트는 신경을 쓰지 않는다.
파일 구조
src/lib/
├── api.ts ← 중앙 선택점 (Mock or Real)
├── api.mock.ts ← Mock 구현
└── api.real.ts ← Real API (이건 생략, api.ts에 직접 구현)
api.ts - 분기점 역할
src/lib/api.ts (핵심: 여기가 분기점!)
import type {
CreateRoomRequest,
RoomVerificationResponse,
Room,
VerifyCodeRequest,
RoomCreatedResponse,
} from '@/types/room';
import type { CreateLetterRequest, LetterSummaryResponse } from '@/types/letter';
import { mockApi } from './api.mock';
// ✨ 여기서 환경변수를 읽어서 Mock/Real 결정
const API_URL = import.meta.env.VITE_API_URL;
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
// ============ Error Handling ============
export class ApiError extends Error {
code: string;
constructor(code: string, message: string) {
super(message);
this.code = code;
this.name = 'ApiError';
}
}
// ============ Real API 구현 ============
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
};
}
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
try {
const response = await fetch(`${API_URL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
},
...options,
});
const json: ApiResponse<T> = await response.json();
if (!response.ok || !json.success) {
const code = json.error?.code || 'UNKNOWN_ERROR';
const message = json.error?.message || `API Error: ${response.status}`;
throw new ApiError(code, message);
}
return json.data!;
} catch (error) {
if (error instanceof ApiError) throw error;
throw new ApiError('NETWORK_ERROR', '네트워크 오류가 발생했습니다');
}
}
// ============ Real API 함수들 ============
const realApi = {
createRoom: (data: CreateRoomRequest) =>
request<RoomVerificationResponse>('/rooms', {
method: 'POST',
body: JSON.stringify(data),
}),
verifyEmail: (data: VerifyCodeRequest) =>
request<RoomCreatedResponse>('/rooms/verify', {
method: 'POST',
body: JSON.stringify(data),
}),
getRoom: (roomCode: string) =>
request<Room>(`/rooms/${roomCode}`),
createLetter: (roomCode: string, data: CreateLetterRequest) =>
request<LetterSummaryResponse>(`/rooms/${roomCode}/letters`, {
method: 'POST',
body: JSON.stringify(data),
}),
};
// ============ 핵심: Mock/Real 선택 ============
export const api = USE_MOCK ? mockApi : realApi;
// ↑ ↑
// 환경변수로 결정 realApi 또는 mockApi
3. Mock 데이터 관리
이제 API 에서 결과 값을 미리 정의하여 API 와 같이 사용한다. (AI 쓰면 개꿀)
# src/lib/api.mock.ts
import type {
CreateRoomRequest,
RoomVerificationResponse,
VerifyCodeRequest,
RoomCreatedResponse,
Room,
} from '@/types/room';
import type { CreateLetterRequest, LetterSummaryResponse } from '@/types/letter';
import { ApiError } from './api';
// 네트워크 지연 시뮬레이션
const delay = (ms: number = 1000) =>
new Promise(resolve => setTimeout(resolve, ms));
export const mockApi = {
// ============ 방 생성 ============
createRoom: async (data: CreateRoomRequest): Promise<RoomVerificationResponse> => {
console.log('🔴 [MOCK] createRoom:', data);
await delay(1000); // 네트워크 지연 시뮬레이션
// ✅ 응답 타입이 정확히 RoomVerificationResponse
return {
roomCode: 'a3bX9kLm',
email: data.email,
};
},
};
4. 환경변수로 전환 제어
이제 .env 파일 과 .env.local 파일로 나눈다.
.env 와 .env.local 파일로 나눈 이유는 실제 서버 주소가 노출되어 깃허브에 올라가는것을 막고 깃허브에 올라간 내 코드가 서버에서 주소 수정없이 배포를 하기 위함이다.
아래와 같이 .env 파일에 주소를 작성하지 않으면 http://localhost/api 로 자동 설정되므로 API 서버와 내 배포될 프론트 프로젝트가 같이 있다면 내부서버를 사용하게 되어 사용가능하다.
이 방식을 사용하게 되면 내 환경에서는 .env.local 이 있기 때문에 .env 을 무시하게 되고 다른 팀원 혹은 배포환경은 .env.local 가 없기 때문에 .env 를 사용하게 된다.
이 방법은 자체 서버를 구축해 놓은 방법에 사용가능하다.
# .env
`# 프론트와 백엔드가 같은 서버에 배포될 경우
VITE_API_URL=/api
# Mock API 사용 여부 (기본값: false)
VITE_USE_MOCK=false`
# .env.local
# Mock으로 개발하는 경우
VITE_API_URL=http://00.000.00.000/api
VITE_USE_MOCK=true # 또는 실제 서버로 테스트하는 경우 false 로 전환 한다.
만약 자주 사용하는 Vercel를 사용하게 되면 Vercel대시보드의 Environment Variables 로 들어가 VITE_API_URL 입력하면 된다.
(다른 배포 서비스 또한 위와 같은 방식으로 변수에 직접 기입하면 된다.)