[댕누비/React] 공공 API 요청 한도 초과 대응

titleImg2.png

React/typescript + Firebase 기반으로 반려동물과 함께할 수 있는 국내 여행지를 소개하는 웹사이트 댕누비를 개발하며 겪었던 많은 오류들중에서도 제일 골치가 아팠던건 API 요청 초과 문제. 개발을 하면서도 계속 새로 호출하니 하다보면 요청량을 초과해서 결국 개발 중에도 페이지가 먹통이 되는 상황까지 벌어졌습니다...ㅠㅠ 어쨌든 이번 글에서는 그 문제를 어떻게 해결했는지, 그리고 캐싱 구조를 어떻게 설계했는지를 기록해보려고 합니다!

문제 상황

  • 지역별 페이지, 테마 페이지에서 매번 API 요청
  • 지역변경, 테마변경과 같은 페이지 전환, 새로고침 등등에서도 동일한 API 호출을 반복
  • → 결국 일일 1000건 제한 초과로 LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR, 22 오류 발생

해결 방법

캐싱 구조 도입

지역별 / 테마별로 고정된 데이터는 Cloud Function을 통해 미리 API로 호출하고, 결과를 Firestore에 저장한 뒤, 1시간마다 만료, 캐시 데이터가 없을 시 다시 캐싱 도록 변경했습니다. 

지역별 페이지 캐싱 코드

// src/api/cacheAPI.ts

const TTL_MS = 1000 * 60 * 60;  // 1시간

export async function getCachedRegion(areaCode: number, page = 1) {
  const ref = doc(db, 'regionPlaces', `${areaCode}_page_${page}`);
  const snap = await getDoc(ref);

  if (snap.exists()) {
    const { places, updatedAt } = snap.data() as { places: Place[]; updatedAt: Timestamp };
    const age = Date.now() - updatedAt.toMillis();

    if (age < TTL_MS) {
      console.log(`✅[캐시 히트] regionPlaces/${areaCode}_page_${page} (age: ${Math.floor(age / 1000)}s)`);
      return places;
    }
    console.log(`⚠️[캐시 만료] regionPlaces/${areaCode}_page_${page}`);
  } else {
    console.log(`⚠️[캐시 없음] regionPlaces/${areaCode}_page_${page}`);
  }

  // 캐시 없거나 만료 → API 호출
  const fresh = await fetchRegionPlacesFromAPI(areaCode, page);
  await setDoc(ref, { places: fresh, updatedAt: serverTimestamp() });
  return fresh;
}

 

테마별 페이지 캐싱 코드

export async function getCachedTheme(
  themeKey: ThemeKey,
  page = 1
): Promise<Place[]> {
  const ref = doc(db, 'themePlaces', `${themeKey}_page_${page}`);
  const snap = await getDoc(ref);

  if (snap.exists()) {
    const { places, updatedAt } =
      snap.data() as { places: Place[]; updatedAt: Timestamp };
    const age = Date.now() - updatedAt.toMillis();

    if (age < TTL_MS) {
      console.log(`✅[캐시 히트] ${themeKey}_page_${page} (age ${Math.floor(age/1000)}s)`);
      return places;
    } else {
      console.log(`⚠️[캐시 만료] ${themeKey}_page_${page} (age ${Math.floor(age/1000)}s)`);
    }
  } else {
    console.log(`⚠️[캐시 없음] ${themeKey}_page_${page}`);
  }

  console.log(`🔄[API 호출] fetchThemePlacesFromAPI(${themeKey}, ${page})`);
  const fresh = await fetchThemePlacesFromAPI(themeKey, page);
  await setDoc(ref, { places: fresh, updatedAt: serverTimestamp() });
  return fresh;
}

functions>src>fetchExternal까지 미리 API 호출하도록 해준 뒤에 Firebase console - Firestore Database에 들어가면 잘 저장된 것을 확인할 수 있다!

etc-image-1
흑흑 드디어 22 오류 해결...

결과 요약

항목 개선 전 개선 후
지역/테마 데이터 매번 API 실시간 호출 Firestore TTL 캐싱
요청 수 지역 39개 + 테마 필터 → 다수 병렬 요청 최초 1회만 요청 후 재사용
오류 발생 22,  LIMITED_NUMBER_OF_SERVICE_REQUESTS 거의 발생 안 함

 

공공 API는 실시간 호출보다 먼저 캐싱 구조부터 설계하는 게 맞는 것 같습니다

처음에는 편하게 호출해서 바로 쓰고 싶었지만, 개발 과정에서도 계속 실시간 API를 요청하다 보니 요청 한도 초과, 응답 지연, 테스트 불가 같은 문제가 반복적으로 발생했습니다.

게다가 지역별이나 테마별 여행지 정보는 실시간성이 필요한 데이터라기보다는 가변성이 낮아, 1시간~1일 주기 캐싱만 해도 충분한 것 같아요. 결국 이 구조를 설계하면서 체감한 건, 속도도 빨라지고, 비용도 줄고, 사용자 경험도 안정적이라는 것. 실시간 호출보다 데이터 흐름 전체를 제어할 수 있는 구조가 먼저 필요하다는 걸 배웠습니다.