리액트 API 연동 코드, axios로 클린하게 짜보기

개미·2024년 3월 3일
1
post-thumbnail
post-custom-banner

나는 어떻게 API 연동 코드를 작성하고 있었나

요즘 부쩍 클린한 코드에 관심이 많아지고 있다. 기존에는, 어떻게든 기능만 돌아가면 되는 거 아닌가?라는 생각이 컸었는데 초기에 조금만 더 시간을 들여 고민하면 미래의 나와 나의 동료가 편해진다는 것을 깨달았다.

나의 기존 API 연동 코드는 다음과 같았다. (겨우 반년 전의 코드)

fetch(`${api}/api/userprofile`, {
    headers: {
    	Gauth: getCookie("access"),
    },
})
  .then((response) => response.json())
  .then((data) => {
  setFields(data.fields);
})
  .catch((error) => {
  console.error("fields를 불러오는 데 실패하였습니다.");
  Sentry.captureException(error);
});

모든 API 호출시에 위의 코드 포맷을 반복해서 작성하였다. 헤더명이 무엇이었는지 헷갈려서 다시 찾아보는 경우도 많았고, 실수로 response.json()을 리턴해주지 않아 서비스에 오류가 생기기도 했다.
여기다가 나중에는 상태코드가 401인 경우 refresh token을 서버에게 보내주는 코드도 추가되어야 했다.

이러한 냄새나는 코드를 고칠 필요성을 느꼈다.

🫧클린하게 짜보기

Step1. fetch 대신 axios

fetch는 http 요청을 위해 사용되며, 브라우저에 내장되어 있어 설치할 필요가 없다.

axios 또한 http 요청을 위해 사용되나, 라이브러리로 설치를 해야 사용가능하다. fetch보다 편리하고 깔끔한 코드를 짤 수 있도록 도와준다.

차이점을 살펴보자.

// GET
fetch(`${api}/api/userprofile`)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

axios.get(`${api}/api/userprofile`)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });

// POST
fetch(`${api}/api/userprofile`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: data,
})
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

axios.post(`${api}/api/userprofile`, data)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });

fetch의 경우 .json()으로 응답 데이터를 JSON 형식으로 파싱해야 하지만, axios는 그럴 필요 없이 바로 response.data로 접근 할 수 있다.

특히나 axios의 진가는 POST에서 확인할 수 있는데, POST 요청시 필수적인 method, headers의 Content-Type을 작성할 필요가 없고 body에 매핑할 필요없이 data만 넘기면 되어서 코드가 깔끔해진다.

Step2. axios instance로 공통 config 설정

axios instance을 이용하여 사용자가 api의 기본 config 값을 설정해줄 수 있다.

기본 예시 코드

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

// axios 대신 위에서 지정한 instance 사용
instance.get(`${api}/api/userprofile`)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });

이를 활용하여 서비스에 맞는 공통 사항을 한번에 구현할 수 있었다.

활용 코드

export const api = axios.create({
	baseURL: import.meta.env.VITE_BASE_URL,
	timeout: 10 * 1000,
	headers: {
		Accept: 'application/json',
		'Content-Type': 'application/json',
	},
});

baseURL은 환경변수에서 가져오고, 기본 timeout 값과 headers를 설정해주었다.

Step3. axios instance의 interceptor로 인증 로직 구현

interceptor는 뜻과 같이 http 요청이나 응답을 처리하기 전에 실행되는 함수이다.

기본 예제 코드

// 요청 인터셉터 추가하기
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);
  });

인터셉터를 이용하여 공통적인 인증 로직을 구현할 수 있다.

요청 인터셉터

const onRequestFulfilled = async (config: InternalAxiosRequestConfig) => {
	const accessToken = getAccessToken();

	if (accessToken && config.headers) {
		config.headers['Authorization'] = `Bearer ${accessToken}`;
	}

	return config;
};

const onRequestRejected = (error: AxiosError) => {
	Promise.reject(error);
};

api.interceptors.request.use(onRequestFulfilled, onRequestRejected);

요청 인터셉터에서는 header에 access token을 담는다.

응답 인터셉터

const onResponseFulfilled = (response: AxiosResponse) => response;

