
프론트엔드에서 백엔드 API를 연결할 때는 Swagger 문서를 통해 엔드포인트, 요청 데이터, 응답 구조 등을 확인한 뒤 API 요청 코드를 작성하게 된다.
JavaScript 프로젝트에서 Swagger와 Axios를 활용하여 API를 연결하는 기본적인 방법은 이전 글에서 정리하였다.
이번 글에서는 TypeScript 기반 React 프로젝트에서 Swagger 문서를 참고하여 Axios API 요청 구조를 구성하는 방법을 정리한다.
TypeScript에서는 JavaScript와 기본적인 API 연결 방식은 동일하지만, 요청 데이터와 Axios 설정에 타입을 명시하여 보다 안전하게 API를 관리할 수 있다.
로그인 기능이 아직 구현되지 않은 개발 초기 단계에서는 인증이 필요한 API를 테스트하기 위해 백엔드에서 테스트용 Access Token을 발급받아 사용할 수 있다.
필요한 경우 백엔드에 다음과 같은 설정을 요청한다.
- 테스트용 Access Token 발급
- 로컬 개발 주소에 대한 CORS 허용
- 프론트엔드 배포 주소에 대한 CORS 허용
테스트 토큰은 개발 과정에서 반복적으로 사용하기 때문에 가능하다면 충분한 유효기간을 가진 토큰을 사용하는 것이 편리하다.
서버 주소와 테스트 토큰은 코드에 직접 작성하지 않고 .env 파일에서 관리한다.
VITE_SERVER_URL_API=https://example-server.com
VITE_TEST_TOKEN=your_test_token
Vite에서는 VITE_ 접두사가 붙은 환경변수를 다음과 같이 사용할 수 있다.
import.meta.env.VITE_SERVER_URL_API
실제 Access Token이나 API Key와 같은 값은 저장소에 노출되지 않도록
.env파일을.gitignore에 추가하여 관리하는 것이 좋다.
로컬 개발 서버를 사용하는 경우 vite.config.ts에서 Proxy를 설정할 수 있다.
import { defineConfig, UserConfigExport } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from '@svgr/rollup';
const config: UserConfigExport = defineConfig({
plugins: [react(), svgr()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
'/admin': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
},
},
});
export default config;
/api, /admin으로 시작하는 요청을 백엔드 서버로 전달하도록 설정한 예시이다.
target: 요청을 전달할 백엔드 서버changeOrigin: 요청의 Origin을 대상 서버에 맞게 변경secure: HTTPS 인증서 검증 여부
개발 환경과 실제 배포 환경의 서버 주소가 다르다면 환경에 맞게 설정하여 사용한다.
API 요청에 공통으로 사용되는 설정을 관리하기 위해 Axios 인스턴스를 별도의 파일로 분리한다.
// API.ts
import axios, {
InternalAxiosRequestConfig,
} from 'axios';
// localStorage에서 Access Token 가져오기
export const getAccessToken = () =>
localStorage.getItem('accessToken');
// 테스트용 Access Token
const testToken = import.meta.env.VITE_TEST_TOKEN;
// Axios 인스턴스 생성
const api = axios.create({
baseURL: import.meta.env.VITE_SERVER_URL_API,
headers: {
'Content-Type': 'application/json',
},
});
// 요청 인터셉터
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getAccessToken() || testToken;
if (token && config.headers?.set) {
config.headers.set(
'Authorization',
`Bearer ${token}`,
);
}
return config;
},
(error) => Promise.reject(error),
);
export default api;
Axios 인스턴스를 만들어두면 모든 API 요청에서 공통으로 사용하는 설정을 한 곳에서 관리할 수 있다.
baseURL
- 공통 서버 주소 관리
headers
- 공통 Request Header 설정
interceptors
- API 요청 전 Access Token 등의 공통 로직 처리
로그인된 사용자의 accessToken이 존재하면 해당 토큰을 사용하고, 없는 경우 개발용 testToken을 사용하도록 구성하였다.
JavaScript와 달리 TypeScript에서는 Axios 요청 설정에도 타입을 지정할 수 있다.
(config: InternalAxiosRequestConfig) => {
// ...
}
InternalAxiosRequestConfig는 Axios 인터셉터 내부에서 전달되는 Request Config의 타입이다.
또한 Axios 버전에 따라 headers가 AxiosHeaders 형태로 관리되므로 set() 메서드를 사용하여 Header를 추가할 수 있다.
config.headers.set(
'Authorization',
`Bearer ${token}`,
);
이를 통해 TypeScript의 타입 검사를 유지하면서 Authorization Header를 설정할 수 있다.
API를 구현하기 전에 Swagger에서 해당 API의 명세를 확인한다.

