자바스크립트

[JS] 한 페이지에서 많은 API 호출을 효율적으로 처리하는 방법 / Promise.all 사용하기

solfa 2024. 7. 9. 03:21

문제상황

우리 서비스는 한 페이지에서 get 요청을 어마무시하게 많이 날리고 있다!

후덜덜

물론 받아오는 데이터들이 전부 간단한 string이지만 요청을 한 번에 처리하기엔 서버에 무리가 갈 수도 있다고 판단하였다.

그리고 이런 대량의 api의 get 요청은 어떻게 진행시키면 좋은지 궁금해져서 어떻게 처리를 하나 찾아보았다.


해결 방법

get요청을 병렬적으로 실행한다!

병렬적으로 서버 요청 보내는 함수 만들기: Promise.all을 사용하여 여러 API 요청을 병렬적으로 실행한다.

사용자에겐 로딩중을 띄워서 조금 기다리게 한 후 그 사이에 순차적으로, get 요청을 병렬적으로 실행하여 데이터를 받아오는 방법을 사용하면 된당

 

Promise.all

Promise.all은 여러 개의 프로미스를 병렬로 실행할 수 있게 해주는 JavaScript의 기능이다. 주어진 모든 프로미스가 완료될 때까지 기다린 후, 모든 결과를 배열로 반환한다. 단, 하나라도 실패하면 전체가 실패한다! 그래서 나처럼 무지성 st의 get 요청을 할 때 매우 좋다.

 

Promise.all 사용 예제

const fetchData = async () => {
  try {
    const [happinessData, summaryData, activityData, locationData] =
      await Promise.all([
        getAllHappiness(),
        getAllSummary(),
        getAllTopActivities(),
        getAllTopLocations(),
      ]);

    setData({
      happiness: happinessData,
      summary: summaryData,
      activities: activityData,
      locations: locationData,
    });
  } catch (error) {
    console.error("Error fetching data:", error);
  } finally {
    setLoading(false);
  }
};

 

이 코드는 다음과 같은 흐름으로 동작한다.

  1. Promise.all은 getAllHappiness(), getAllSummary(), getAllTopActivities(), getAllTopLocations()의 프로미스들을 병렬로 실행한다.
  2. 모든 요청이 완료되면 그 결과들이 배열 형태로 반환된다.
  3. 각각의 데이터를 상태에 저장한다.
  4. 모든 작업이 완료되면 로딩 상태를 false로 변경하여 로딩 중 컴포넌트를 숨긴다.
Promise.all(loadImagePromises).finally(() => {
        setItemsReady(true); // 모든 이미지 로딩이 완료되었을때 상태 업데이트
      });
    }

 

이렇게 모든 로딩 끝나고 컴포넌트 보여주기도 가능! 위 코드처럼 그냥 try - catch - finally에서 보여줘도 되긴 함


해야 하는 일

1. 요청을 전부 받아 올 동안 사용자에게 보여줄 로딩중 컴포넌트 만들기

2. 병렬적으로 서버 요청 보내는 함수 만들기

 

코드 작성하기

1. 요청을 전부 받아 올 동안 사용자에게 보여줄 로딩중 컴포넌트 만들기

 

로딩중 컴포넌트는 선택이긴 하지만 사용자 편의를 개선하기 위해... 넣었다! react native로 개발을 했는데 rn에는 로딩 indicator가 기본으로 있으니 이 걸 적극적으로 사용해도 좋을 듯 하다.

import React from 'react';
import { View, ActivityIndicator, Text } from 'react-native';

const Loading = () => {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <ActivityIndicator size="large" color="#0000ff" />
      <Text>Loading...</Text>
    </View>
  );
};

export default Loading;

 

ActivityIndicator와 Text로 간단한 로딩 컴포넌트를 만들었다.

 

 

2. 병렬적으로 서버 요청 보내는 함수 만들기

이제 다수의 API 요청을 병렬적으로 실행하고 데이터를 상태에 저장하는 코드를 작성해야 한다.

 

2-1. 데이터를 받아올 상태 만들기

const isFocused = useIsFocused();
const [loading, setLoading] = useState(true);
const [data, setData] = useState({
  happiness: null,
  summary: null,
  activities: null,
  locations: null,
});
const [userName, setUserName] = useState("");

useState와 useEffect를 사용하여 상태를 관리하고, 컴포넌트가 포커스될 때 데이터를 가져오게 하기 위해서 react native에만 있는 useisfocused를 사용했다.

 

사담이지만...

rn에서 하단 네비게이션바로 이동을 했을 때 useEffect를 쓴다고 해서 그 때마다 데이터를 불러오지 못하는 문제가 있었다 ㅠ

