prevent duplicated API call

mochang2·2024년 2월 28일
0

RETROSPECT

목록 보기
4/5

0. 공부하게 된 이유

최적화는 유저 경험 향상을 위해 필수적인 부분이라고 생각한다.
프론트엔드에서 최적화할 수 있는 부분이 많으며 중복 API 호출 제거가 그 중 하나이다.
최선은 로직상 중복되는 API 호출을 없애는 것이지만, 유저 플로우 상 어쩔 수 없거나 API 호출 코드를 추상화해서 사용했을 경우 분리하기가 어려운 경우가 있다.

내가 마주했던 경우는, 모든 API 호출 전 데스크톱 어플리케이션에 업데이트가 있는지 확인하는 API를 호출을 하고 있었다.
웹소켓을 연결하지 않았기 때문에 클라이언트에서 업데이트가 있는지 확인하는 방법은 API 호출뿐이었다.
해당 어플리케이션에서 내에서 할 수 있는 동작 중에 업데이트의 우선순위가 가장 높았기 때문에 화면을 전환하거나, 공지를 확인하거나 하는 등 모든 상황에서 업데이트 여부를 확인할 수 있도록 구현되어 있었다. 업데이트 확인 API를 별도로 분리하기 위해서는 어플리케이션 켰을 때, 로그인을 했을 때, 페이지를 이동했을 때 등등 다양한 곳에서 수동으로 호출해야만 했었다.
이 때문에 중복된 API를 호출하지 않도록 하기 위해서는 업데이트 확인 API를 분리하는 것보다 새로운 메서드를 만드는 것이 더 낫다고 생각했다.

이외에도 사용자가 버튼 클릭 시 API를 호출하는 상황에서, 실수로 더블 클릭을 하는 경우도 있다.
이때 버튼을 disabled하는 방어 로직을 구현하지 않았다면 중복된 API를 호출하게 된다.

이왕 공부하는 김에 중복 API 호출을 제거하는 방법에는 어떠한 방법이 더 있을지 고민하고 정리해봤다.
참고로 캐싱은 내가 이야기하고자 하는 바와 거리가 있다고 생각해 기술하지는 않았다.
(에러 발생 시 재요청하는 등의 로직 또한 아래 설명들과 불필요한 것 같아서 예시 코드에서는 전부 제거했다.)

1. 가장 먼저 호출된 API 사용

use first API

요청 자체를 하지 않음

가장 먼저 호출된 API의 결과를 신뢰해도 될 때 사용할 수 있다.

const history = new Map<string, number>();
const MAX_AGE = 1_000; // 1초

async function request(config: TConfig) {
  const now = Date.now();
  const isFirstRequest = !history.get(config.url);
  const isStale = history.get(config.url) && now - history.get(config.url) > MAX_AGE;

  if (isFirstRequest || isStale) {
    history.set(config.url, now);

    const result = await fetch(config.url); // 간단하게 get 요청만 보낸다고 가정
    
    if (result.ok) {
      return await result.json();
    }
    
    // 기타 에러 처리
  }

  return new NetworkError({ // 에러 선언을 위한 커스텀 클래스
    url: config.url,
    code: 200,
    status: "success",
    message: "Automatic cancel - duplicated request",
  });
}

위 코드는 아주 간단하다.
history 객체에 특정 url로 요청을 보낸 마지막 시간을 저장한다.
(받은 응답을 저장하고 재사용하는 것이 아니기 때문에 캐시와는 차이가 있다)
해당 url로 보내는 첫 요청이거나, stale하다가 판단하면 요청을 진행한다.
아니라면 외부에서 에러를 처리할 수 있도록 커스텀한 에러 객체를 return한다.

나는 개인적으로 이 방법이 아래와 같이 비동기 처리되지 않은 곳에서 유용했다.

function initializeUserData() {
  requestAuth();
  // API를 호출하지만 await 선언이 안 된 함수1
  // 내부에서 업데이트 여부 확인 함수 호출
  requestBasicStatus();
  // API를 호출하지만 await 선언이 안 된 함수2
  // 내부에서 업데이트 여부 확인 함수 호출
  // ...
}

2. 가장 나중에 호출된 API 사용

use last API

요청 지연

"이벤트 호출을 지연시키거나 한 번에 처리하게 해서 API의 호출을 최소화하기 위한다"라고 하면 가장 먼저 생각나는 것은 debounce일 것이다.

function debounce<T extends (...args: any[]) => any>(
  callback: T,
  millisecond: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>

  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      callback.apply(this, args)
    }, millisecond)
  }
}

// 사용
const delayedRequest = debounce(request, 1000);

첫 번째 요청은 무조건 호출하고 그 다음 호출부터 지연시키는 옵션 등 lodash스러운 debounce 코드는 여기를 참조하자.

debounce가 가장 유용하게 쓰이는 곳은 검색창이 아닐까 싶다.
사용자가 하나의 모음, 자음을 입력할 때마다 API를 호출하면 너무 많은 API를 호출하고 리렌더링을 자주해야 하기 때문에, 사용자 입력이 어느 정도 멈춰있다 싶을 때 API를 호출한다.
(확신은 없지만, velog 임시저장도 뭔가 비슷한 느낌인 것 같다)

요청 중단

최신 브라우저에는 API 요청을 중단할 수 있는 AbortController를 제공한다.

요청이 중단되면 에러가 발생하기 때문에 AbortController를 사용하기 위해서는 별도의 에러 처리 로직이 필요하다.
또한 요청을 보내는 순간 이미 클라이언트의 리소스를 사용한 것이다.
이러한 이유 때문에 다른 방법들보다 더 고려해야 할 것이 많은 방법이다.

const controller = new AbortController();
const history = new Map<string, number>();
const COOL_DOWN_TIME = 1_000; // 1초

async function request(config: TConfig) {
  const now = Date.now();
  const lastRequestTime = latestRequest.get(path);
  const isRequestDuplicated = lastRequestTime && currentTime - lastRequestTime < coolDownTime;
  if (isRequestDuplicated) {
    controller.abort();
  }

  try {
    latestRequest.set(config.url, currentTime);

    const data = await fetch(config.url);
    
    // 응답 코드 관련 에러 처리 생략
    
	return await data.json();
  } catch (e) {
    const error = e as Error;

    if (error.name === "AbortError") {
      console.error("API Abortion!"); // 무시
    } else {
      // 기타 에러 처리
    }
  }
}

위 코드도 아주 간단하다.
history 객체에 특정 url로 요청을 보낸 마지막 시간을 저장한다.
과거에 해당 url로 보낸 기록이 있고, 그것이 1초 이내이면 중복된 요청이라고 판단하고 요청을 중단한다.

단순히 중복된 API를 제거하는 데에도 사용할 수 있지만 개인적으로는, 페이지 이동 혹은 사용자 행동에 의해 요청이 취소되어야 하는 경우 혹은 과거에 보낸 요청에 대한 응답이 더이상 유효하지 않은 경우 등에 더 유용한 API라고 생각한다.

profile
개인 깃헙 repo(https://github.com/mochang2/development-diary)에서 이전함.

0개의 댓글