주로 확인해야 하는 내용은 다음과 같다.
- HTTP Method (
GET,POST,PATCH,DELETE등)- Endpoint
- Path / Query Parameter
- Request Body
- Response Data
- Response Status Code
이번에는 학교 이메일 등록 신청 API를 예시로 사용한다.
POST /certification/school-email-registration
해당 API는 학교 이메일 도메인의 화이트리스트 등록을 신청하는 API이다.
Swagger에서 Request Body를 확인하면 다음과 같은 데이터가 필요하다.
{
"schoolId": "550e8400-e29b-41d4-a716-446655440001",
"contactEmail": "user@example.com",
"schoolEmail": "snu.ac.kr"
}
Response는 다음과 같은 구조로 반환된다.
{
"message": "string",
"code": "ILLEGAL_ARGUMENT",
"result": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"memberId": "550e8400-e29b-41d4-a716-446655440002",
"schoolId": "550e8400-e29b-41d4-a716-446655440003",
"contactEmail": "user@example.com",
"schoolEmail": "snu.ac.kr",
"createdAt": "2026-01-13T15:30:00",
"completedAt": "2026-01-14T10:00:00",
"status": "PENDING"
}
}
TypeScript에서는 이렇게 Swagger에서 확인한 Request와 Response 구조를 각각 타입으로 정의하여 사용할 수 있다.
먼저 Swagger의 Request Body를 기준으로 요청 데이터의 타입을 정의한다.
export interface SchoolEmailRegistrationRequest {
schoolId: string;
contactEmail: string;
schoolEmail: string;
}
이제 API 함수에 전달되는 데이터는 반드시 위 구조를 만족해야 한다.
const data: SchoolEmailRegistrationRequest = {
schoolId: '550e8400-e29b-41d4-a716-446655440001',
contactEmail: 'user@example.com',
schoolEmail: 'snu.ac.kr',
};
예를 들어 schoolId를 누락하거나 존재하지 않는 필드를 전달하면 TypeScript에서 타입 오류를 확인할 수 있다.
Request뿐만 아니라 Swagger의 Response 구조도 TypeScript 타입으로 정의할 수 있다.
먼저 학교 이메일 등록 신청의 상태값을 별도의 타입으로 정의한다.
export type SchoolEmailRegistrationStatus =
| 'PENDING'
| 'APPROVED'
| 'REJECTED';
이렇게 Union Type을 사용하면 status에는 지정된 세 가지 값만 사용할 수 있다.
PENDING: 승인 대기APPROVED: 승인 완료REJECTED: 승인 거절
이후 result에 포함되는 데이터의 타입을 정의한다.
export interface SchoolEmailRegistrationResult {
id: string;
memberId: string;
schoolId: string;
contactEmail: string;
schoolEmail: string;
createdAt: string;
completedAt: string | null;
status: SchoolEmailRegistrationStatus;
}
마지막으로 전체 Response 구조를 정의한다.
export interface SchoolEmailRegistrationResponse {
message: string;
code: string;
result: SchoolEmailRegistrationResult;
}
status를 단순히 string으로 정의하지 않고 Union Type을 사용했기 때문에 정해진 상태값 이외의 값을 사용할 경우 TypeScript에서 오류를 확인할 수 있다.
const status: SchoolEmailRegistrationStatus =
'PENDING'; // 정상
const status: SchoolEmailRegistrationStatus =
'WAITING'; // Type Error
completedAt은 신청이 아직 처리되지 않은 경우 값이 존재하지 않을 수 있으므로, 실제 API에서 null을 반환하는 명세라면 다음과 같이 nullable 타입으로 정의할 수 있다.
completedAt: string | null;
Request와 Response 타입을 정의했다면 실제 API 함수에 적용한다.
// certificationAPI.ts
import api from '../../../app/API';
export type SchoolEmailRegistrationStatus =
| 'PENDING'
| 'APPROVED'
| 'REJECTED';
export interface SchoolEmailRegistrationRequest {
schoolId: string;
contactEmail: string;
schoolEmail: string;
}
export interface SchoolEmailRegistrationResult {
id: string;
memberId: string;
schoolId: string;
contactEmail: string;
schoolEmail: string;
createdAt: string;
completedAt: string | null;
status: SchoolEmailRegistrationStatus;
}
export interface SchoolEmailRegistrationResponse {
message: string;
code: string;
result: SchoolEmailRegistrationResult;
}
export const postSchoolEmailRegistration = async (
data: SchoolEmailRegistrationRequest,
): Promise<SchoolEmailRegistrationResponse> => {
try {
const response =
await api.post<SchoolEmailRegistrationResponse>(
'/certification/school-email-registration',
data,
);
return response.data;
} catch (error) {
console.error(
'학교 이메일 등록 신청 API 요청 실패:',
error,
);
throw error;
}
};
TypeScript를 사용하면서 중요한 부분은 Request와 Response 모두 타입을 지정한다는 것이다.
Request에는 다음과 같이 타입을 지정한다.
data: SchoolEmailRegistrationRequest
Axios의 post()에는 서버에서 반환되는 Response 타입을 지정한다.
api.post<SchoolEmailRegistrationResponse>(
'/certification/school-email-registration',
data,
);
따라서 response.data 내부의 데이터 구조와 각 필드의 타입을 TypeScript가 추론할 수 있다.
response.data.message;
// string
response.data.result.schoolEmail;
// string
response.data.result.status;
// SchoolEmailRegistrationStatus
특히 status는 string이 아니라 다음 세 가지 값으로 제한된다.
'PENDING' | 'APPROVED' | 'REJECTED'
작성한 API 함수는 필요한 페이지에서 import하여 사용할 수 있다.
import {
postSchoolEmailRegistration,
SchoolEmailRegistrationRequest,
} from '../api/certificationAPI';
function SchoolEmailRegistrationPage() {
const handleSubmit = async () => {
const data: SchoolEmailRegistrationRequest = {
schoolId:
'550e8400-e29b-41d4-a716-446655440001',
contactEmail: 'user@example.com',
schoolEmail: 'snu.ac.kr',
};
try {
const response =
await postSchoolEmailRegistration(data);
console.log(response.message);
console.log(response.result.status);
if (response.result.status === 'PENDING') {
console.log('승인 대기 중');
}
if (response.result.status === 'APPROVED') {
console.log('승인 완료');
}
if (response.result.status === 'REJECTED') {
console.log('승인 거절');
}
} catch (error) {
console.error(
'학교 이메일 등록 신청 실패:',
error,
);
}
};
return (
<button onClick={handleSubmit}>
등록 신청
</button>
);
}
export default SchoolEmailRegistrationPage;
API 함수의 반환 타입이 정의되어 있기 때문에 페이지에서 별도로 Response 타입을 지정하지 않아도 각 필드의 타입이 자동으로 추론된다.
또한 status를 Union Type으로 정의했기 때문에 조건문을 작성할 때도 IDE의 자동완성을 활용할 수 있다.
if (response.result.status === 'APPROVED') {
// 승인된 경우 처리
}
존재하지 않는 상태값과 비교하거나 잘못된 값을 할당하는 실수도 개발 단계에서 확인하기 쉬워진다.
JavaScript에서도 Swagger 문서를 확인하여 동일한 API를 연결할 수 있다.
하지만 TypeScript에서는 Swagger의 명세를 타입으로 옮겨 코드에서도 데이터 구조를 명확하게 표현할 수 있다.
| 구분 | JavaScript | TypeScript |
|---|---|---|
| Request 구조 | 개발자가 직접 확인 | 타입으로 명시 가능 |
| Response 구조 | 실행 후 데이터 확인 | 반환 데이터 타입 추론 가능 |
| 잘못된 필드 사용 | 런타임까지 발견하지 못할 수 있음 | 개발 단계에서 확인 가능 |
| 상태값 관리 | 일반 문자열로 사용 | Union Type으로 값 제한 가능 |
| IDE 자동완성 | 제한적 | Request / Response 필드 자동완성 |
전체적인 API 연결 흐름은 다음과 같다.
Swagger
↓
Request / Response 구조 확인
↓
TypeScript 타입 정의
↓
Axios API 함수
↓
페이지에서 사용
Swagger에서 정의된 데이터 구조뿐만 아니라 PENDING, APPROVED, REJECTED와 같이 정해진 값도 타입으로 표현하면 API 명세를 코드에 더욱 정확하게 반영할 수 있다.
Swagger를 활용한 API 연결 방식 자체는 JavaScript와 TypeScript에서 크게 다르지 않다.
TypeScript에서는 여기서 한 단계 더 나아가 Swagger의 Request와 Response 명세를 타입으로 정의하여 실제 코드와 연결할 수 있다.
SchoolEmailRegistrationRequest
↓
postSchoolEmailRegistration()
↓
SchoolEmailRegistrationResponse
↓
SchoolEmailRegistrationStatus
이를 통해 잘못된 Request 데이터를 전달하거나 Response에 존재하지 않는 필드를 사용하는 문제를 개발 단계에서 확인할 수 있다.
또한 status처럼 사용할 수 있는 값이 정해져 있다면 Union Type을 활용하여 허용되는 값까지 타입으로 제한할 수 있다.
따라서 TypeScript 프로젝트에서는 Swagger를 단순히 API 주소와 데이터 구조를 확인하는 문서로만 사용하기보다, Swagger 명세를 Request / Response 타입으로 옮겨 API 코드와 함께 관리하는 방식으로 활용하는 것이 좋다.