Fetch API 요청, 응답 가로채기 (Interceptor)

October 23, 2022

axios는 interceptor에 로직을 추가하여 api 요청, 응답을 전/후처리할 수 있습니다. 팀 프로젝트에서는 fetch를 이용하고 있어 그동안 매 api 요청, 응답에 대하여 직접 로직을 작성해 불편함이 있어 fetch로 interceptor를 구현해보았습니다.

API 요청 함수

간단한 API 요청 함수입니다. 응답 객체에 body를 추가하고 성공, 실패에 따라 반환, throw를 하는 간단한 구조입니다. RequestError는 응답 객체를 담기 위한 커스텀 에러 클래스입니다.

const request = (baseUrl: string) => async <SuccessBody, ErrorBody>(
  url: string,
  options: RequestInit
) => {
  const response = await fetch(url, options)

  if (!response.ok) {
    const body = (await response.json().catch(() => ({}))) as SuccessBody
    const responseWithBody = { ...response, body }
    throw new RequestError(responseWithBody)
  }

  const body = (await response.json().catch(() => ({}))) as ErrorBody
  const responseWithBody = { ...response, body }
  return responseWithBody
}

fetch api를 사용해 상태코드에 따라 성공, 실패를 처리하는 로직을 가지고 있습니다. 여기서 interceptor가 전/후처리를 해야할 곳은 3곳입니다.

  • 요청 전
  • 응답 후 실패 시
  • 응답 후 성공 시

요청 가로채기

request 함수는 요청 시 fetch에 url과 options를 전달합니다. 먼저 이 두 값을 전처리하는 로직을 넣어야합니다. onRequest라는 함수를 추가해 요청 시 url과 options에 대한 전처리 로직을 추가합니다.

const interceptedRequest = interceptor.onRequest(url, options)
const response = await fetch(interceptedRequest.url, interceptedRequest.options)

응답 가로채기

fetch api는 상태코드로 요청의 성공여부를 알 수 있는 ok 프로퍼티를 제공합니다. response 객체를 ok가 true인 경우 onSuccess, false인 경우 onError 함수를 거치도록 합니다.

if (!response.ok) {
  const body = (await response.json().catch(() => ({}))) as SuccessBody
  const responseWithBody = { ...response, body }
  throw new RequestError(interceptor.onError(responseWithBody))
}

const body = (await response.json().catch(() => ({}))) as ErrorBody
const responseWithBody = { ...response, body }
return interceptor.onSuccess(responseWithBody)

useInterceptor 훅

accessToken은 문자열, 즉 원시값이므로 콜백 함수 내의 accessToken은 업데이트되지 않습니다. accesToken이 변경될 때마다 interceptor 함수 내의 accessToken 값을 업데이트하기 위해 useEffect를 이용해 훅을 만들었습니다.

export const interceptor: Interceptor = {
  configs: null,
  onRequest: (url, options, configs) => ({ url, options }),
  onSuccess: response => {},
  onError: response => {},
  set({ configs, onRequest, onSuccess, onError }) {
    if (configs) this.configs = configs
    if (onRequest) this.onRequest = onRequest
    if (onSuccess) this.onSuccess = onSuccess
    if (onError) this.onError = onError
  },
}

const useInterceptor = ({
  configs,
  onRequest,
  onSuccess,
  onError,
}: Partial<UseInterceptor>) => {
  useEffect(() => {
    interceptor.set({ configs, onRequest, onSuccess, onError })
  }, [configs, onRequest, onSuccess, onError])
}

이렇게 만든 훅을 최상위 컴포넌트에서 사용하면 요청 시 baseUrl 추가, 401 상태코드에 대한 로그아웃 등의 로직을 추가할 수 있습니다.

const App = () => {
  const userContext = useUserContext

  useInterceptor({
    configs: {
      baseUrl: 'http://localhost:8080'
      accessToken: userContext.accessToken,
    },
    onRequest: (url, options, configs) => ({
      url: `${configs.baseUrl}${url}`,
      options: {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${configs.accessToken}`,
        },
      },
    }),
    onError: response => {
      if (response.status === 401) {
        userContext.logout()
      }
    },
  })

  // ...
}

우정민

웹 개발, 프론트엔드