안녕하세요 dev_writer입니다.
현 회사에 입사하면서 axios를 현업에서 처음 다루게 되었는데요, 그 과정에서 알게 된 axios와 axios 인터셉터에 대해 정리해 보겠습니다.
본 글에 사용되는 axios는 1.7.9 버전을 이용하였습니다.
Axios
우선 axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트입니다. (axios 소개 문서)
특징
- 서버 사이드에서는 node.js의 http 모듈을, 브라우저에서는 XMLHttpRequest를 사용합니다.
- Promise API를 지원합니다.
- 요청 및 응답을 인터셉트할 수 있습니다.
- JSON 데이터를 자동으로 변환합니다.
- XSRF (Cross-site request forgery, 크로스 사이트 요청 위조)를 막기 위한 클라이언트 사이드를 지원합니다.
axios와 주로 비교되는 fetch도 있는데, axios와 fetch의 비교는 내용이 깊어지기에 아래 reference에 추가한 글들을 읽어보시기를 추천합니다.
axios 이용 예시
axios를 이용하여 OpenAI에 메시지 생성을 하는 것을 예시로 들겠습니다. (POST 예시)
1️⃣ axios (Axios 인스턴스)를 만든 후 호출하기
axios를 만든 뒤 이용하는 방식은 axios.post를 이용하면 됩니다. axios의 라이브러리 코드를 보면 post 이외에도 get, delete 등 HTTP Method 별 요청을 정의해 둔 것을 볼 수 있습니다.
이때 T는 받고자 하는 응답 데이터 형식, R은 T를 data로 가지고 있는 AxiosResponse, D는 요청 형식을 의미합니다.
가령 ChatGPT를 연결하는 케이스를 본다면 아래 코드처럼 작성하면 됩니다. (openai의 라이브러리를 이용하지 않는 경우)
import { Axios } from 'axios';
// OpenAI API 키 저장
const OPENAI_API_KEY = 'Open AI API 키';
// POST 요청 시 보낼 데이터 정의
const requestData = {
model: 'gpt-4o',
messages: [
{
role: 'developer',
content: 'You are a helpful assistant.',
},
{
role: 'user',
content: 'Tell me about axios',
}
]
};
// 부가적인 요청 설정 정의
const requestConfig = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
}
};
// 받을 응답 타입 정의
type responseData = {
id: string;
object: string;
created: number;
model: string;
system_fingerprint: string;
choices: {
index: number;
message: {
role: string;
content: string;
},
logprobs: object;
finish_reason: string;
}[];
usage: {
prompt_token: number;
completion_tokens: number;
total_tokens: number;
completion_tokens_details: {
reasoning_tokens: number;
accepted_prediction_tokens: number;
rejected_prediction_tokens: number;
}
}
};
export class ChatGptApiInstance {
protected readonly axios: Axios;
constructor() {
// 기본 baseURL 설정
this.axios = new Axios({ baseURL: 'https://api.openai.com' });
}
// data는 responseData 타입
async generateText() {
const { data } = await this.axios.post<responseData>('/v1/chat/completion', requestData, requestConfig);
}
}
이때 post에 정의된 T (responseData에 해당) 말고 R, D를 정의하지 않아도 되는 이유는 T를 정의할 시 R은 기본값으로 AxiosResponse<T>로 정의되며, D는 any 타입으로 정의되기 때문입니다. 만약 요청 타입 (type)을 명확히 할 것이라면, post<응답 타입, AxiosResponse<응답 타입>, 요청 타입>으로 작성하면 됩니다.
2️⃣ axiosInstance를 이용한 방법 (axios.create)
다음으로 가능한 방법은 axios (AxiosStatic).create를 통해 AxiosInstance를 이용하는 방법이 있습니다.
첫 번째 방식과 코드는 거의 같습니다.
import axios, { AxiosInstance } from 'axios';
// .. 같은 코드들
export class ChatGptApiInstance {
protected readonly axiosInstance: AxiosInstance;
constructor() {
// 기본 baseURL 설정
this.axiosInstance = axios.create({
baseURL: 'https://api.openai.com',
});
}
// data는 responseData 타입
async generateText() {
const { data } = await this.axiosInstance.post<responseData>('/v1/chat/completion', requestData, requestConfig);
}
}
1번과 2번의 차이
그렇다면 1번 (new Axios)을 이용하는 방법과 2번 (axios.create)을 이용하는 방법의 차이점은 무엇일지 알아보겠습니다.
AxiosInstance 필드를 이용할 시, 필드로 직접 요청이 가능합니다. (함수로서 이용 가능)
async generateText() {
const { data } = await this.axiosInstance<responseData>('/v1/chat/completions', { method: 'POST', data: requestData, headers: requestConfig.headers });
return data;
}
해당 예시 코드처럼 필드를 함수로 사용할 수 있습니다. new Axios를 이용할 때에는 불가능합니다.
이 외에도 헤더를 좀 더 세밀히 다룰 수 있다는 특징이 보입니다.
declare class Axios {
constructor(config?: axios.AxiosRequestConfig);
defaults: axios.AxiosDefaults;
...
}
interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers: HeadersDefaults;
}
interface HeadersDefaults {
common: RawAxiosRequestHeaders;
delete: RawAxiosRequestHeaders;
get: RawAxiosRequestHeaders;
head: RawAxiosRequestHeaders;
post: RawAxiosRequestHeaders;
put: RawAxiosRequestHeaders;
patch: RawAxiosRequestHeaders;
options?: RawAxiosRequestHeaders;
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
}
Axios를 이용할 경우에는 axios.defaults.headers. [common | delete |... unlink?]가 가능한 반면
AxiosInstance에서는 axios.defaults.headers ['example'] = 'value'처럼 HeaderDefaults 이외의 키도 문자열로 지정할 수 있게 되어 있습니다.
구버전에서는 Axios 클래스가 따로 없고 AxiosInstance만 있었던 반면, 지금은 Axios와 AxiosInstance가 나뉘게 되었습니다. (AxiosInstance가 Axios를 상속받도록 설계) AxiosInstance가 Axios를 상속받은 인터페이스인 만큼, 향후 확장성을 고려했을 때에는 AxiosInstance를 이용하는 게 더 나아 보입니다.
Interceptor
개발 생태계에서 인터셉터 (Interceptor)란 요청과 응답을 가로채는 역할을 하는 기능을 뜻합니다. 위에 특징에 작성하였듯, axios에서도 인터셉터를 이용하여 요청과 응답을 가로채고 수정할 수 있습니다.
axios 공식 문서에서는 아래와 같은 예시 코드를 안내하고 있습니다.
/* 해당 예제 코드에서의 axios는 axios 모듈의 AxiosStatic을 의미합니다. */
// 요청 인터셉터 추가하기
axios.interceptors.request.use(function (config) {
// 요청이 전달되기 전에 작업 수행
return config;
}, function (error) {
// 요청 오류가 있는 작업 수행
return Promise.reject(error);
});
// 응답 인터셉터 추가하기
axios.interceptors.response.use(function (response) {
// 2xx 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 데이터가 있는 작업 수행
return response;
}, function (error) {
// 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
// 응답 오류가 있는 작업 수행
return Promise.reject(error);
});
인터셉터는 언제 쓰기 유용할까?
요청과 응답을 가로채고 수정할 수 있다는 점에서, 인터셉터는 반복되는 요청 형식 작성을 단축할 수 있다는 이점이 있습니다.
요청의 경우, 단순히 헤더에 토큰을 주입하는 것 말고도 요청 파라미터들을 모두 이용하여 별도의 signature를 생성한 뒤 API 요청 시 전달해야 하는 작업 등이 있다면 주어진 요청 파라미터들을 취합하여 signature를 생성하는 함수를 주입시킬 수도 있습니다.
응답의 경우에도 로깅을 해야 하는 경우가 있다면, 인터셉터에서 로깅을 하도록 할 수도 있습니다.
정리하자면 인터셉터를 이용하면 반복적인 행위를 한 번의 코드로 자동화할 수 있다는 특징이 있습니다.
인터셉터 사용 예시
해당 예시에서의 요청/응답 형식은 실제 OpenAI와 다를 수 있습니다.
AxiosInstance 방식의 인터셉터를 이용한 코드를 작성해 보겠습니다.
조금 더 요구사항을 복잡하게 하기 위해, ChatGPT에 POST 요청 시 전달되는 body의 key 값들을 모두 취합하여 signature를 생성해야 하는 요구사항이 ChatGPT API에 있다고 가정해 보겠습니다.
1단계: ChatGptApiInstance 및 인터셉터 정의
각 API마다 공통적으로 사용할 ChatGptApiInstance를 정의합니다.
const OPENAI_API_KEY = 'Open AI API 키';
const ENDPOINT = 'https://api.openai.com';
export type GptApiResponse<T = any> = {
data: T,
code: string;
};
export class ChatGptApiInstance {
protected readonly axiosInstance: AxiosInstance;
protected constructor(
) {
this.axiosInstance = axios.create({
baseURL: ENDPOINT,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
}
});
// 요청 인터셉터
this.axiosInstance.interceptors.request.use(
(config) => this.onFulfilledRequest(config),
(error) => this.onRejectedRequest(error),
);
// 응답 인터셉터
this.axiosInstance.interceptors.response.use(
(response) => this.onFulfilledResponse(response),
(error) => this.onRejectedOnResponse(error),
);
}
protected async onFulfilledRequest(
config: InternalAxiosRequestConfig,
): Promise<InternalAxiosRequestConfig> {
Logger.log(`[OpenAI API request] ${config.method} ${config.url}`);
const payload: string = generateSignaturePayload(config.data);
return {
...config,
data: {
...config.data,
// 기존 요청된 data 이외에 추가로 signature 덧붙임
sign: generateSign(payload),
}
};
}
private onFulfilledResponse(
res: AxiosResponse<GptApiResponse>
): Promise<AxiosResponse> {
Logger.log(`[OpenAI API response] ${config.method} ${config.url}`);
if (res.data.code === '0') {
return Promise.resolve({
...res,
data: res.data,
});
}
return Promise.reject(new Error(`[${res.data.code}]`));
}
protected async onRejectedRequest(error: unknown): Promise<never> {
return Promise.reject(error);
}
protected async onRejectedOnResponse(error: unknown): Promise<never> {
return Promise.reject(error);
}
}
- AxiosInstance를 ChatGptApiInstance에 필드로 관리되도록 합니다.
- 요청 인터셉터를 정의합니다.
- 요청 전 요청한 데이터들 (config 하위 필드)을 이용하여 signature를 생성하는 함수를 인터셉터에 정의합니다. 또한 요청 중 에러 발생 시 처리할 함수를 정의합니다.
- 응답 인터셉터를 정의합니다.
- axios 응답을 이용하여 정상 여부를 판정하고 정상이라면 resolve, 오류라면 reject 시키는 작업을 인터셉터에 정의합니다.
이제 각 API 구현체들은 signature 생성을 직접 진행하지 않아도 됩니다.
2단계: 각 구현체 정의 (Text, Image)
각 구현체에서는 아래처럼 작성하면 됩니다.
// 텍스트 생성 API 구현체
export class ChatGptTextApi extends ChatGptApiInstance {
constructor() {
super();
}
async generateText(): Promise<GptTextResponse> {
const { data } = await this.axiosInstance.post<GptTextResponse>(
'/v1/chat/completions',
{
...textRequestData,
}
)
return data;
}
}
// 이미지 생성 API 구현체
export class ChatGptImageApi extends ChatGptApiInstance {
constructor() {
super();
}
async generateImage(): Promise<GptImageResponse> {
const { data } = await this.axiosInstance.post<GptImageResponse>(
'/v1/images/generations',
{
...imageRequestData,
}
)
return data;
}
}
결론
axios와 이것이 가지고 있는 인터셉터에 대해 정리해 보니 텍스트로만 알고 있던 내용을 직접 접할 수 있던 좋은 계기였습니다.
- 사내 코드는 현재 버전 (1.7.9) 보다 더 이전 버전이기에, 구조 및 원리가 지금 정리한 글과 약간 다르게 흘러갑니다. 이 때문에 axios는 외부 라이브러리이므로 업데이트에 따라 구조, 원리가 달라질 수 있다는 특징을 체감할 수 있었습니다. (Axios 클래스가 정의되고 요청 인터셉터의 config가 AxiosRequestConfig에서 InternalAxiosRequestConfig로 변경된 것 등)
- axios에서는 정리한 글처럼 인터셉터를 바로 지원하지만, fetch에서는 직접 인터셉터를 구현해야 하는 등 fetch가 axios에 비해 지원되는 기능들이 제한된다는 점도 체감할 수 있었습니다.
지금까지 axios 및 axios의 인터셉터에 대해 알아봤습니다. 만약 틀린 점이 있거나 고칠 점이 있다면 피드백 발생 시 반영하겠습니다.
Reference
'언어 > JS&TS' 카테고리의 다른 글
[설정] JS/TS 프로젝트 설정법 (in Webstorm) (1) | 2025.01.28 |
---|