카테고리 없음

[React/리액트] React에서 순차적 API 호출 처리하기: 위치 기반 버스 도착 정보 조회 사례

solfa 2025. 5. 4. 20:13

React에서 순차적 API 호출 처리하기: 위치 기반 버스 도착 정보 조회 사례

서론

한 API의 결과가 다른 API를 호출하는 전제 조건이 되는 경우가 있다.

버스 번호판 인식 앱을 개발하다가 사용자의 위치를 먼저 서버에 전송하고, 그 위치 기반으로 버스 도착 정보를 조회해야 하는 상황이 발생했다. 이런 순차적 API 호출은 React에서 어떻게 처리할 수 있을까?

이번 글에서는 React Query의 Dependent Queries를 활용한 실제 구현 사례를 작성해보겠다.

문제 상황

요구사항 및 서비스 플로우이다.

  1. 사용자가 버스 번호를 입력하면 이 후에 위치 정보가 서버로 전송됨
  2. 서버에서 위치를 처리하는데 약간의 시간이 필요함
  3. 위치가 처리되면 해당 위치 기반으로 버스 도착 정보를 조회할 수 있음
  4. 하지만 위치 처리 전에 버스 도착 정보를 요청하면 빈 결과가 반환됨

다양한 해결 방법들

방법 1: 간단한 타임아웃 접근

// ❌ 안타깝게도 이 방법은 예측 불가능
useEffect(() => {
  locationTracker.startTracking();
  
  // 2초 후에 버스 정보 가져오기... 하지만 항상 충분한 시간일까?
  setTimeout(() => {
    getBusArrival();
  }, 2000);
}, []);

 

이 방법의 문제점

  • 네트워크 상태에 따라 필요한 시간이 달라질 수 있음
  • 너무 짧으면 데이터를 놓치고, 너무 길면 UX가 저하됨

방법 2: Promise 체이닝

useEffect(() => {
  let mounted = true;
  
  locationTracker.startTracking()
    .then(() => new Promise(resolve => setTimeout(resolve, 2000)))
    .then(() => mounted && getBusArrival())
    .then(data => mounted && setExpectedBuses(data))
    .catch(error => console.error(error));
    
  return () => { mounted = false; };
}, []);
 

장단점

  • 장점: 명확한 실행 순서, 에러 처리 가능
  • 단점: 콜백 지옥과 유사한 문제, 상태 관리가 복잡함

방법 3: async/await + 상태 관리

useEffect(() => {
  let mounted = true;
  
  const fetchData = async () => {
    try {
      await locationTracker.startTracking();
      await new Promise(resolve => setTimeout(resolve, 2000));
      if (mounted) {
        const data = await getBusArrival();
        setExpectedBuses(data);
      }
    } catch (error) {
      console.error(error);
    }
  };
  
  fetchData();
  return () => { mounted = false; };
}, []);

 

장단점

  • 장점: 읽기 쉬운 코드, 비동기 처리가 자연스러움
  • 단점: 여전히 타임아웃에 의존, 수동 상태 관리 필요

방법 4: React Query의 Dependent Queries (현재 적용한 방법)

1. 위치 상태 확인 쿼리

const { data: locationStatus } = useQuery({
  queryKey: ['locationStatus'],
  queryFn: async () => {
    // 위치 추적이 시작되지 않았다면 시작
    if (!locationTracker.isTracking()) {
      locationTracker.startTracking();
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
    
    // 서버에서 위치를 처리했는지 확인
    try {
      await getBusArrival();
      return true; // 성공하면 위치가 처리된 것
    } catch {
      return false; // 실패하면 아직 처리 중
    }
  },
  refetchInterval: (data) => data ? false : 2000, // 성공하면 멈춤
  enabled: true,
});

 

  • 위치 추적 여부를 먼저 확인하고, 추적 중이 아니면 자동으로 시작
  • getBusArrival()을 시험 삼아 호출하여 서버가 위치를 처리했는지 확인하는 창의적인 방법
  • 성공하면 true를 반환하여 의존 쿼리 실행 허용
  • refetchInterval 함수를 사용하여 성공 후에는 폴링 중지

2. 의존성이 있는 버스 도착 정보 쿼리

const { data: expectedBuses = [] } = useQuery<BusInfo[]>({
  queryKey: ['busArrivals'],
  queryFn: getBusArrival,
  refetchInterval: 30000,
  enabled: locationStatus === true, // 🔑 핵심: 위치가 준비되었을 때만 실행
});
  • enabled 옵션으로 locationStatus가 true일 때만 실행되도록 설정
  • 30초마다 새로운 버스 도착 정보를 가져오도록 자동 갱신 설정
  • 기본값으로 빈 배열을 설정하여 undefined 에러 방지

 

React Query의 Dependent Queries란?

React Query 공식 문서에 따르면

"Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run."

https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries

 

Dependent Queries | TanStack Query React Docs

useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is r...

tanstack.com

 

Dependent Queries는 특정 쿼리가 다른 쿼리의 결과에 의존하는 경우 사용된다. enabled 옵션을 사용하면 된다.

작동 원리

1. enabled 옵션이 false인 동안 쿼리는 실행되지 않는다.
2. enabled가 true로 변경되면 쿼리가 실행된다.
3. 이를 통해 데이터 fetch의 순서를 제어할 수 있다.
// locationStatus가 true일 때만 expectedBuses 쿼리 실행
enabled: locationStatus === true

 

 

상태 관리 흐름

  • 초기 상태: status: 'pending', fetchStatus: 'idle'
  • 의존성 충족 시: status: 'pending', fetchStatus: 'fetching'
  • 데이터 로드 완료: status: 'success', fetchStatus: 'idle'

실제 구현 시 고려사항과 성능 최적화 팁

1. 폴링 최적화

refetchInterval: (data) => data ? false : 2000

 

최적화 포인트

  • 위치가 확인되면 더 이상 폴링하지 않게 한다.
  • 불필요한 네트워크 요청 제거로 성능 향상
  • 조건부 폴링으로 리소스 절약

2. 쿼리 무효화 전략

const queryClient = useQueryClient();

// 위치 변경 시 버스 정보 무효화
locationTracker.onLocationChange(() => {
  queryClient.invalidateQueries(['busArrivals']);
});

 

데이터 일관성 유지

  • 위치가 변경되면 기존의 버스 도착 정보는 더 이상 유효하지 않음
  • 쿼리 무효화로 자동으로 새로운 데이터 요청
  • 사용자에게 항상 최신 정보 제공

더 나은 해결 방안들

방법 5: Server-Sent Events (SSE)

// 클라이언트
useEffect(() => {
  // 위치 전송
  await postUserLocation({ latitude, longitude });
  
  // SSE 연결 - 서버가 준비되면 알림
  const eventSource = new EventSource('/api/bus-arrival-stream');
  
  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    setExpectedBuses(data);
    eventSource.close();
  };
  
  eventSource.onerror = (error) => {
    console.error('SSE Error:', error);
    eventSource.close();
  };
  
  return () => eventSource.close();
}, []);

 