react 에서 그냥 useEffect를 사용하는 것과 같은 것!

 

아무튼 저렇게 데이터를 받아올 배열 상태를 만들어둔다.

 

2-2. Promise.all을 사용하여 여러 API 요청을 병렬적으로 실행하기

const fetchData = async () => {
  try {
    const [happinessData, summaryData, activityData, locationData] =
      await Promise.all([
        getAllHappiness(),
        getAllSummary(),
        getAllTopActivities(),
        getAllTopLocations(),
      ]);

    setData({
      happiness: happinessData,
      summary: summaryData,
      activities: activityData,
      locations: locationData,
    });
  } catch (error) {
    console.error("Error fetching data:", error);
  } finally {
    setLoading(false);
  }
};

 

2-3. 데이터 로딩 및 UI 업데이트

로딩 중일 때는 로딩 컴포넌트를 표시하고, 데이터가 로드되면 실제 데이터를 보여준다.

리턴 코드가 좀 긴데... 양해 부탁 ㅎ

useEffect(() => {
  if (isFocused) {
    fetchData();
    fetchUserName();
  }
}, [isFocused]);

if (loading) {
  return <Loading />;
}

return (
  <ScrollView>
    <ReportBox>
      <UserText>{userName}님의</UserText>
      <LeftText>평균 행복지수는</LeftText>
      <View style={{ flexDirection: "row", alignItems: "center" }}>
        {data.happiness ? (
          <FocusText>{data.happiness.data.level}</FocusText>
        ) : (
          <></>
        )}
        <LeftText>이에요</LeftText>
        {data.happiness ? (
          <FocusText>{data.happiness.data.emoji}</FocusText>
        ) : (
          <></>
        )}
      </View>
      <CriteriaButton onPress={() => setModalVisible(true)}>
        <CriteriaButtonText>기준이 궁금해요!</CriteriaButtonText>
      </CriteriaButton>
    </ReportBox>

    <FirstReportBox>
      <UserText>{userName} 님은</UserText>
      <View style={{ flexDirection: "row", alignItems: "center" }}>
        {data.summary ? (
          <FocusText>{data.summary.data.time_of_day}</FocusText>
        ) : (
          <></>
        )}
        <LeftText>에</LeftText>
      </View>
      <View style={{ flexDirection: "row", alignItems: "center" }}>
        {data.summary ? (
          <FocusText>{data.summary.data.location}</FocusText>
        ) : (
          <></>
        )}
        <LeftText>에서</LeftText>
      </View>
      <View style={{ flexDirection: "row", alignItems: "center" }}>
        {data.summary ? (
          <FocusText>{data.summary.data.activity}</FocusText>
        ) : (
          <></>
        )}
        <LeftText>을 할 때</LeftText>
      </View>
      <LeftText>가장 행복했어요</LeftText>
      <DataeBtn onPress={handleDataBtnPress}>
        <DataText>전체 데이터 보기</DataText>
      </DataeBtn>
    </FirstReportBox>

    <ActivityReportBox>
      <TitleText>행복한 활동 BEST 3</TitleText>
      <SubTitleText>
        {userName} 님은 이런 활동을 할 때 행복하군요!
      </SubTitleText>
      {data.activities && data.activities.data ? (
        data.activities.data.map((activity, index) => (
          <SecondReportBox key={index}>
            <NumText>{activity.ranking}</NumText>
            <NumtitleText>{activity.activity}</NumtitleText>
            <ImojiText>{activity.emoji}</ImojiText>
          </SecondReportBox>
        ))
      ) : (
        <></>
      )}
    </ActivityReportBox>

    <MapReportBox>
      <TitleText>행복했던 장소 BEST 3</TitleText>
      <SubTitleText>{userName} 님은 이런 장소에서 행복했어요</SubTitleText>
      {data.locations && data.locations.data ? (
        data.locations.data.map((location, index) => (
          <SecondReportBox key={index}>
            <NumText>{location.ranking}</NumText>
            <NumtitleText>{location.location}</NumtitleText>
          </SecondReportBox>
        ))
      ) : (
        <></>
      )}
    </MapReportBox>
    <View style={{ height: 200 }} />
  </ScrollView>
);

 

 


코드 리팩토링 하기, 중복 코드 문제 해결!

Promise.all을 이용해 api 요청을 병렬적으로 실행하여 데이터 받아오기는 성공적으로 구현했다.

하지만 뭔가 찝찝한 부분이 있었다. 바로 어마무시하게 많이 날리는 get 요청과 함께 공존할 수 밖에 없는 어마무시하게 긴 api 호출 코드 ㅠㅠㅠ

 

