Skip to content

도메인별 Api 파일 구분 및 공통 에러 핸들러 생성

Wang Hoeun edited this page Jan 5, 2023 · 2 revisions

1. 반복되는 Api 연결 코드

코넥트 페이지는 팀원을 구하는 글을 올리고, 또 그 글을 다른 사용자가 확인하는 게 주요 기능이기 때문에, 글을 저장하고 불러오는 과정에서 api를 요청하는 일이 빈번하게 발생한다.

다른 로직과 뷰 관련 코드 뿐만 아니라 api 요청코드까지 여러곳에서 반복적으로 쓰이게 되니 한눈에 알아보기 어려운 코드가 되었고, 이를 해결하기 위해 관심사의 분리를 진행하며 api 코드도 데이터 도메인별로 api 파일을 구분하고 공통 에러 핸들러 또한 생성하여 어느 곳에서든 간단하게 작성할 수 있게 만들어 놓았다.

2. 데이터 도메인별로 api 파일 구분

프로젝트에서는 크게 총 4개의 도메인과 그외 기타 등등을 포함하여 총 5개의 도메인이 존재한다.

먼저 팀과 유저 보드에서 내가 쓴 글을 저장하거나 불러오는 요청을 해야하기에

팀(team) 그리고 개인(user) 관련 도메인.

그리고 댓글과 대댓글 요청은 모든 보드에서 동일하게 저장하고 불러오는 과정이 요청되어지기 때문에

댓글(comment) 관련 도메인.

그리고 로그인 진행 시 실제 사용자의 로그인 한 정보를 저장하고 입력한 정보를 불러와야 하는 경우가 필요하기에

사용자(auth)관련 도메인.

이렇게 총 4가지와, 그외는 프로필 사진 관련 요청 기술스택 사진 불러오기와 같이

그 외 기타의 api 도메인

까지 총 5개의 도메인으로 나누어져 있다.

📦api
 ┣ 📂instance
 ┃ ┣ 📜privateApiInstance.js
 ┃ ┣ 📜publicApiInstance.js
 ┃ ┗ 📜responseHandler.js
 ┣ 📜auth.api.js
 ┣ 📜comment.api.js
 ┣ 📜etc.api.js
 ┣ 📜team.api.js
 ┗ 📜user.api.js

api와 관련된 폴더는 이와 같이 구성되어져 있다. instance 폴더를 제외한 나머지 파일들을 보면 위에서 설명한 바와 같이 총 5개의 도메인으로 나누어져 있는 것을 볼 수 있다.

3. instance 폴더

코넥트는 사용자가 로그인을 하여 서비스를 이용하는 웹페이지이다.

그래서 글 작성이나 내 프로필 수정과 같은 로그인을 한 후 진행이 가능한 서비스가 존재하고,

팀 / 개인 소개글을 확인할 수 있는 페이지와 같이 로그인을 진행하지 않아도 이용 가능한 서비스가 존재한다.

우리는 각각을 private(로그인 필요O) / public(로그인 필요X) 으로 나누었고 각각의 api instance도 따로 만들어주었다.

publicApiInsatance.js

import axios from 'axios';
import { ROOT_API_URL } from 'constant/api.constant';
import { successHandler, errorHandler } from './responseHandler';

const publicApiInstance = axios.create({
  baseURL: ROOT_API_URL,
});

publicApiInstance.defaults.timeout = 2500;

publicApiInstance.interceptors.request.use(
  (config) => {
    return config;
  },
  (error) => {
    // 요청 에러가 발생했을 때 수행할 로직
    console.log(error); // 디버깅
    return error;
  },
);

publicApiInstance.interceptors.response.use(successHandler, errorHandler);

export default publicApiInstance;

baseURL을 상수처리하여 baseURL로 시작하는 publicApiInstance를 axios.create 해주었고, request로 data를 api연결 요청 할 수 있게 만들고, 연결이 잘 되었다면, responseHandler안에 있는 successHandler가 응답되게, 에러가 발생하였다면 errorHandler가 응답되도록 작성하였다.

