React API 스웨거(spring swagger)와 axios 최적화로 관리하는 방법, 커스텀 인스턴스 객체 모듈화 + 객체리터럴 + zus

2024. 2. 29. 13:23_Project/GDSC_BLUE_MOSAIC

728x90

 

 

30초만에 API를 연결하고 싶다면? 이 방법을 사용해보자.

 

#커스텀인스턴스 #객체리터럴

코드 복붙은 맨 아래에
 
기본코드

 
실사용예

 
 
위 코드를 사용하는 방법


Swagger API 명세서

 
Spring swagger는 API 명세서 작성을 자동화하고 테스트, 문서화까지 할 수 있는 편리한 오픈 소스 소프트웨어다. 포스트맨과 같은 툴로 API를 확인하는 것이 아닌 웹사이트에서 버튼 딸깍으로 쉽게 테스트를 할 수 있으며, API가 controller별로 정리되어있으니 프론트는 백엔드로부터 스웨거 명세서를 받았다면 편리한 유지보수를 위해서 문서 그대로 작성하는 것이 좋다. 따라서 controller 종류별로 API를 정리하도록 하자. 아래의 사진을 자세히보자.
 

 
추후에 react-query를 적용하기 위해 useFriendQuery.ts 라는 이름을 적용했을 뿐, 단순한 api이다. 스웨거에 맞춰 friend 컨트롤러에 맞는 FriendApis를 만들어주면된다. 여기선 api 하나하나 export를 하지 않고, FriendApis를 객체화해서 하나만 보낸다. 따라서 Friend-controller의 API를 사용한다면, FriendApis 받아오면 된다. 
 
 

axios custom instance 생성

그렇다면 기존 axios와 차이점은 무엇인가? 그동안 gpt의 도움을 받아 작성한 기본적인 코드는 아래에 접은 글과 같다. axios.RESTAPI('주소', requestbody); 를 비동기코드다 보니까 렌더링 되고난 후에 데이터를 사용할 수 있게 await로 받고, async를 사용하기 위해 이를 함수화해서 useEffect까지 사용하는, 하나 사용하려고 되게 번거로운 과정을 처리해야한다.
 

// POST 요청 전송
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

 

더보기
import React, { useEffect, useState } from 'react';
import axios from 'axios';