getReports라는 이름 안에 묶인 300줄이 넘는 api 코드들...

심지어 모두 get, 받아오는 데이터 형식도 전부 비슷한 형태라 복붙을 엄청 했던 기억이 아직까지도 생생하다 ㅎ

이 무지성 코드 복붙을 개선하고 싶어서 코드의 중복된 부분은 추출해서 재사용하기로 했다.

1. 공통 로직 추출

먼저, API 요청의 공통된 로직을 하나의 함수로 추출한다.

import AsyncStorage from "@react-native-async-storage/async-storage";
import axios from "axios";

const fetchWithToken = async (url) => {
  try {
    const token = await AsyncStorage.getItem("accessToken");
    if (!token) {
      throw new Error("토큰없음");
    }

    const response = await axios.get(url, {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
    });

    return response.data;
  } catch (error) {
    console.error("Error fetching data:", error);
    throw error;
  }
};

 

2. 추출한 공통 함수 사용

위에서 만든 fetchWithToken 함수를 사용하여 API 호출 함수를 단순화한다.

export const getYearLocation = async () => {
  const url = `${PUBLIC_DNS}/api/report/year/location`;
  return fetchWithToken(url);
};

export const getYearGraph = async () => {
  const url = `${PUBLIC_DNS}/api/report/year/graph`;
  return fetchWithToken(url);
};
 
 ...
 // 나머지 함수들도 동일한 패턴으로 작성

 

사실 더 간단하게 쓸 수 있다. 그냥 바로 url을 넣어버리면 된다!

import fetchWithToken from "위에서 작성한 거 불러와줌";

export const getAllHappiness = async () => fetchWithToken("/api/report/all/happiness/");
export const getAllSummary = async () => fetchWithToken("/api/report/all/summary");
export const getAllTopActivities = async () => fetchWithToken("/api/report/all/top-activities");
export const getAllTopLocations = async () => fetchWithToken("/api/report/all/top-locations");
export const getMonthGraph = async () => fetchWithToken("/api/report/month/graph");
export const getMonthHappiness = async () => fetchWithToken("/api/report/month/happiness");
export const getMonthSummary = async () => fetchWithToken("/api/report/month/summary");
export const getMonthActivities = async () => fetchWithToken("/api/report/month/top-activities");
export const getMonthLocations = async () => fetchWithToken("/api/report/month/top-locations");
export const getYearHappiness = async () => fetchWithToken("/api/report/year/happiness");
export const getYearSummary = async () => fetchWithToken("/api/report/year/summary");
export const getYearActivities = async () => fetchWithToken("/api/report/year/top-activities");
export const getYearLocation = async () => fetchWithToken("/api/report/year/top-locations");
export const getYearGraph = async () => fetchWithToken("/api/report/year/graph");

 

env를 살리고 싶으면 이렇게 해도 똑같이 작동한다.

import { fetchWithToken } from "적절한 경로로 수정";
import { PUBLIC_DNS } from "@env";

export const getAllHappiness = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/all/happiness/`);
export const getAllSummary = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/all/summary`);
export const getAllTopActivities = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/all/top-activities`);
export const getAllTopLocations = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/all/top-locations`);
export const getMonthGraph = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/month/graph`);
export const getMonthHappiness = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/month/happiness`);
export const getMonthSummary = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/month/summary`);
export const getMonthActivities = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/month/top-activities`);
export const getMonthLocations = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/month/top-locations`);
export const getYearHappiness = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/year/happiness`);
export const getYearSummary = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/year/summary`);
export const getYearActivities = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/year/top-activities`);
export const getYearLocation = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/year/top-locations`);
export const getYearGraph = async () => fetchWithToken(`${PUBLIC_DNS}/api/report/year/graph`);

 

이렇게 코드를 326줄에서 16줄로 줄였다!

나 혼자서 코드를 짜다보니 너무 나만 알아볼 수 있게 작성하게 돼서 문제였는데 앞으로는 유지보수가 쉬운 코드를 짜야겠다

흑흑

 


완성된 화면

 

 

아무튼 이렇게 병렬로 api 호출 + 코드 리팩토링까지 끝냈다.

빌드 글 작성까지 가보쟈규

728x90

'자바스크립트' 카테고리의 다른 글

이벤트 버블링&캡쳐링, 이벤트 전파  (0) 2024.07.08
this란 무엇인가?  (0) 2024.07.08
파일 효과적으로 가져오기 | script defer, async  (0) 2024.07.08
BOM 이란?  (0) 2024.07.08
DOM 이란?  (0) 2024.07.08