privateApiInstance.js

import axios from 'axios';
import { ROOT_API_URL, TOKEN } from 'constant/api.constant';
import { handleToken } from 'service/auth';
import { successHandler, errorHandler } from './responseHandler';

const privateApiInstance = axios.create({
  baseURL: ROOT_API_URL,
});

privateApiInstance.defaults.timeout = 2500;

const setAcessTokenInRequestConfig = (config) => {
  const accessToken = handleToken.getAccessToken();
  const refreshToken = handleToken.getRefreshToken();
  // 요청을 보내기 전에 수행할 로직

  if (!config?.headers || !accessToken || !refreshToken) {
    return config;
  }
  config.headers.Authorization = accessToken;
  config.headers[TOKEN.REFRESH] = refreshToken;
  return config;
};

privateApiInstance.interceptors.request.use(
  (config) => {
    config.headers['Content-Type'] = 'application/json; charset=utf-8';
    const newConfig = setAcessTokenInRequestConfig(config);
    return newConfig;
  },
  (error) => {
    // 요청 에러가 발생했을 때 수행할 로직
    console.log(error); // 디버깅
    return error;
  },
);

privateApiInstance.interceptors.response.use(successHandler, errorHandler);

export default privateApiInstance;

publicApiInstance보다는 조금 더 로직이 추가 되었는데, privateApiInstance는 로그인한 사용자에게만 부여되는 token의 존재 여부도 판단해줘야 하기 때문에, 토큰이 존재할 때만 private서비스를 이용할 수 있게 작성하였고, public에서와 동일하게 response문에 각각 successHandler, errorHandler를 넘겨주어 실행 되게 작성하였다.

reponseHandler 내부에서의 successHandler

export function successHandler(response) {
  const { data, headers } = response;
  return { ...data, headers };
}

request요청을 보내는 data와 header를 포함하고 있다.

reponseHandler 내부에서의 errorHandler

export function errorHandler(error) {
  // 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
  // 응답 오류가 있는 작업 수행
  if (error.response) {
    // 요청이 이루어졌으며 서버가 2xx의 범위를 벗어나는 상태 코드로 응답했습니다.

    const {
      response: { data: apiData, status },
    } = error;

    // 서버에서 설정한 커스텀 에러 타입 키값들
    // code, message, status
    const hasCode = apiData && apiData.hasOwnProperty('code');
    const hasMessage = apiData && apiData.hasOwnProperty('message');
    const hasStatus = apiData && apiData.hasOwnProperty('status');

    // 서버에서 보낸 custom 에러 메세지가 있을 경우 해당 메세지를 에러 메세지로 전달
    if (hasCode && hasMessage && hasStatus) {
      return Promise.reject(new HttpError(apiData.message, status));
    }

    return Promise.reject(new HttpError(response.message, status));
  }
  if (error.request) {
    // 요청이 이루어 졌으나 응답을 받지 못했습니다.
    // `error.request`는 브라우저의 XMLHttpRequest 인스턴스 또는
    // Node.js의 http.ClientRequest 인스턴스입니다.
    console.log(error.request);
  } else {
    // 오류를 발생시킨 요청을 설정하는 중에 문제가 발생했습니다.
    console.log('Error', error.message);
  }

  return Promise.reject(new Error('요청 도중 에러 발생'));
}

200번대 범위를 벗어나는 상태 코드로 응답했을 때, 서버에서 보내오는 에러 메세지를 콘솔에 띄워주도록 하였고, 요청이 이루어졌으나 응답을 받지 못했는지 vs 요청 자체가 오류가 났는지를 구분하여 각각 에러 메세지를 도출하였다.

위와 같이 successHandler와 errorHandler 각각을 만들어주고 private경우와 public경우를 나눠 Instance를 생성하였고, 총 5개의 도메인으로 나누어진 API 연결 코드들을 이를 활용하여 작성하였다.

4. 도메인 작성 예시

team