단점

  • 장점: 실시간 통신, 서버 주도적 알림, 폴링 불필요
  • 단점: 서버 구현 복잡도 증가, 추가 프로토콜 이해 필요

방법 6: WebSocket

// 클라이언트
useEffect(() => {
  const ws = new WebSocket('ws://localhost:3000/bus-updates');
  
  ws.onopen = () => {
    // 위치 정보 전송
    ws.send(JSON.stringify({ type: 'location', data: { latitude, longitude } }));
  };
  
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === 'bus-arrivals') {
      setExpectedBuses(data.payload);
      ws.close();
    }
  };
  
  return () => ws.close();
}, []);

 

장단점

  • 장점: 양방향 통신, 실시간성, 더 복잡한 상호작용 가능
  • 단점: 더 복잡한 구현, 인프라 고려사항 증가

방법 7: 서버에서 통합 엔드포인트 제공

// 서버
app.post('/api/bus-tracking', async (req, res) => {
  const { busNumber, latitude, longitude } = req.body;
  
  // 1. 위치 저장
  await saveUserLocation(userId, latitude, longitude);
  
  // 2. 통합 처리 후 결과 반환
  const busArrivals = await getBusArrivalsForLocation(userId, busNumber);
  
  res.json({ arrivals: busArrivals });
});

이 방법이 가장 깔끔한 이유

  • 클라이언트 로직 단순화
  • 한 번의 요청으로 처리 완료
  • 서버에서 처리 시간 관리 가능

어떤 방식이 더 나을까?

현재 프로젝트에서의 선택 기준

  1. 서버 리소스가 제한적이라면: React Query + Dependent Queries
  2. 실시간성이 중요하고 서버 개선이 가능하다면: SSE 또는 WebSocket
  3. 가장 깔끔한 구조를 원한다면: 서버에서 통합 엔드포인트 제공

사실 이 문제의 근본적인 해결은 백엔드 API 설계의 개선에 있다. 클라이언트가 순차적 API 호출을 처리하는 것보다, 서버에서 모든 처리를 완료하고 한 번에 결과를 제공하는 것이 가장 이상적이다. 이는 클라이언트 로직을 단순화하고, 네트워크 레이턴시를 줄이며, 더 나은 사용자 경험을 제공한다. ㅎ 백엔드에게 바꿔달라 요청해봐야겠다.

 

자세한 코드는 pr 참고

https://github.com/Team-GomSun/FE/pull/18

 

feat: 버스 도착 전 위치 상태 확인 추가 by ssolfa · Pull Request #18 · Team-GomSun/FE

1️⃣ 어떤 작업을 했나요? (Summary) resolved refactor: API 요청 순서 지정 #17 위치 추적 후 버스 도착에 대한 종속 쿼리 구현 사용자 위치 정보 전송 후 이후에 주변 도착 버스 정보를 가져와야 하는데

github.com

 

결론

 

location 뒤에 버스 도착 정보인 arrival이 오는 걸 확신하는 코드가 되었다.

근데 아무리 생각해도 이런 경우 (주기적으로 get을 하는 것 보다)는

프론트에서 flag를 걸어서 확인하고 이후에 순차적으로 요청을 하는 것 보다

백엔드에서 조건이 성립했을 때 sse나 웹소켓으로 보내주는게 맞는 것 같다.

728x90