next.js | fetchWrapper로 공통 인증 에러 잡기

dobby·2024년 6월 10일
0

초보 프론트 개발자라면 항상 고민하는게 있을 것 같다.
공통 에러를 어떻게 잡지?
혹은
토큰 만료와 같은 인증 에러는 토큰 재발급 후 기존 요청을 재요청해야 하는데 어떻게 구조를 잡아야하지?

나는 초보니까 당연히 이 고민을 했다.
카카오테크캠퍼스에서 프로젝트를 진행하면서 이 고민을 하게 되었는데, 당시엔 시간이 너무 촉박해서 깊게 고민할 수 없었다.
그래서 단순 무식하게 api를 요청하는 곳의 catch 영역에서 인증 에러 발생 시 똑같은 api를 한 번 더 작성하는 식으로 끝냈다.

하지만!
그런 방법은 유지보수도 그렇지만 효율적이지 않은 코드이다.
그땐 아무것도 몰랐기 때문에 '이렇게 하면 안될거 같은데..'라는 생각만 들었지 마땅한 방법이 떠오르지 않았지만, 이젠 어느정도 안다!

한 곳에서 fetch 호출하기

next.js는 api 요청시 fetch를 사용할 것을 권장하고 있다.
그렇기에 fetch를 기준으로 작성하겠다.
참고로 내 방법이 좋은 방법이 아닐 수도 있다.
충분히 고민해 본 후 더 적절한 방법을 사용하는 것이 좋다!!
(axios는 제공하는 middleware를 통해 잡을 수 있다.)

api를 호출할 때 fetch를 사용한다.
호출할 때 파일마다 fetch를 작성해준다면, 공통 에러를 잡기 쉽지 않다.
그렇기에 파일 하나에서 fetch 요청을 수행하도록 하고, 그 곳에서 공통 에러를 잡는 것이 좋다.
그리고 나는 이것을 fetchWrapper로 이름 지었다.

fetchWrapper 만들기

어떤 식으로 만들어야 할까 고민하다, 아래 글을 발견하고 감을 잡았다.
Fetch Wrapper

글에서 알려주는 fetchWrapper 코드는 아래와 같다.

class FetchWrapper {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async get(url) {
    const response = await fetch(`${this.baseUrl}${url}`);
    return response.json();
  }

  async put(url, data) {
    const response = await fetch(`${this.baseUrl}${url}`, {
      method: 'PUT',
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
      },
    });
    return response.json();
  }

  async post(url, data) {
    const response = await fetch(`${this.baseUrl}${url}`, {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
      },
    });
    return response.json();
  }

  async delete(url) {
    const response = await fetch(`${this.baseUrl}${url}`, {
      method: 'DELETE',
    });
    return response.json();
  }
}

const fetchWrapper = new FetchWrapper('https://my-api.com');

// GET request
fetchWrapper
  .get('/users')
  .then((users) => console.log(users))
  .catch((error) => console.error(error));

// PUT request
fetchWrapper
  .put('/users/1', { name: 'John', age: 30 })
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

// POST request
fetchWrapper
  .post('/users', { name: 'Jane', age: 25 })
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

// DELETE request
fetchWrapper
  .delete('/users/1')
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

위 코드처럼 클래스를 사용하는 것이 baseUrl을 인스턴스의 상태로 저장하기 용이할 것 같았고, 유지보수하기에도 편할 것 같았다.


코드를 참고하여 내 방식대로 수정해봤다.

// src/lib/fetchWrapper.ts
import showToast from '@/utils/showToast';
import { getToken } from './getToken';
import { getReissuanceToken } from './getReissuanceToken';

const tokenErrorHandler = async (response: any) => {
  const { error } = await response.json();
  switch (error.code) {
    case 1002:
      await getReissuanceToken();
      break;
    case 1201:
      await getToken();
      break;
    default:
      showToast('토큰 오류가 발생했습니다.', 'error');
      break;
  }
};

class FetchWrapper {
  baseUrl = '';

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async call(url: string, fetchNext: any, retry = 3) {
    const response = await fetch(this.baseUrl + url, fetchNext);
    const result = await response.json();

    if (!response.ok) {
      // 인증 자격에 관한 오류 처리
      if (response.status === 401) {
        await tokenErrorHandler(response);
        // 재발급 후 재요청
        if (retry !== 0) {
          this.call(url, fetchNext, retry - 1);
        }
      } else {
        // 인증 외 오류는 호출한 곳에서 처리
        return result;
      }
    }

    return result;
  }
}

const fetchWrapper = new FetchWrapper(`${process.env.NEXT_PUBLIC_BASE_URL}` || '');

export default fetchWrapper;

인스턴스 생성 시 baseUrl을 넘겨주어 상태로 저장하도록 했다.
또한, call이라는 공통 함수를 만들어 공통 함수 내에서 모든 api 요청을 처리할 수 있도록 하고, 인증 에러가 발생하면 토큰을 재발급하거나, 새로 발급하는 등 적절하게 처리하도록 했다.

토큰 재발급/생성 후엔 기존 요청을 재요청 해주어야 하므로 call을 재귀호출하여 기존 요청을 재시도하도록 했다.

재귀호출은 무한 반복의 오류가 발생할 수 있으므로, retry를 통해 제어하도록 했다.

이렇게 하면 인증 에러는 한 곳에서 처리할 수 있고, 그 외 오류나 정상 응답은 호출된 곳에서 처리할 수 있게 된다.

인증 에러가 아닌 다른 공통 에러도 fetchWrapper 내에서 처리해주면 된다.

fetchNext 부분은 headers, body 등의 내용이 들어간다.
headers에 토큰이 필요한 요청이 있고 필요없는 요청이 있어서 하나로 구분하기엔 힘들 것 같아 이 부분은 호출하는 곳에서 받아 쓰기로 했다.


api 호출 파일에서의 사용

import fetchWrapper from '@/lib/fetchWrapper';

...
const res = await fetchWrapper.call(`요청 url`, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${tokenManager.getToken()}`,
    },
    credentials: 'include',
    body: JSON.stringify({
      nickname: info.nickname ? info.nickname : '',
      photoNum: info.id ? info.id : 1,
      type: info.role,
      voteType: null,
    }),
  });

  if (res.success === false) {
    if (res.error.code === 1001) {
      if (info.id === null) {
        showToast('프로필을 선택해주세요.', 'error');
      } else if (info.nickname === null) {
        showToast('닉네임을 입력해주세요.', 'error');
      } else if (info.role !== 'OBSERVER' && info.role !== 'PROS' && info.role !== 'CONS') {
        showToast('허용되지 않는 입장 타입 입니다.', 'error');
      } else {
        showToast('입장 실패했습니다.\n 다시 시도해주세요.', 'error');
      }
    } else if (res.error.code === 1004) {
      showToast('이미 참여한 아고라입니다.', 'error');
    } else if (res.error.code === 2000) {
      showToast('선택한 타입의 인원이 꽉 찼습니다.', 'error');
    }
    return null;
  }

  const result = res.response;
  return result;

공통 에러는 fetchWrapper에서 처리하고, api마다 발생할 수 있는 에러 응답이 달라지는 것은 호출한 곳에서 처리하도록 했다.

우리 팀은 http 상태코드 뿐만 아니라, code를 더 세분화하여 에러를 정리했다.
그게 1001, 1004 등의 코드이다.


아직 배우는 중이기 때문에, 내 방식이 올바른 방식이라고는 딱 잘라서 말할 수는 없다.
하지만 카텍캠 때보단 성장한 것 같아서 뿌듯하다!

profile
성장통을 겪고 있습니다.

0개의 댓글