서버나 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
request 인터셉터는 요청을 보내기 전에 요청을 가로채서 요청을 수정할 수 있습니다. 예를 들어, 요청을 보내기 전에 헤더에 토큰을 추가할 수 있습니다.
여기서 어떠한 요청을 보냈는 지 로그를 찍는 코드도 추가해 줍니다. 개발 과정에서는 여기에 로그를 찍어 두어 디버깅을 하는 쪽이 편리했습니다. config의 모든 속성을 찍는 것 보다는 필요한 속성만 골라 찍도록 구성했습니다.
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); });
로 선언하는 이유는 다음과 같습니다. 타입을 선언할 때, 같은 이름의 타입이 이미 선언되어 있을 경우 에러가 발생합니다. 이를 방지하기 위해 namespace를 사용하여 타입을 선언합니다. namespace를 사용하면 같은 이름의 타입이 선언되어 있어도 에러가 발생하지 않습니다. 또한 전역에서 별도의 import 없이 사용할 수 있어 각 컴포넌트에서도 타입 관리가 쉬워집니다. 단, 타입명을 명확하게 지정해 주어야 개발 효율성이 높아지겠죠??
jsonplaceholder 가이드↗에 나온 예시를 참고하여 요청 타입을 설정합니다.
사용할 API의 요청 타입을 설정합니다. GET - https://jsonplaceholder.typicode.com/posts/{id}와 POST - https://jsonplaceholder.typicode.com/posts 에서 요청을 보내는 경우 아래와 같이 요청 타입을 설정합니다.
제가 주로 사용하는 네이밍 방식입니다. namespace를 통해 요청 타입을 선언한 후 {메소드}{API 이름}{요쳥형태} 형식으로 네이밍합니다.
이제 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, };};
물론 axios의 기본 기능 또는 fetch를 사용하여 서버와 통신하는 것이 훨씬 빠를 수 있습니다. 하지만 오류가 발생했을 때 오류 처리를 쉽게 할 수 있을지는 의문이죠.
axios를 사용하여 서버와 통신하는 서비스 파일을 작성하는 방법을 알아보았습니다. 이 방법을 통해 서버와 통신하는 서비스 파일을 작성하면 타입 체크와 개발 효율성을 높일 수 있는 효과를 얻을 수 있습니다.