Axios 직접 타이핑하기
이번에는 axios 라이브러리를 직접 타이핑(Type Declaration) 해보는 연습을 해보겠다. 이미 만들어진 타입 선언 파일을 사용하는 것이 일반적이지만, 직접 타입을 선언하면서 구조를 이해하고 타입 시스템에 대한 감각을 기르는 것이 핵심 목표이다. 백지에서 직접 타이핑해보는 이와 같은 연습은 TypeScript 실력을 빠르게 향상시키는 데 많은 도움이 된다.
1. zaxios.ts 파일 생성
우선 axios의 역할을 흉내 내기 위한 파일을 만든다. zaxios.ts라는 파일을 생성하고 다음과 같은 코드를 입력한다.
interface Zaxios {}
declare const zaxios: Zaxios;
interface Post {
userid: number;
id: number;
title: string;
body: string;
}
(async () => {
try {
const res = await zaxios.get<Post>('https://jsonplaceholder.typicode.com/posts/1');
console.log(res.data.userid);
const res2 = await zaxios.post<Post>('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userid: 1,
});
console.log(res2.data.id);
} catch (error) {
if (zaxios.isAxiosError<{ message: string }>(error)) {
console.log(error.response?.data.message);
}
}
})();
이 코드에서는 zaxios.get, zaxios.post, zaxios.isAxiosError 등의 메서드를 사용하고 있다. 하지만 현재 Zaxios 인터페이스는 빈 객체이므로 타입 오류가 발생하게 된다.
2. 기본적인 메서드 시그니처 추가
이제 get, post, isAxiosError, create와 같은 메서드를 Zaxios 인터페이스에 추가한다. 응답의 타입을 제네릭으로 지정할 수 있도록 타입 매개변수도 추가한다.
interface ZaxiosResponse<ResponseData> {
data: ResponseData;
}
interface Zaxios {
get<ResponseData>(url: string): ZaxiosResponse<ResponseData>;
post<ResponseData>(url: string, requestData: unknown): ZaxiosResponse<ResponseData>;
isAxiosError<ResponseData>(error: unknown): boolean;
create(): Zaxios;
}
이후 위 코드를 실행하면 res.data.userid나 res2.data.id에 대한 접근에서 타입 오류는 발생하지 않지만, isAxiosError는 여전히 올바른 타입 판별을 제공하지 못한다.
3. 에러 타입 선언과 타입 서술(narrowing) 사용
isAxiosError 함수는 단순히 boolean을 반환하는 것이 아니라, error가 ZaxiosError 타입임을 좁혀주는 타입 서술 함수(type predicate) 여야 한다. 이를 위해 다음과 같이 타입을 선언하고 수정한다.
interface ZaxiosError<ResponseData> {
response?: ZaxiosResponse<ResponseData>;
}
interface Zaxios {
get<ResponseData>(url: string): ZaxiosResponse<ResponseData>;
post<ResponseData>(url: string, requestData: unknown): ZaxiosResponse<ResponseData>;
isAxiosError<ResponseData>(error: unknown): error is ZaxiosError<ResponseData>;
create(): Zaxios;
}
이제 if (zaxios.isAxiosError(error)) 구문을 통해 error 객체가 ZaxiosError 타입임을 보장할 수 있다. 이 덕분에 error.response?.data.message에 안전하게 접근할 수 있다.
4. 클래스 형태 및 함수 호출 형태 타이핑
Axios는 new Axios() 형태로도 사용할 수 있고, axios(config)처럼 함수로도 호출할 수 있다. 이를 위해 클래스와 함수 형태를 모두 타이핑해야 한다.
interface Config {
url: string;
method: string;
}
declare class ZAxios {
constructor();
get<ResponseData>(url: string): ZaxiosResponse<ResponseData>;
post<ResponseData>(url: string, requestData: unknown): ZaxiosResponse<ResponseData>;
}
그리고 Zaxios 인터페이스가 ZAxios 클래스를 확장하면서 함수로도 호출될 수 있도록 정의한다.
interface Zaxios extends ZAxios {
<ResponseData>(config: Config): ZaxiosResponse<ResponseData>;
isAxiosError<ResponseData>(error: unknown): error is ZaxiosError<ResponseData>;
create(): Zaxios;
}
마지막으로 다음과 같이 선언을 마무리한다.
declare const zaxios: Zaxios;
5. 전체 예제 코드
interface Config {
url: string;
method: string;
}
interface ZaxiosResponse<ResponseData> {
data: ResponseData;
}
interface ZaxiosError<ResponseData> {
response?: ZaxiosResponse<ResponseData>;
}
declare class ZAxios {
constructor();
get<ResponseData>(url: string): ZaxiosResponse<ResponseData>;
post<ResponseData>(url: string, requestData: unknown): ZaxiosResponse<ResponseData>;
}
interface Zaxios extends ZAxios {
<ResponseData>(config: Config): ZaxiosResponse<ResponseData>;
isAxiosError<ResponseData>(error: unknown): error is ZaxiosError<ResponseData>;
create(): Zaxios;
}
declare const zaxios: Zaxios;
6. 정리 및 결론
- 타입스크립트에서 직접 타입 선언을 해보는 연습은 매우 중요하다.
- 실제 구현 없이 declare 키워드를 활용하여 타입만 정의할 수 있다.
- unknown 타입은 any보다 안전하고, 후속 타입 좁히기를 통해 타입 안정성을 유지할 수 있다.
- 함수 호출 가능 인터페이스, 클래스 확장, 타입 서술 함수 등의 고급 기능도 함께 익힐 수 있다.
- 직접 선언한 타입은 실제 axios의 타입 정의와는 다르지만, 타입 에러 없이 작동하는 수준까지 작성하는 것이 중요하다.