import { API } from 'constant/api.constant';
import privateApiInstance from './instance/privateApiInstance';
import publicApiInstance from './instance/publicApiInstance';

const teamApi = {
  GET_TEAM_ARR(config) {
    return publicApiInstance({
      url: API.TEAM.INDEX,
      method: 'get',
      ...config,
    });
  },
  GET_TEAM_DETAIL({ id }) {
    return publicApiInstance({
      url: `${API.TEAM.INDEX}/${id}`,
      method: 'get',
    });
  },
  GET_TEAM_LIKES() {
    return publicApiInstance({
      url: API.TEAM.LIKE,
      method: 'get',
    });
  },
  GET_TEAM_READS() {
    return publicApiInstance({
      url: API.TEAM.READS,
      method: 'get',
    });
  },
  POST_TEAM_POST({ data }) {
    return privateApiInstance({
      url: API.TEAM.INDEX,
      method: 'post',
      data,
    });
  },
  EDIT_TEAM_POST({ id, data }) {
    return privateApiInstance({
      url: `${API.TEAM.INDEX}/${id}`,
      method: 'patch',
      data,
    });
  },
  ADD_TEAM_LIKE({ id }) {
    return privateApiInstance({
      url: `${API.TEAM.LIKE}/${id}`,
      method: 'patch',
    });
  },
  DELETE_TEAM_LIKE({ id }) {
    return privateApiInstance({
      url: `${API.TEAM.UNLIKE}/${id}`,
      method: 'delete',
    });
  },
};

export default teamApi;

작성된 글을 불러오는 get요청 / 글 작성하여 올리는 post요청 / 수정하는 patch 요청 / 좋아요 등록,삭제 요청 으로 이루어져있다.

5. 우리가 얻은 효과

api요청 코드들은 url, method, params로 구성하여 한눈에 알아보기 쉽도록 작성되었고, 이렇게 상수화,폴더화 시키니 리펙토링도 더욱더 간편하고 다른 페이지에서의 api연결 요청 코드도 짧아지는 좋은 효과를 얻을 수 있었다.

6. fetch가 아닌 axios를 사용한 이유

axios란?

axios는 브라우저, Node.js를 위한

Promise API를 활용하는 HTTP 비동기 통신 라이브러리이다.

(백엔드와 프론트엔드와 통신을 쉽게하기 위해 AJAX도 더불어 사용하기도 한다.)

axios 의 특징

  • 운영 환경에 따라 브라우저의 XMLHttpRequest 객체 또는 Node.js의 HTTP API 사용
  • Promise(ES6) API 사용
  • 요청과 응답 데이터의 변형
  • HTTP 요청 취소 및 요청과 응답을 JSON 형태로 자동 변경

axios 사용법

  • Axios 다운로드
    • yarn add axios / npm i axios
  • HTTP Methods
    • GET : 데이터를 가져와서 보여줄 때 사용
    • POST : 새로운 리소스를 생성할 때 사용
    • PATCH / PUT : 데이터베이스에 저장되어 있는 내용을 갱신할 때
    • DELETE : 데이터베이스에 저장되어 있는 내용을 삭제할 때

fetch vs axios

일반적으로 자바스크립트에서 API를 연동하기 위해서는 fetch api를 사용하곤 한다.

react에서도 자바스크립트 built-in 라이브러리 중 하나인 fetch api를 사용하는데, 코넥트 프로젝트에서는 axios를 사용하였다. 이유는 fetch 와 axios 차이점에 있는데, 그 차이점들에 대해 살펴보겠다.

compatibility?

fetch : Chrome 42+, FireFox 39+, Edge 14+, Safari 10.1+
axios : fetch보다  많은 브라우저에도 지원가능함

위에서 보는것과 같이 axios는 fetch에 비해 많은 브라우저를 지원해준다.

data types

fetch : body로 전달되며, 문자열형태로 전달됨.
axios : data로 전달되며, 자동으로 JSON으로 변환함.

fetch 함수는 응답을 promise 객체로 가져다주고, axios는 객체의 데이터를 바로 json으로 받아온다.

