문제상황
우리 서비스는 한 페이지에서 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);
}
};
이 코드는 다음과 같은 흐름으로 동작한다.
- Promise.all은 getAllHappiness(), getAllSummary(), getAllTopActivities(), getAllTopLocations()의 프로미스들을 병렬로 실행한다.
- 모든 요청이 완료되면 그 결과들이 배열 형태로 반환된다.
- 각각의 데이터를 상태에 저장한다.
- 모든 작업이 완료되면 로딩 상태를 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 호출 + 코드 리팩토링까지 끝냈다.
빌드 글 작성까지 가보쟈규
'자바스크립트' 카테고리의 다른 글
이벤트 버블링&캡쳐링, 이벤트 전파 (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 |