PromleeBlog
sitemapaboutMe

posting thumbnail
TypeScript 환경에서 axios 타입 셋팅하기 (React, Next.js, React Native, node.js, ...)
Setting up axios types in a TypeScript environment (React, Next.js, React Native, node.js, ...)

📅

🚀

들어가기 전에🔗

이 포스팅은 axios 1.7.9 버전을 기준으로 작성되었습니다.
서버나 open api를 호출 할 때 가장 많이 사용하는 npm 라이브러리는 axios일 것입니다. 명확히 명시되어있는 api의 경우 Response, Request 타입을 지정해주어 TypeScript 환경에서의 오류 발생 가능성과 개발 효율성을 높일 수 있습니다.
이 포스팅의 정보는 Next.js 개발환경을 기준으로 작성되었지만, React, React Native, node.js 등 다양한 환경에서도 폴더 구조 변경, token 저장 방식 변경 등 약간의 수정으로 사용 가능합니다.
테스트를 위한 api는 jsonplaceholder를 사용합니다.
아래는 필자의 앱 라우팅 방식 폴더 구조입니다. 각자의 환경에 맞게 미리 생성해두시면 편리합니다. Next.js 환경이 아닌 다른 환경에서는 util 폴더의 내부 구조만 참고해주시면 됩니다.
📦src
┣ 📂app
┃ ┣ 📂test 				// 테스트 페이지(/test)
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📜layout.tsx
┃ ┗ 📜page.tsx				// 홈 페이지(/)
┗ 📂util
┃ ┣ 📂apis
┃ ┃ ┣ 📂@types 				// 타입 폴더
┃ ┃ ┃ ┣ 📜shared.ts
┃ ┃ ┃ ┣ 📜test.ts
┃ ┃ ┣ 📂service 			// api 서비스 폴더
┃ ┃ ┃ ┣ 📜test.ts
┃ ┃ ┣ 📜axios.ts 			// axios 설정 파일
┗ ┗ ┗ 📜index.ts

🚀

1. Axios 설치 및 기본 설정🔗

해당 프로젝트의 루트 디렉토리에서 아래 명령어를 실행합니다.
npm install axios
다음으로, .env등의 환경변수 파일을 생성하고 api의 엔드포인트 url을 추가해줍니다.
NEXT_PUBLIC_SERVER_URL=https://jsonplaceholder.typicode.com

🚀

2. axios.ts 파일 작성🔗

2.1. axios 인스턴스 생성🔗