이는 key값을 지정할때 등 여러곳에서 유용하게 쓰일 수 있는 장점이 있다.

Response state

axios는 응답에 status code와 status text 두개가 전달되지만, fetch는 ‘OK’ 속성이 포함됐는지, 아닌지만 판별한다.

따라서 오류가 발생하거나, 예외 등으로 인한 '200' status code가 아닌 다른 코드가 발생했을 때 fetch를 쓰면 단순히 오류가 떴구나~ 정도만 알 수 있고 axios는 코드를 통해 디버깅을 하기 전, 어느정도 유추할 수 있다.

CSRF(XSRF) Issue

손쉽게 Input 값을 바꾼다거나...특정 element를 삽입하여 악성 작업을 서버로 하여금 유도시켜 부적절한 행동이 발생시키는데, axios는 그것을 자체 설정을 통해 방지할 수 있다.

axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';

위와 같이 CSRF Token을 저장할 쿠키와 헤더의 이름만 설정해주면 된다. 따라서 서버는 단순히 토큰이 담겨있는지만 요청으로부터 확인하고 실행할 요청을 거를 수 있다.

반면 fetch는 이러한 보안적인 기능이 없다.

Timeout

서버에 트래픽이 몰렸거나, 혹은 서버가 꺼졌을때 응답이 오지 않는 경우가 있는데, axios는 Timeout을 지정해 줄 수 있다.

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

Interceptor

이것이 우리가 axios를 선택하게 된 가장 큰 이유 중 하나다.

axios는 요청에 대한 Intercept를 제공한다.

intercept는

axios.post('/v0/api/login',body)
...
axios.post('/v0/api/register',body)
...
axios.get('/v0/api/getSession')

이런 식으로 매번 어떠한 작업에 대한 요청을 작성하기 보다

const instance = axios.create({
 baseURL:'v0/api',
 timeout:1000
})

이렇게 axios를 객체를 만들고

instance.interceptors.request.use(
  function (config) {
    config.headers["Content-Type"] = "application/json; charset=utf-8";
    config.headers["Authorization"] = " 토큰 값";
    return config;
  },
  function (error) {
    console.log(error);
    return Promise.reject(error);
  }
);

요청에 대한 type, 인가 토큰 값 등을 설정하고 error의 예외 처리를 적용하고

instance.interceptors.response.use(
  function (response) {
    console.log(response);
    return response.data.data;
  },
  function (error) {
    errorController(error);
  }
);

응답이 발생하면 그에 맞는 예외처리를 해 놓은 상태로 두고

export default instance를 통해 axios객체를 export 시키면 다른 파일에서 가져다 사용한다.

이러한 구성을 통해 기준이 되는 api의 헤더나 타입을 통일시켜 매번 axios.get / [axios.post](http://axios.post) 등을 써가며 코드의 길이를 늘려도 되지 않는 효과를 제공해주곤 합니다.

실제 프로젝트에 적용할 당시 정리한 Notion 페이지 링크⇒

[axios interceptor 활용](https://www.notion.so/axios-interceptor-f3e69c4862af4c598479356e5f735b7e)

Instance

위 예시 코드에서 본 것과 같이 axios는 사용자 지정 config로 새로운 인스턴스를 만들 수 있다.(axios객체를 생성한 것이 instance라고 할 수 있다.)

좀 더 자세한 설명은 [axios 공식문서](https://axios-http.com/kr/docs/instance)에서도 찾아 볼 수 있다.

이 또한 우리가 axios를 선택하게 된 가장 큰 이유 중 하나다.

private과 public 관련 instance를 각각만들어 코드를 변수와 하였고, 가독성을 높일 수 있었다.

이는 fetch에선 제공 되지 않는다.


위와 같은 기능들 때문에 fetch대신 axios를 선택하여 진행하는 것이 더 좋을 것 같다고 판단했고, axios를 선택 하였다.

참고자료

https://velog.io/@leitmotif/fetch-vs-axios

Clone this wiki locally