[ 공모전 ] 지도 페이지 : useQuery & Promise - 병렬처리 (Paralle)

최문길·2024년 6월 24일
0

공모전

목록 보기
19/46

내가 사용할 API들의 주소와 url주소다.

이전 시간에 SubTabs UI...

이전 시간에는 서울시 공공데이터 API들을 처리 하기 위한 UI를 subTabs로 나타내어 api를 호출 하기로 결정하였다.



Issue : API Call

내가 사용할 API들은 총 3개인데, 2개는 동시에 호출해야한다는 조건이 있다.

2개를 동시 호출해서 데이터를 받아서 kakao Map에 뿌려줘야 한다.

조건에 따라서 내가 생각해야 하는 것들이다.

  • 2개를 어떻게 동시 호출 할지,
  • 데이터 처리 및, 에러 처리를 어떻게 처리 해줘야 할지
  • 최적화도 생각도 해야 한다.

정리하자면 API 모듈화하기 이다.

하나의 탭에서 각기 다른 종류의 API호출을 해야 하는데 내가 신경써야 하는 것들을 정리해 봤다.



  • 2개의 API들은 병렬로 호출되어야 한다.
  • 2개의 API통신이 모두 성공한다고 상정한다. ( seoul api이므로 )
  • react-query를 사용하면 좋겠음, caching 기능이 있으므로 stale값을 지정해 한 번 호출할 때만 시간이 걸렸으면 좋겠음. 그 이후에는 caching된 데이터를 사용하였으면 좋겠다.
  • 2개를 동시 호출 해도 렌더링 최적화 됬으면 좋겠다.
  • data를 받아올 때, loading처리와 에러처리가 한 번에 이루어졌으면 좋겠다.

이 조건들을 보면, 내가 이전 프로젝트에서 사용했던 promise.all ,react-query 문서를 읽어보다가 paralle Query하고 싶으면,

useQuery 2번 호출


일딴, server와의 통신은 react-query를 사용하고, useQuery로 2번 호출 했을 때 성능 측정과, 리렌더링 횟수를 카운팅 해보면서, 기준점을 잡아보자

axios instance만들기

/**
 * @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번만 사용된다. 뿐만 아니라 병렬적으로 처리가 되었다!!.


useQuery 2번

  • ✅ 2개의 API들은 병렬로 호출되어야 한다.
  • ✅ 2개의 API통신이 모두 성공한다고 상정한다. ( seoul api이므로 )
  • ✅ react-query를 사용하면 좋겠음, caching 기능이 있으므로 stale값을 지정해 한 번 호출할 때만 시간이 걸렸으면 좋겠음. 그 이후에는 caching된 데이터를 사용하였으면 좋겠다.
  • ✅ 2개를 동시 호출 해도 렌더링 최적화 됬으면 좋겠다.
  • ✖️ data를 받아올 때, loading처리와 에러처리가 한 번에 이루어졌으면 좋겠다.

제일 중요한 마지막 부분이 되지 못해서, 탈락


useQueries

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를 제공하니 사용하셈~~ 이라고 해석 할 수 있다. (해석 부족하니... 틀렸으면 댓글 좋아요)

useQueries

  • ✅ 2개의 API들은 병렬로 호출되어야 한다.
  • ✅ 2개의 API통신이 모두 성공한다고 상정한다. ( seoul api이므로 )
  • ✅ react-query를 사용하면 좋겠음, caching 기능이 있으므로 stale값을 지정해 한 번 호출할 때만 시간이 걸렸으면 좋겠음. 그 이후에는 caching된 데이터를 사용하였으면 좋겠다.
  • ✖️ 2개를 동시 호출 해도 렌더링 최적화 됬으면 좋겠다.
  • ✖️ data를 받아올 때, loading처리와 에러처리가 한 번에 이루어졌으면 좋겠다.

마지막 2개 부분이 안되므로 탈락이다.

useQueries를 사용할 때 주의 할점

useQueries를 사용해 보니, 얉은 지식으로는
useQueries를 사용할 때 각 API마다 에러처리와 로딩 처리를 할 때는 따로 해줘야 한다는 생각이 든다. 그래서 조금더 useQueries에 대해 구글링을 해 봤는데 TkDodo가 작성한 깃허브가 있어서 퍼왔다.

useQueries는 모든 case관련하여 전부 커버하지 못한다고 한다. 따라서 react-error-boundary 라이브러리와 같이 사용하여 case를 관리 하는 것을 권장 하고 있다.

TkDodo...

Promise all

Promise.all vs Promise.allSettled

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개의 API들은 병렬로 호출되어야 한다.
  • ✅2개의 API통신이 모두 성공한다고 상정한다. ( seoul api이므로 )
  • ✖️react-query를 사용하면 좋겠음, caching 기능이 있으므로 stale값을 지정해 한 번 호출할 때만 시간이 걸렸으면 좋겠음. 그 이후에는 caching된 데이터를 사용하였으면 좋겠다.
  • ✖️ 2개를 동시 호출 해도 렌더링 최적화 됬으면 좋겠다.
  • ✖️ data를 받아올 때, loading처리와 에러처리가 한 번에 이루어졌으면 좋겠다.

따라서 위의 2가지는 되지만, 아래 3가지는 되지 못한다.

Promise.allSettled의 특징을 살린 활용방법은?

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의 결합

위의 상황들에서, 조금만 더 생각하면 될것 같았는데 (지금은 당연한 거지만,,) 생각에 물꼬가 트였다.

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,
  })
  //...


  • ✅ 2개의 API들은 병렬로 호출되어야 한다.
  • ✅ 2개의 API통신이 모두 성공한다고 상정한다. ( seoul api이므로 )
  • ✅ react-query를 사용하면 좋겠음, caching 기능이 있으므로 stale값을 지정해 한 번 호출할 때만 시간이 걸렸으면 좋겠음. 그 이후에는 caching된 데이터를 사용하였으면 좋겠다.
  • ✅ 2개를 동시 호출 해도 렌더링 최적화 됬으면 좋겠다.
  • ✅ data를 받아올 때, loading처리와 에러처리가 한 번에 이루어졌으면 좋겠다.

모든 조건에 충족한다.
모든 API통신의 상태를 한번에 보장하고, caching과 loading, error처리도 여러 가지를 처리할 필요도 없어졌다.


내가 사용한 코드

Seoul시에 보내는 API요청은 API router로 보내는 것을 참고하자... ( 이유는 다음 글에서 )

API콜 함수 정의

/**
 * 기존에는 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 []; // 성공 실패시 균일하게 해주기 위해서
  }
};

API Hook

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)

useQueries는 그렇다면 어디다 사용하면 좋을지 생각을 조금씩 해보았다.
지금 생각나는 것으로는

하나의 페이지에서 여러 뉴스기사 - 각각의 다른 API들을 호출 할 때 loading, error를 따로 처리해주는 서비스 페이지에 적합하다고 생각이든다.

1개의 댓글

comment-user-thumbnail
2024년 7월 2일

useQuery는 여러개 호출하면 알아서 병렬처리로 되는것으로 알고있어요
또한 캐싱된 데이터를 사용하고싶으시면 prefetch후 Hydration을 하시면 더 도움이 될 것 같습니다!

답글 달기

관련 채용 정보