axios.ts 파일을 생성하고 다음을 import 합니다.
src/util/apis/axios.ts
import axios, {
  AxiosError,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from "axios";
제일 먼저, 아래와 같이 axios 인스턴스를 생성합니다. baseURL은 환경변수에서 가져오고, headers는 기본적으로 Content-Type을 설정합니다.
src/util/apis/axios.ts
import axios from 'axios';
 
export const API = axios.create({
  baseURL: process.env.NEXT_PUBLIC_SERVER_URL,
  headers: {
    "Content-Type": "application/json",
  },
});

2.2. auth 토큰 관리 (선택)🔗

서버와 함께 로그인 기능을 구현 시에 토큰을 관리하는 방식은 다양합니다. 필자는 로컬 스토리지에 토큰을 저장하고 가져오는 방식을 사용합니다.
토큰을 사용하지 않는 경우 이 부분은 무시해도 됩니다.
src/util/apis/axios.ts
export const AuthStorage = {
  async setToken( accessToken: string ) {
    await localStorage.setItem("accessToken", accessToken);
  },
 
  async getToken(): Promise<string | null> {
    return await localStorage.getItem("accessToken");
  },
 
  async clear() {
    await localStorage.removeItem("accessToken");
  },
};

2.3. request 인터셉터 설정🔗

request 인터셉터는 요청을 보내기 전에 요청을 가로채서 요청을 수정할 수 있습니다. 예를 들어, 요청을 보내기 전에 헤더에 토큰을 추가할 수 있습니다.
여기서 어떠한 요청을 보냈는 지 로그를 찍는 코드도 추가해 줍니다. 개발 과정에서는 여기에 로그를 찍어 두어 디버깅을 하는 쪽이 편리했습니다. config의 모든 속성을 찍는 것 보다는 필요한 속성만 골라 찍도록 구성했습니다.
src/util/apis/axios.ts
API.interceptors.request.use(
  async (config: InternalAxiosRequestConfig) => {
    const token = await AuthStorage.getToken(); // 토큰 미사용시 무시
    if (token) {	// 토큰 사용시 헤더에 토큰 추가
      config.headers.Authorization = `Bearer ${token}`;
    }
    console.log({
      headers: config.headers,
      method: config.method,
      url: config.url,
      baseUrl: config.baseURL,
      data: config.data,
      params: config.params,
    });
    return config;
  },
  (error: AxiosError) => {
    console.log("API request error", error.config);
    return Promise.reject(error);
  }
);

2.4. response 인터셉터 설정🔗

response 인터셉터는 응답을 받았을 때 응답을 가로채서 응답을 수정할 수 있습니다. 서버에서 어떠한 응답이 오는지 로그를 찍는 코드도 추가해 줍니다.
이 때 서버에서 받는 데이터를 잘 확인하고, response에서 원하는 data를 꺼내서 사용해야 합니다. 이 때 서버에서 받는 데이터가 data 속성에 들어있는 경우가 있고, 종종 data.data 속성에 들어있는 경우가 있습니다.
error가 발생했을 때에는 좀 더 상세한 로그를 찍도록 구성했습니다. Shared.ErrorResponse 타입은 아래에서 설명하도록 하겠습니다.
src/util/apis/axios.ts
API.interceptors.response.use(
	(response: AxiosResponse) => {
		console.log({
			status: response.status,
			statusText: response.statusText,
			data: response.data,
		});
		return response.data;	// 서버에서 받는 데이터가 data 속성에 들어있는 경우
		// return response.data.data;	// 서버에서 받는 데이터가 data.data 속성에 들어있는 경우
	},
	async (error: AxiosError) => {
		console.warn(error.config?.url + " API response error", {
			response_data: error.response?.data,
			status: error.response?.status,
			request_info: {
				method: error.config?.method,
				url: error.config?.url,
				baseUrl: error.config?.baseURL,
				headers: error.config?.headers,
				params: error.config?.params,
				data: error.config?.data,
			},
		});
		const errorData: Shared.ErrorResponse = error.response?.data as Shared.ErrorResponse;
		alert(`${errorData.error.code}: ${errorData.error.message}`);
		return Promise.reject(error);
	}
);

🚀

3. 타입 설정🔗

👨‍💻
타입을 namespace로 선언하는 이유
타입을
namespace
로 선언하는 이유는 다음과 같습니다. 타입을 선언할 때, 같은 이름의 타입이 이미 선언되어 있을 경우 에러가 발생합니다. 이를 방지하기 위해 namespace를 사용하여 타입을 선언합니다. namespace를 사용하면 같은 이름의 타입이 선언되어 있어도 에러가 발생하지 않습니다. 또한 전역에서 별도의 import 없이 사용할 수 있어 각 컴포넌트에서도 타입 관리가 쉬워집니다. 단, 타입명을 명확하게 지정해 주어야 개발 효율성이 높아지겠죠??

3.1. 공통 타입 설정🔗

서버에서 에러 형태가 다양할 수 있습니다. 예를 들어, 서버에서 에러 형태가 다음과 같을 수 있습니다.
{
  "data": null,
  "error": {
    "code": 400,
    "message": "Bad Request"
  },
  "success": false
}
이런 경우에는 공통 타입을 설정해 줍니다. eslint 설정이 되어 있는 경우에는 최상단에 위치해야 합니다.
src/util/apis/@types/shared.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
namespace Shared {
  export interface ErrorResponse {
    data: null;
    error: {
      code: number;
      message: string;
    };
    success: boolean;
  }
}
추가적으로 페이지네이션, 단순 메시지 response 타입 등도 설정해 주면 좋습니다. 아래는 필자의 프로젝트에서 사용하는 타입입니다.
src/util/apis/@types/shared.ts(예시)
/* eslint-disable @typescript-eslint/no-unused-vars */
namespace Shared {
  export interface ErrorResponse {
    data: null;
    error: {
      code: number;
      message: string;
    };
    success: boolean;
  }
 
  export interface messageResponse {
    message: string;
  }
 
  export interface Pagenation {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    content: any;
    pageable: Pageable;
    totalPages: number;
    totalElements: number;
    last: boolean;
    size: number;
    number: number;
    sort: Sort;
    first: boolean;
    numberOfElements: number;
    empty: boolean;
  }
 
  interface Pageable {
    sort: Sort;
    pageNumber: number;
    pageSize: number;
    offset: number;
    paged: boolean;
    unpaged: boolean;
  }
 
  interface Sort {
    sorted: boolean;
    unsorted: boolean;
    empty: boolean;
  }
}

3.2. Request 타입 설정🔗

jsonplaceholder 가이드에 나온 예시를 참고하여 요청 타입을 설정합니다.
사용할 API의 요청 타입을 설정합니다. GET - https://jsonplaceholder.typicode.com/posts/{id}POST - https://jsonplaceholder.typicode.com/posts 에서 요청을 보내는 경우 아래와 같이 요청 타입을 설정합니다.
제가 주로 사용하는 네이밍 방식입니다. namespace를 통해 요청 타입을 선언한 후 {메소드}{API 이름}{요쳥형태} 형식으로 네이밍합니다.
src/util/apis/@types/test.ts
declare namespace TestRequest {
	export interface GetPostsParams {
		id: number;
	}
	export interface GetPostsQuery {
		userId: number;
	}
	export interface PostPostsBody {
		title: string;
		body: string;
		userId: number;
	}
}

3.3. Response 타입 설정🔗

이번에는 응답 타입을 설정합니다. 요청 타입과 마찬가지로 namespace를 통해 응답 타입을 선언한 후 {메소드}{API 이름}{설명} 형식으로 네이밍합니다.
src/util/apis/@types/test.ts
declare namespace TestResponse {
	export interface GetPosts {
		id: number;
		title: string;
		body: string;
		userId: number;
	}
}

🚀

4. 서비스 파일 작성🔗

4.1. 서비스 파일 작성🔗

이제 axios를 사용하여 서버와 통신하는 서비스 파일을 작성합니다. 필자의 경우 서비스 파일은 apis/service 폴더에 저장합니다.
저는 각 함수에 대한 설명을 주석으로 달아 둡니다. 이 때 각 함수의 설명은 해당 함수의가 사용되는 기능을 설명하는 것이 아니라, 해당 함수가 사용하는 API의 설명을 달아야 합니다.
src/util/apis/service/test.ts
import { API } from "../axios";
 
export const TestService = () => {
  const url = "/posts";
 
  /**
   * 포스트 상세 조회 - id 별 조회
   * @api-doc: https://jsonplaceholder.typicode.com/guide/
   */
  const getPosts = async (
    params: TestRequest.GetPostsParams,
    { userId }: TestRequest.GetPostsQuery,
  ) => {
    const response = (await API.get(`${url}/${params.id}`, {
      params: { userId },
    })) as TestResponse.GetPosts;
    return response;
  };
 
  /**
   * 포스트 전체 조회
   * @api-doc: https://jsonplaceholder.typicode.com/guide/
   */
  const getAllPosts = async () => {
    const response = (await API.get(`${url}`)) as TestResponse.GetPosts[];
    return response;
  };
 
  /**
   * 포스트 생성
   * @api-doc: https://jsonplaceholder.typicode.com/guide/
   */
  const postPosts = async (body: TestRequest.PostPostsBody) => {
    const response = (await API.post(`${url}`, body)) as TestResponse.GetPosts;
    return response;
  };
 
  return {
    getPosts,
    getAllPosts,
    postPosts,
  };
};
 

4.2. index.ts 파일 작성🔗

이제 서비스 파일을 쉽게 임포트 하여 사용할 수 있도록 index.ts 파일을 작성합니다.
src/util/apis/index.ts
export { TestService } from "./service/test";

🚀

5. 테스트 코드 작성🔗

이제 테스트 코드를 작성합니다. 필자의 경우 테스트 코드는 app/test 폴더에 저장합니다.
src/app/test/page.tsx
"use client";
 
import { TestService } from "@/util/apis";
import { useEffect, useState } from "react";
 
export default function TestPage() {
	const [posts, setPosts] = useState<TestResponse.GetPosts>();
	const [getAllPosts, setGetAllPosts] = useState<TestResponse.GetPosts[]>();
	const [postPosts, setPostPosts] = useState<TestResponse.GetPosts>();
 
	useEffect(() => {
		const fetchPosts = async () => {  // 포스트 상세 조회
			const response = await TestService().getPosts({ id: 1 }, { userId: 1 });
			setPosts(response);
		};
		
		const fetchAllPosts = async () => {  // 포스트 전체 조회
			const response = await TestService().getAllPosts();
			setGetAllPosts(response);
		};
 
		const postPosts = async () => {  // 포스트 생성
			const response = await TestService().postPosts({ title: "test123", body: "test123", userId: 1 });
			setPostPosts(response);
		};
 
		fetchPosts();
		fetchAllPosts();
		postPosts();
	}, []);
 
	return (
		<div>
			<div>Test</div>
			<div>{posts?.title}</div>
			{getAllPosts?.map((post) => (
				<div key={post.id}>{post.title}</div>
			))}
			<div>{postPosts?.title}</div>
		</div>
	);
}
테스트 페이지
테스트 페이지

🚀

결론🔗

물론 axios의 기본 기능 또는 fetch를 사용하여 서버와 통신하는 것이 훨씬 빠를 수 있습니다. 하지만 오류가 발생했을 때 오류 처리를 쉽게 할 수 있을지는 의문이죠.
axios를 사용하여 서버와 통신하는 서비스 파일을 작성하는 방법을 알아보았습니다. 이 방법을 통해 서버와 통신하는 서비스 파일을 작성하면 타입 체크와 개발 효율성을 높일 수 있는 효과를 얻을 수 있습니다.

더 생각해 보기🔗

참고🔗