const onResponseRejected = async (error: AxiosError) => {
	const requestConfig = error.config;

	if (!requestConfig) return Promise.reject(error);

	if (error.response?.status === 401) {
		const errorCode = (error.response?.data as ApiResponse<null>)?.errorCode;
		if (errorCode === '401/0001') {
			renewRefreshToken();
		} else if (errorCode === '401/0002') {
			const newAccessToken = await renewAccessToken();
			requestConfig.headers['Authorization'] = `Bearer ${newAccessToken}`;

			return api(requestConfig);
		}
	}
};

api.interceptors.response.use(onResponseFulfilled, onResponseRejected);

보통 응답 인터셉터에서는 401 에러가 났을 경우에 토큰 갱신을 요청하도록 한다. 나의 경우에는 서버에서 에러코드가 401/001일 때는 로그인을 다시 해야하고, 401/002일 때는 refresh token을 통한 토큰 갱신을 하도록 요청을 받아서 위와 같이 구현을 했다.

renewRefreshToken와 renewAccessToken 함수는 Github 소스코드을 참조하라

Step4. 코드 분리하기

깔끔한 코드를 작성하기 위해서 적절한 api 폴더 구조를 고민했다. 팀원과 상의 끝에 다음과 같은 폴더 구조를 채택하였다.

└── src/
    ├── apis/
    │   ├── index.ts
    │   ├── account.ts
    │   ├── auth.ts
    │   ├── ...
    ├── hooks/
    │   └── apis/
    │       ├── account.ts
    │       ├── auth.ts
    │       └── ...
    └── models/
        ├── account/
    	│       ├── request/
        │       │    └── accountInfoRequestDTO.ts
    	│       └── response/
        │            └── accountInfoResponseDTO.ts
        ├── auth/
    	│       ├── request/
    	│       └── response/
        └── ...

src/models

// src/models/accounts/response/accountInfoResponseDTO.ts
export type AccountInfoResponseDTO = {
	depositorName: string;
	account: string;
	accountBank: string;
};

도메인 별로 폴더를 다르게 두었고, 도메인 폴더마다 request, response 폴더를 아래에 두었다. 그리고 각각 request, response 타입을 작성하였다. 작성한 타입은 apis와 hooks에서 사용된다.

src/apis/

// src/apis/account.ts
import { AccountInfoRequestDTO } from '@models/account/request/accountInfoRequestDTO';
import { AccountInfoResponseDTO } from '@models/account/response/accountInfoResponseDTO';
import { ApiResponse } from '@type/apiResponse';

import { api } from '.';

export const patchAccountInfo = async (
	data: AccountInfoRequestDTO,
): Promise<ApiResponse<null>> => {
	const response = await api.patch('/api/v1/members/me/account', data);
	return response.data;
};

기본적으로 도메인 별로 파일명을 작성하였다. 각 파일에서는 해당 도메인의 api 호출 함수를 작성하였다.

index.ts에서는 axios instance를 작성하였다.

src/hooks/

// src/hooks/account.ts
import { patchAccountInfo } from '@apis/account';
import { AccountInfoRequestDTO } from '@models/account/request/accountInfoRequestDTO';
import { useMutation } from '@tanstack/react-query';

export const usePatchAccountIfo = (
	successCallback?: () => void,
	errorCallback?: (error: Error) => void,
) => {
	return useMutation({
		mutationFn: (data: AccountInfoRequestDTO) => patchAccountInfo(data),
		onSuccess: successCallback,
		onError: errorCallback,
	});
};

src/hooks/에서도 src/apis/와 동일하게 도메인 별로 파일을 다르게 하였다. Tansack Query를 사용한 훅을 사용하였다.

Tansack Query가 궁금하다면?

결론

매번 반복해서 작성했던 공통적인 API 호출 시 사용되는 코드를 분리함으로써 코드의 가독성이 향상되는 것을 체감할 수 있었다. 성급히 코드를 작성하기 이전에, 다양한 레퍼런스를 참고하며 좋은 코드에 대한 고민이 필요하다는 것을 깨달았다.

소스코드가 궁금하다면?

참조

https://axios-http.com/kr/docs/instance
https://axios-http.com/kr/docs/interceptors

profile
개발자
post-custom-banner

0개의 댓글