본문 바로가기
잡다한 글들....

React 프로젝트 와 API 구조 구성 (feat. 환경변수 Mock 사용법)

by 개발자 인민군 2026. 2. 26.

이 글에서는 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 입력하면 된다.
(다른 배포 서비스 또한 위와 같은 방식으로 변수에 직접 기입하면 된다.)