[문제 해결] - promise.all을 사용하여 병렬적으로 API 호출하기

Donggu(oo)·2023년 9월 14일
0

[Solo Project] - saveme

목록 보기
4/5
post-thumbnail

1. 문제 현상


  • 약 5000개의 서울시 공공 데이터를 호출해야 하는데 1회 호출 시 최대 1000개의 데이터만 호출할 수 있었다. 그래서 1000 ~ 2000, 2001 ~ 3000... 이렇게 1000개 씩 나누어서 동기적으로 호출을 하니 전체 데이터를 불러올 때 까지 시간이 너무 오래 걸리는 문제가 발생했다.

2. 문제 원인


  • 기존의 로직은 api 호출 시 마지막 데이터의 페이지 까지 1000개 씩 순차적으로 호출하는 로직으로 작성을 했었다.

    1. 요청 시작 페이지인 start와 요청 마지막 페이지인 end 변수는 while 루프 안에서 증가 api 호출이 발생할 때 마다 증가한다.

    2. response를 불러온 페이지의 데이터를 rowData에 저장하고 전체 데이터를 저장하는 allRowData에 다시 push하여 각 페이지의 데이터를 합친다.

    3. 현재 페이지를 가져오고 전체 데이터의 총 개수를 나타내는 totalCount와 현재 범위인 end를 비교하여 종료 조건을 확인하여 totalCount가 end보다 작거나 같으면, 모든 페이지의 데이터를 가져왔으므로 루프를 종료한다.

    4. 루프는 totalCount 까지 모든 페이지의 데이터를 가져올 때까지 반복한다.

  • 그러다 보니 이전의 데이터를 불러올 때까지 다음 호출은 blocking 되어 전체 데이터를 불러올 때 까지 비효율적으로 호출을 하고 있었다.

const MAX_ROWS = 1000;

export const useGetData = () => {
  const [toiletData, setToiletData] = useState<ToiletData[]>([]);
  const [dataLoading, setDataLoading] = useState<boolean>(true);

  const getData = async () => {
    try {
      let start = 1;
      let end = MAX_ROWS;
      const allRowData = [];

      while (true) {
        const res = await PROXY_API.get(`${start}/${end}`);
        const rowData = res.data.SearchPublicToiletPOIService.row;
        const totalCount = res.data.SearchPublicToiletPOIService.list_total_count;

        allRowData.push(...rowData);

        if (totalCount <= end) {
          break;
        }

        start = end + 1;
        end = Math.min(end + MAX_ROWS, totalCount);
      }
      setToiletData(allRowData);
      setDataLoading(false);
    } catch (err) {
      console.error(err);
    }
  };

  useEffect(() => {
    getData();
  }, []);

  return { toiletData, dataLoading };
};

3. 문제 해결


  • 결론적으로 위 문제는 병렬적으로 api를 호출할 수 있는 promise all을 통해 해결할 수 있었다.

    1. 먼저 몇 번의 api 호출이 필요한지 알기 위해 getTotalCount 함수를 호출하여 전체 데이터 개수를 구한다.

    2. 병렬로 실행시킬 promise를 promises 배열에 추가한다.

    3. promises 배열에 추가된 모든 promise들을 병렬로 실행시키기 위해 promise.all을 통해 api를 호출한다.

    4. 각 요청의 응답이 담겨있는 res 배열에서 data만 추출한다.

const MAX_ROWS = 1000;

export const useGetData = () => {
  const [toiletData, setToiletData] = useState<ToiletData[]>([]);
  const [dataLoading, setDataLoading] = useState<boolean>(false);

  const getData = async () => {
    setDataLoading(true);
    try {
      const totalCount = await getTotalCount();
      const promises: any = [];

      for (let start = 1; start <= totalCount; start += MAX_ROWS) {
        const end = Math.min(start + MAX_ROWS - 1, totalCount);
        promises.push(PROXY_API.get(`${start}/${end}`));
      }

      const res = await Promise.all(promises);

      const allRowData = res.reduce((accumulator, current) => {
        const rowData = current.data.SearchPublicToiletPOIService.row;
        return accumulator.concat(rowData);
      }, []);

      setToiletData(allRowData);
      setDataLoading(false);
    } catch (err) {
      console.error(err);
    }
  };

  const getTotalCount = async () => {
    try {
      const res = await PROXY_API.get("1/1");
      return res.data.SearchPublicToiletPOIService.list_total_count;
    } catch (err) {
      console.error(err);
      return 0;
    }
  };

  useEffect(() => {
    getData();
  }, []);

  return { toiletData, dataLoading };
};

  • 추가적으로 allRowData로 합치는 로직에서 기존에는 아래와 같이 map과 flat을 사용했었다.
console.time("using flat");
const allRowData = res.map((res) => res.data.SearchPublicToiletPOIService.row).flat();
console.timeEnd("using flat");

  • 그런데 로직을 다듬던 중 flat 메서드의 속도가 느리다는 글을 발견했고 해당 글에서는 V8엔진이 flat 메서드는 아직 최적화 되지 않았지만 concat 메서드에는 최적화 되어 있어 속도가 더 빠르다고 설명하고 있었다.

  • 실제로 아래와 같이 reduce와 concat으로 로직을 수정하여 시간을 체크해보니 무려 10배 가까이 차이가 나는 것을 확인할 수 있었다.

console.time("using reduce & concat");
const allRowData = res.reduce((accumulator, current) => {
  const rowData = current.data.SearchPublicToiletPOIService.row;
  return accumulator.concat(rowData);
}, []);
console.timeEnd("using reduce & concat");

0개의 댓글