내가 사용할 API들의 주소와 url주소다.
- <병원&약국/>
서울시 동물약국 인허가 API
http://openapi.seoul.go.kr:8088/(인증키)/json/LOCALDATA_020302_${지역구}/1/1000/01
서울시 동물병원 인허가 API
http://openapi.seoul.go.kr:8088/(인증키)/json/LOCALDATA_020301_${지역구}/1/5/
- <산책로/>
서울시 주요 공원현황
http://openAPI.seoul.go.kr:8088/process.env.NEXT_PUBLIC_SEOUL_PARK/json/SearchParkInfoService/1/132/
이전 시간에는 서울시 공공데이터 API들을 처리 하기 위한 UI를 subTabs로 나타내어 api를 호출 하기로 결정하였다.
내가 사용할 API들은 총 3개인데, 2개는 동시에 호출해야한다는 조건이 있다.
2개를 동시 호출해서 데이터를 받아서 kakao Map에 뿌려줘야 한다.
조건에 따라서 내가 생각해야 하는 것들이다.
정리하자면 API 모듈화하기 이다.
하나의 탭에서 각기 다른 종류의 API호출을 해야 하는데 내가 신경써야 하는 것들을 정리해 봤다.
이 조건들을 보면, 내가 이전 프로젝트에서 사용했던 promise.all
,react-query 문서를 읽어보다가 paralle Query
하고 싶으면,
일딴, server와의 통신은 react-query
를 사용하고, useQuery로 2번 호출 했을 때 성능 측정과, 리렌더링 횟수를 카운팅 해보면서, 기준점을 잡아보자
/**
* @param LOCALDATA_020301_${api_query}/01/endPoint
*/
const getAnimalHospitalData = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL}`,
})
/**
* @param LOCALDATA_020302_${api_query}/01/endPoint
*/
const getAnimalPharamcyData = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY}`,
})
axios instance를 만들고 탭을 클릭시 각 탭에 있는 queryString으로 api들을 호출 하게 만들었다.
그 다음에는 useQuery
안에 queryFn을 넣어 호출 하게 해주었다.
/**
@api_query
* 도봉구 : DB
...이하 24개
*/
const ParalledQueries = () => {
const { data: ho } = useQuery({
queryKey: ['hospital'],
queryFn: () => getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
select(data) {
console.log(data)
},
})
const { data: ph } = useQuery({
queryKey: ['pharamcy'],
queryFn: () => getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
select(data) {
console.log(data)
},
})
console.log("몇 번 리렌더링이 발생 될까요?")
//...
네트워크 창을 열고 , console
로 확인 한 결과
리렌더링이 1번만 사용된다. 뿐만 아니라 병렬적으로 처리가 되었다!!.
제일 중요한 마지막 부분이 되지 못해서, 탈락
useQueries
도 react-query에서 제공하는 API중 하나로, 여러 개의 useQuery를 병렬로 실행해주는 Hook이다.
//...
const countRef = useRef(0)
const { data, isPending, isLoading } = useQueries({
queries: [
{
queryKey: ['hospital'],
queryFn: () => getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
},
{
queryKey: ['pharamcy'],
queryFn: () => getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
},
],
combine(results) { // 각 queryFn 실행 후, 하나의 값으로 모일 수 있는 메소드이다.
return { // 객체 타입으로 return 해줘야한다.
data: results.map((result) => result.data),
isPending: results.some((results) => results.isPending),
isError: results.some((results) => results.isError),
isLoading: results.some((results) => results.isLoading),
}
},
})
useEffect(() => {
countRef.current++
}, [data]) //data는 새로운 배열로 계속 변경 되므로, 의존성에 넣어놨다.
return <div>리렌더링 몇번 되냐{countRef.current-1}</div> // 3번
리렌더링이 총 3번 일어난다. 그 이유는 위의 공식 홈페이지에서 있는 부분에서 유추 할 수 있는데
If the number of queries you need to execute is changing from render to render,
the number of queries
쿼리들의 수인데 너가 실행시키기려는 쿼리들의 수가 렌더링마다 변경된다면, 이기에
이럴 때 TanStack Query에서는 useQueries
를 제공하니 사용하셈~~ 이라고 해석 할 수 있다. (해석 부족하니... 틀렸으면 댓글 좋아요)
마지막 2개 부분이 안되므로 탈락이다.
useQueries를 사용해 보니, 얉은 지식으로는
useQueries를 사용할 때 각 API마다 에러처리와 로딩 처리를 할 때는 따로 해줘야 한다는 생각이 든다. 그래서 조금더 useQueries에 대해 구글링을 해 봤는데 TkDodo가 작성한 깃허브가 있어서 퍼왔다.
useQueries는 모든 case관련하여 전부 커버하지 못한다고 한다. 따라서 react-error-boundary
라이브러리와 같이 사용하여 case를 관리 하는 것을 권장 하고 있다.
promise all을 사용하려는 이유가 있다.
- 서울시에서 관리하는 공공데이터를 받는다.
- 데이터를 불러오는데 데이터의 갯수가 2000이상이다. (지금 프로젝트에서는 각 지역구마다 2000개씩 ,5만개를 받는 것으로 되어있다. )
따라서, 데이터를 요청할 때, 실패 하면 빠르게 대응하고, 오직 성공한 값들에 대해서만 빠르게 다루고 싶다.
const getDataArr = [
()=> getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
()=> getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
]
const [isLoading,setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
Promise.all(getDataArr).then((results) => console.log(results))
}, [])
병렬적으로 처리가 되고, 리렌더링 발생은 일어 나지 않는다.
그렇지만 실질적으로 비동기 통신을 react-query와 같이 사용 하지 않을 때는 훨씬 로직이 복잡해 진다.
//...생략
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const getDataArr = [
()=> getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
()=> getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
]
useEffect(() => {
setIsLoading(true)
Promise.all(getDataArr)
.then((results) => {...})
.catch((err) => {
setIsError(true)
})
.finally(() => setIsLoading(false))
}, [])
if(isLoading) return <div>로딩중...</div>
if(isError) return <div>에러 발생</div>
위와 같이 UX를 생각하는 참다운 개발자는 loading
, error
관련 state를 사용하여 UI를 각각 보여줘야 한다.
물론 ref와 같은 훅을 사용 할 수 있겠지만, props로 내려준다고 가정 했을 시, forwardRef
로 감싸줘야 한다는 불편함?이 있다.
렌더링을 확인해 보면 총 4번 발생하게 된다.
따라서 위의 2가지는 되지만, 아래 3가지는 되지 못한다.
Promise.allSettled를 사용하면 실패한것만을 가지고 다시 시도 할 수 있는 훌륭한 메소드입니다.
const getDataArr = [
()=> getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
()=> getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
]
Promise.allSettled(getDataArr)
.then((result) => {
// 실패한 것들만 필터링해서 다시 시도
result.forEach(async (val, index) => {
if(val.status === 'rejected') {
await getDataArr[index]; // 실패한 요청 다시 ajax
}
})
})
.catch((err) => console.log(err));
위의 상황들에서, 조금만 더 생각하면 될것 같았는데 (지금은 당연한 거지만,,) 생각에 물꼬가 트였다.
Promise.all과 useQuery를 결합하기
const getDataArr = [
()=> getAnimalHospitalData('LOCALDATA_020301_DB/1/1000/01'),
()=> getAnimalPharamcyData('LOCALDATA_020302_DB/1/1000/01'),
]
const ParalledPage = () => {
const getParalledData = async () => {
try {
const results = await Promise.all(getDataArr)
return results
} catch (err) {
console.log(err)
}
}
const { data, isLoading, isError } = useQuery({
queryKey: ['ANIMAL'],
queryFn: getParalledData,
})
//...
모든 조건에 충족한다.
모든 API통신의 상태를 한번에 보장하고, caching과 loading, error처리도 여러 가지를 처리할 필요도 없어졌다.
Seoul시에 보내는 API요청은 API router로 보내는 것을 참고하자... ( 이유는 다음 글에서 )
/**
* 기존에는 fn에 각 axios인스턴스를 담았지만 api router로 보낼때 함수는 보내지 못하여 export해주었고 기존 객체 배열에서 뺐습니다.
*/
const DYNAMIC_API_QURIES = [
{ api_name: 'animal_hospital', query_key: 'LOCALDATA_020301_' },
{ api_name: 'animal_pharmacy', query_key: 'LOCALDATA_020302_' },
];
/**
*
* @param api_query react-query에서 enabled와 지역구 query로 활용하는 인자 값입니다.
* @returns
*/
export const ParalledQueriesAnimalMedicineAPI = async (api_query: string | null) => {
try {
const results = await Promise.all(
DYNAMIC_API_QURIES.map(async query => {
const result = await axios.post(`${process.env.NEXT_PUBLIC_SEOUL_API_URL}`, {
api_query,
api_name: query.api_name,
query_key: query.query_key,
});
return result.data;
}),
);
return results;
} catch (err) {
console.log(err, 'map Error');
return []; // 성공 실패시 균일하게 해주기 위해서
}
};
interface I_QueryProps {
api_type: 'hospital' | 'walk';
api_query: string | null;
}
enum LOCATION_QUERY {
HOSPITAL = 'hospital',
PHARMACY = 'pharmacy',
PARK = 'park',
}
// api_type에 따라서 병원&약국 아니면 산책로
// api_query에 따라서 각 지역구를 호출
// api_query가 null이면 호출 하지 않기
// kakao 내장 검색으로 위치 찾으면 호출 하지 않기
const useLocationQuery = (props: I_QueryProps) => {
const { api_type, api_query } = props;
const { data: medicine, isLoading } = useQuery({
queryKey: [LOCATION_QUERY.HOSPITAL, api_query],
queryFn: () => ParalledQueriesAnimalMedicineAPI(api_query),
enabled: !!api_query && api_type === 'hospital', // animal로 할껄 그랬네..
select: refineSeoulApiData,
refetchOnWindowFocus: false,
staleTime: Infinity, // caching된 데이터만 사용하려구
});
//... 생략
이틀 정도 생각하고 작성해봤던 코드들이였다.
뿌듯하다
useQueries는 그렇다면 어디다 사용하면 좋을지 생각을 조금씩 해보았다.
지금 생각나는 것으로는
하나의 페이지에서 여러 뉴스기사 - 각각의 다른 API들을 호출 할 때 loading, error를 따로 처리해주는 서비스 페이지에 적합하다고 생각이든다.
useQuery는 여러개 호출하면 알아서 병렬처리로 되는것으로 알고있어요
또한 캐싱된 데이터를 사용하고싶으시면 prefetch후 Hydration을 하시면 더 도움이 될 것 같습니다!