const MyComponent = () => {
  const [responseData, setResponseData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // 보낼 데이터
        const requestBody = {
          title: 'My Post',
          body: 'This is the content of my post.',
          userId: 1,
        };

        // POST 요청 보내기
        const response = await axios.post('https://jsonplaceholder.typicode.com/posts', requestBody);

        // 성공적으로 응답 받았을 때의 처리
        setResponseData(response.data);
      } catch (error) {
        // 에러 발생 시의 처리
        setError(error.message);
      }
    };

    fetchData();
  }, []); // 빈 배열을 전달하여 컴포넌트가 처음 렌더링될 때만 useEffect가 실행되도록 함

  return (
    <div>
      {error ? (
        <p>Error: {error}</p>
      ) : responseData ? (
        <div>
          <h1>{responseData.title}</h1>
          <p>{responseData.body}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

export default MyComponent;

 
이런 코드를 API가 추가되거나 변경될 때 마다, 수작업으로 입력하는 것은 매우매우 번거로운 일이며, 가장 큰 문제는 복사 붙여넣기를 했더라해도 기존 코드와 알잘딱으로 돌아갈리가,,, 없다. 스웨거도 거리가 멀다. 결국엔 개발자가 또 수정하고 리팩토링해야하는 리소스 낭비가 생긴다. 만약, 복사 붙여넣기로 코드가 독립적으로 유연하게 추가 및 삭제가 가능하다면?
 
axios에는 instance 생성 기능을 제공한다. 바로 axios.create를 통해 baseURL:을 등록할 수 있고 withCredientials라던가 config 별도의 설정을 미리할 수 있다.
 
공식문서
https://axios-http.com/kr/docs/intro

시작하기 | Axios Docs

시작하기 브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리 Axios란? Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트 입니다. 그것은 동형 입니다(동일한 코

axios-http.com

 
공식문서를 보고 자주 사용하는 config 옵션을 정리해본다. 우리는 여기서 baseURL이나 url만 필수로 사용한다.

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

 
// `url`은 요청에 사용될 서버 URL입니다. url: '/user',
// `method`는 요청을 생성할때 사용되는 메소드입니다. method: 'get', // 기본값
// `url`이 절대값이 아닌 경우 `baseURL`은 URL 앞에 붙습니다. // 상대적인 URL을 인스턴스 메서드에 전달하려면 `baseURL`을 설정하는 것은 편리합니다. baseURL: 'https://some-domain.com/api',
// `headers`는 사용자 지정 헤더입니다. headers: {'X-Requested-With': 'XMLHttpRequest'},
// `timeout`은 요청이 시간 초과되기 전의 시간(밀리초)을 지정합니다. // 요청이 `timeout`보다 오래 걸리면 요청이 중단됩니다. timeout: 1000, // 기본값은 `0` (타임아웃 없음) // `withCredentials`은 자격 증명을 사용하여 사이트 간 액세스 제어 요청을 해야 하는지 여부를 나타냅니다. withCredentials: false, // 기본값
 
이런 custom instance는 생성한 경우 한 객체로써 관리할 수 있다. config 기본값을 통해 모든 요청에 적용될 config 기본값을 지정할 수 있고, 커스텀 인스턴스에도 따로 적용 및 우선순위까지 매길 수 있다. 이건 심화과정이니 알아두기만 하자.
 
전역 axios 기본값

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

 
커스텀 인스턴스 기본값

// 인스턴스를 생성할때 config 기본값 설정하기
const instance = axios.create({
  baseURL: 'https://api.example.com'
});

// 인스턴스를 만든 후 기본값 변경하기
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

 
 
 

객체리터럴 : axios custom instance 적용

그렇다면, instance를 생성하는 것까지 이해했다면 이를 어떻게 적용하는지 알아보자. Apis는 우리가 평소에 보는 코드와 다르게 구성되어있다. 키와 값의 쌍으로 구성된, key와 그 속성 value로 이루어진 객체 리터럴 방식으로 관리한다.
 

객체 리터럴 예시

// 키-값 쌍을 포함하는 객체
const person = {
  name: 'John',
  age: 30,
  isStudent: false,
  hobbies: ['reading', 'coding', 'traveling'],
  address: {
    city: 'New York',
    country: 'USA'
  },
  sayHello: function() {
    console.log('Hello!');
  },
};

 
 
이제 슬슬 코드가 눈에 들어온다면 대단하다! 단순하게 FriendApis는 instance-axios.create, add-함수... 로만 구성되어있다. 그렇다면 코드를 추가하고 싶다면? 키-값쌍인 add ~ 부터 한 개를 다 복사해서 아래에 붙여넣고, 고칠 부분은 아래에 하늘색으로 표시해 두었다. 함수명(add), post, data, 주석 정도? 수정할 곳이 매우 적어서 간단하다. 조금만 익숙해지면 누구나 api를 재사용할 수 있다. 

 
 
 

깔끔한 주석으로 외부에서 적용 및 확인하기

그렇다면 어떻게 가져와서 사용할까? 아주아주 간단하다. 아래는 기본 예시이다. FriendApis.add() 이런식으로 API를 불러온다.

많이 생략된 기본 코드다.

 
Apis를 가져온다음에 필요한 부분에 넣으면된다. 비동기 함수라 값을 가져오고 적용하고 싶다면 await을 사용한다. 아래는 실사용 예시이다.
 
함수만 실행하면 되는 경우

 
값을 가져와서 zustand store나 useState에 넣어야하는 경우

그렇다면 이제부터 실사용 꿀팁, 두 주석의 차이를 아는가?

 
 
/** 이렇게 작성된 주석은 외부에서 확인할 수 있다 */ add 에 마우스 커서를 올리면 함수의 설명이 보인다.

 
FriendApis에 커서를 올리면, Friend-controller에 있는 항목들이 모두 보인다. 스웨거나 다름이 없다.

 
 

zustand Store 연결하기

이제 API의 연결은 모두 끝났다. 그렇다면 zustand에 어떻게 간단하게 연결할 수 있을지 알아보자. FriendStore.ts을 가져왔으며 필자는 zustand의 useStore, set과 getState()를 아주 애용한다. 배포 전인 개발자 모드에서만 devtools 적용을 하기 위해 아래의 코드를 사용했다. 코드를 전체 볼 필요는 없고, friendId, friendName, profileImageUrl이 있으며 이는 setFriendId를 통해 변경할 수 있다 정도만 알면된다.

 

더보기
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

export interface FriendInfo {
  friendId: number;
  friendName: string;
  profileImageUrl: string;
  setFriendId: (friendId: FriendInfo['friendId']) => void;
  setFriendName: (friendName: FriendInfo['friendName']) => void;
  setProfileImageUrl: (profileImageUrl: FriendInfo['profileImageUrl']) => void;
}

const createFriendInfoStore = (set) => ({
  friendId: 0,
  friendName: '',
  profileImageUrl: '',
  setFriendId: (friendId: number) => set({ friendId }),
  setFriendName: (friendName: string) => set({ friendName }),
  setProfileImageUrl: (profileImageUrl: string) => set({ profileImageUrl }),
});

let FriendInfoStoreTemp;

//devtools
if (import.meta.env.DEV) {
  FriendInfoStoreTemp = create<FriendInfo>()(devtools(createFriendInfoStore, { name: 'friendInfo' }));
} else {
  FriendInfoStoreTemp = create<FriendInfo>()(createFriendInfoStore);
}

export const FriendInfoStore = FriendInfoStoreTemp;

 
해당 코드를 사용하면 아래처럼 자동완성이 뜬다. setFriendId()에 값만 등록하면 devtools에 userId 값이 변화하는 것이 보일 것이다. useStore을 통해 Store을 가져와서 .set에 연결해서 값을 변경한다. 다만 useStore을 사용하면 hook pattern을 주의해야한다. 안쪽 컴포넌트에서 값을 변경하려고 하면 훅패턴 순서 오류가 자주 발생한다. 따라서 페이지 단에서 API를 관리하는 것이 좋다. 설계부터 이를 유의하자.

  const friendInfo = useStore(FriendInfoStore);

 
따라서 api의 request용으로 값을 가져올 때는useStore로 가져온다면 훅패턴순서 오류가 발생하니 useStore을 api에서 사용하면 안 된다. 따라서 getState() 사용해서 항상 최근 값을 가져온다.
 

오류 발생

 
 
useStore을 사용하지 않고 getState()를 사용하고 자동완성으로 뜨는 것을 확인하자.

 
 
참고로 FriendApis의 전역변수로 data{} 를 미리 만들고 그 안에서 getState()를 하면 옛날 데이터를 가져오더라... 되도록 request body 같이 api 호출과 가까운 곳에서 getState()를 사용해야 호출 즉시 함수를 실행하여 최신데이터를 가져오므로 수상한 버그를 방지할 수 있다.

 
 
이제 zustand도 연결했다면 프론트와 백엔드의 API 연결은 완료다! 스웨거 코드가 모두 개별의 객체로 작동하므로 추가 및 수정을 해도 다른 코드에 영향은 없다. BaseUrl을 배포시에는 BASE_URL과 같은 변수로 변경하면 호출 URL을 한번에 바꿀 수 있다.


 
지금은 더 디벨롭해서 싱글톤패턴을 적용하고 있다. 자바스크립트의 평가가 1번만 이루어진다는 점을 이용해서 instance는 utils로 코드를 분리하고, api만 기존처럼 관리하되 객체리터럴이 아닌 class로 정의하고, 리액트 쿼리에 이를 조합하여 사용하고 있다. 이 또한 발전과정의 단계라고 느꼈다. 추후에 글을 업뎃할 수도…?


기본코드

import axios from 'axios';
import { UserInfoStore } from '../stores/UserInfoStore';

export const DefaultApis = {
  instance: axios.create({
    baseURL: 'http://localhost:8080/api',
    withCredentials: true,
  }),

  /** get */
  get: async () => {
    try {
      const res = await DefaultApis.instance.get('/', { });
      // console.log(res);
      return res.data;
    } catch (error) {
      console.error('Error fetching:', error);
      throw error;
    }
  },

  /** post */
  post: async () => {
    try {
      const res = await DefaultApis.instance.post('/update', { userId: UserInfoStore.getState().userId });
      return res.data;
    } catch (error) {
      console.error('Error fetching: ', error);
      throw error;
    }
  },
};