비정상 API 호출 패턴 감지 및 차단을 위한 클라이언트 측 Rate Limiting 레이어 구축

Nayoung·2026년 3월 24일

1. 문제 정의

발단: 픽잇의 무한 루프로 인한 API 무한 호출 사건

픽잇은 react-router v7 의 비긴급 업데이트와 Suspense 의 startTransition 시 fallback 표시 안함 의 조합으로 인해 백그라운드에서 조용히 API 를 무한 호출한 사건이 있었어요.

(자세한 트러블 슈팅은 여기에서)

해당 사건은 해결이 됐지만, 백그라운드에서 사용자 몰래 조용히 API 가 초에 수십번 요청됐던 버그는 저희의
서버 비용 폭탄!으로 이어질 수 있던 심각한 버그였죠.

그래서 이런 비정상적인 서버 요청에 대해 서버 측 뿐만 아니라 프론트엔드에서도 방어 조치가 있으면 좋을 것 같다는 생각이 들었어요.


또 언제 문제가 될 수 있을까?

당장은 해당 문제를 Suspense retry 버그로만 겪었지만, 상태 업데이트 리렌더링이 잦은 리액트 프로젝트를 하면서 예기치 못한 무한 루프는 앞으로도 언제든지 벌어질 수 있다고 생각해요. 안티 패턴을 다시 한 번 리마인드 할 겸, 실수하기 쉬운 무한 루프의 두 가지 예시를 나열해볼게요.

☝️ useState, useEffect 와 같은 훅의 잘못된 사용

deps 의 누락이나 서로 호출하는 등의 실수로 무한 루프라는 끔찍한 버그가 다시 터질 수 있어요. 물론 이는 개발자의 명백한 실수이지만, 코드가 방대해지고 맥락이 길어질 수록 결국 언젠가 실수할 수 있다고 생각해요. 실제로 이런 무한 루프를 겪은 프론트엔드 개발자들의 경험을 푸는 스레드가 있네요.

reference: https://www.reddit.com/r/react/comments/1mloqcw/ever_accidentally_create_an_infinite_loop_in_react/

게다가 cloudflare 라는 대형 서비스에서조차도 25년에 useEffect의 잘못된 참조로 인한 무한루프를 겪었어요.

reference: https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/

✌️ 부모-자식 상태 변경 무한루프

자식이 부모의 상태를 변경하는 구조가 있다면, useEffect 에 해당 setter를 의존성 배열에 등록하는 순간 무한 루프가 벌어져요. setter로 값을 변경하면, setter자체도 변경되기 때문이에요.

이 또한 애초에 이렇게 작성하면 좋았을 안티 패턴이긴 하겠지만, 이처럼 별 생각 없이 사용했던 코드들이 은밀히 이런 심각한 문제를 발생시킬 수 있어요.

이런 휴먼 에러 말고도 저희가 겪었던 Suspense 의 동작과 라이브러리의 내부적인 업데이트로 인한 문제처럼, 의존성에 의해서도 알지 못하는 새 무한 루프는 언제든 재발할 수 있는 일이라고 판단했어요.

그리고 이러한 무한 루프 속에 API 요청이 섞여있다면 실제 서버 요금 폭탄으로 이어질거에요😱


목표: 클라이언트의 무한 루프에 따른 API 무한 요청 방어 - rate limit

그래서 핵심 문제 해결의 목표는 위와 같이 정의했어요.

DoS 공격이나 서버의 과부하에 초점을 맞춘 해결 과정은 아닙니다!
앞으로 예기치 못한 클라이언트의 무한 루프가 발생했을 때 적절한 대처를 즉시할 수 있도록 고안하는 글이에요.


프론트엔드에서 rate limit 도입의 이점

제가 구상한 rate limit 방법은 ‘수 초내 수십번의 요청이 갈 경우 비정상적인 API 요청이라고 판단, 에러 토스트 안내 후 에러 페이지로 이동’이에요.

그런데 백엔드에서도 429 Too Many Request 로 방어해주시기로 했는데요, 서버에서 이미 막아주고 있는데 프론트에서도 막으면 좋을 이유는 뭘까요?

☝️ 사용자 경험 개선 및 서버 부담 감소

제가 생각했을 때 가장 좋은 점은, 예상치 못한 버그 상황 시에도 사용자 경험을 해치지 않게해주는 점이에요.

서버에서 429 에러를 주었다는 건 해당 클라이언트의 요청이 서버가 설정한 시간만큼 블락되는 것을 의미하는데요, 429 에러가 항상 사용자의 악의적인 테러에만 발생하는 것이 아니라 저희 개발자들의 실수로 벌어진 에러일 경우에도 사용자들은 해당 시간을 기다려야해요. 이는 곧 사용자 탈주 및 서비스 불신으로 이어질 수 있어요. 그래서 서버에서 사용자를 차단해버리기 전에 클라이언트에서 최대한 서버에 무리한 요청이 가지 않도록 방어해주는 게 좋다고 판단했어요.

✌️ API 요청 제한에 유연한 대응 가능

서버에서 만약 같은 사용자가 같은 API 요청에 제한을 10분에 100회할 경우에 블락을 하기로 했다고 가정해볼까요? 그럼 서버에서 결정한 정책은 개인의 무리한 사용에 대한 제재가 아닌 서버 부담 완화에 대한 목적일 수 있어요. 또한 서버는 이미 99회 불필요한 요청을 받고 나서야 블락을 하게 돼요.

그렇다고 10분에 100회 라는 기준을 마음대로 줄일 순 없어요. 보통 사용자의 IP 를 기반으로 블락을 하기 때문에 같은 공용 네트워크를 사용 중인 사용자들은 같은 사용자의 요청으로 카운트되거든요.

하지만 프론트에서 rate limit 를 도입하게 된다면 주 목적은 프론트엔드에서의 무한 루프로 인한 수 초 내 수십번의 요청이 가는 것에 대한 차단이므로 서비스 정책 결정에 영향을 주지 않고 개인에 대해 유연하게, 더 엄격한 기준으로 방어할 수 있어요.


프론트엔드에서 rate limit 도입의 단점

☝️ 반복 요청을 허용해야하는 예외 상황 → 관리 포인트 증가

모든 API 요청에 일괄적인 기준을 적용할 경우, 정상적인 서비스 이용 시나리오에서도 차단이 발생할 수 있는 오탐 가능성이 존재해요.

  • 대량의 데이터 초기화/동기화: 대시보드 진입 시 여러 개의 위젯 데이터를 동시에 호출하거나, 사용자의 액션 한 번에 수십 개의 독립적인 리소스를 패치해야 하는 경우.
  • 실시간 성격의 폴링(Polling): 특정 작업의 완료 상태를 확인하기 위해 짧은 주기로 반복 요청을 보내는 로직이 포함된 경우.
  • 사용자의 의도적인 광클: 검색 필터를 빠르게 여러 번 변경하거나, 페이지네이션을 극도로 빠르게 넘기는 등 예측하기 어려운 사용자 행동 패턴.

따라서 비정상적인 동작이라고 의심할 수 있는 충분한 기준(현재로는 5초 내 20번 이상의 같은 API 요청)을 세우고, 추후 기능이 확장될 때에도 우리 서비스는 client rate limit 가 동작하고 있음을 의식하고 있어야해요.

결과적으로, 이러한 특수 사례들을 '예외 처리'하기 위해 API별로 rate limit 옵션을 세분화해야 할 수 있으며, 이는 프로젝트 규모가 커질수록 유지보수해야 할 화이트리스트 관리 포인트가 늘어남을 의미합니다.

✌️ 보안책은 아니다

프론트엔드의 이러한 방어코드는 개발자 도구로 쉽게 우회할 수 있어요. 따라서 이 코드 반영은 DoS 공격 등에 대한 방어코드는 아닌 점을 명심해야해요.


결단

그럼에도 불구하고, 픽잇의 현재 서비스 규모와 데이터 처리 특성을 고려했을 때 Client Rate Limit 도입의 실익이 더 크다고 판단했어요.

특히 '5초 내 20회'라는 임계치는 다음의 실측 지표를 바탕으로 설정되었습니다:

  • 정상 시나리오: 페이지 진입 시 동일 API 호출은 평균 1회이며, 사용자 인터랙션이 집중되는 상황(빠른 클릭 등)에서도 초당 동일 API 요청은 3회를 넘지 않음을 확인했습니다. (또한 빠른 클릭 등에 대한 대응은 쓰로틀링 등의 대응이 더 적절하다고 생각해요.)
  • 이상 시나리오: 실제 무한 루프 재현 시, 초당 약 20회 이상의 폭발적인 API 요청이 관찰되었습니다.

따라서 정상적인 사용 범주에 충분한 간격을 두면서도, 비정상적인 루프를 즉각 감지할 수 있는 최적의 지점으로 5초 내 20회라는 기준을 도출했습니다. 추후 대량 데이터 처리가 필요한 기능 확장 시에는 API별로 임계치를 세분화하여 대응할 계획이에요.


2. 행동

위의 문제 정의에 따라 저는 사용자의 API 요청에 대해 제한을 두도록 결정했어요. 구체화를 해볼게요.


문제 해결 방법

우선 비정상적인 API 요청이라는 기준은 ‘수 초 내 수십번의 요청이 갈 경우’ 라고 세워두겠습니다.

일단 어떤 이유에서 API가 비정상적으로 빠르게 반복 요청된 것인지 확신할 수 없으므로 설정한 {Retry After}초 까지 대기 후, 그럼에도 같은 문제가 N 회 이상 반복되면 해당 페이지에서 계속 비정상적인 상황이 나아지지 않을 것이라고 판단하고 에러 페이지로 보내는 방법을 생각했어요.

해당 에러 페이지에는 저희 픽잇에 에러 보고서를 보낼 수 있는 Sentry 기능과 메인화면으로 돌아가기 버튼을 제공할거에요.

실제로 구글의 SRE(Site Reliability Engineering, 사이트 신뢰성 공학. 구글에서 제안한 코딩과 자동화 기술로 시스템의 신뢰성을 높이는 철학과 실무를 정리한 문서) 의 과부하 처리(Handling Overload) 챕터에서도 이러한 클라이언트의 제한 방법에 대해 다루고 있어요.

reference: https://sre.google/sre-book/handling-overload/

위의 글에서 클라이언트는 서버의 지속된 작업 과부하 시 3회 재시도 후 재시도를 그만 두고 호출자에게 알리고 있어요.

(백엔드에서 CPU 실제 리소스 사용량에 따른 고객별 오류 응답과 그 후 클라이언트가 적응형 스로틀링으로 부하를 조절하는 아이디어 등을 다루고 있어요. 최적화를 위한 계산식 등 꽤나 흥미로운 내용이니 읽어봐도 좋을 것 같아요ㅎㅎ)

요청이 이미 3번 실패한 경우 다시 시도해도 해결될 가능성이 낮다는 판단에 따른 것인데요, 이 판단에 공감이 되어 Retry After 초 간격으로 N회 재시도 후에도 같은 현상(비정상적으로 반복되는 API 요청)이라면 해당 페이지에서 계속 비정상적인 API 요청이 일어나 서버에 부담이 갈 것이라고 판단, 사용자에게 피드백 메세지 후 오류 페이지로 이동시키는 결정입니다.


정책

  • 5초에 20회 이상 동일한 API 요청이 발생할 경우 해당 API 요청을 중단하고 사용자 토스트 안내
  • 해당 속성은 빌트인으로 on/off 가능
  • 주된 무한 루프 대상 API 인 GET 메서드에 한 해 전역 적용(이미지 대량 업로드 등 API 가 많이 요청될 수 있는 이외 메서드들은 기본 설정 off

의사 결정 사항

apiClient 에서 rate limit 가 발생할 시 window.location.replace() 를 이용해 error/too-many-requests 페이지로 직접 이동

  • 무한 루프가 의심되는 상황임을 가정하고, navigate 이동이 아닌 window.location.replace 의 새로고침+url 이동을 통해 SPA 의 상태 초기화 및 안정적인 페이지 이동

rate limit 에 대한 관리는 전역적으로 하나만 하면되므로 싱글톤처럼 작성 (하나의 store)

고려한 다른 대안은 없나요? - 쓰로틀링/디바운스, 요청 큐잉

☝️ 쓰로틀링(Throttling) / 디바운스(Debounce)

  • 한계: 특정 UI 이벤트(버튼 클릭, 검색 입력)에는 효과적이지만, 프론트엔드의 비즈니스 로직이나 라이브러리 간의 의존성 꼬임으로 발생하는 '코드 레벨의 무한 루프'를 근본적으로 차단하기엔 부족해요.
  • 결정: 쓰로틀링은 '정상적인 사용자의 과도한 액션'을 제어하는 용도로 개별 컴포넌트에서 유지하고, 이번 Rate Limit은 '비정상적인 시스템 동작'을 감지하는 시스템 전체의 안전장치(Fail-safe)로 이원화하여 운영하기로 했습니다.

✌️ 요청 큐잉(Request Queueing) 및 중복 제거

아래는 요청 큐잉을 관리하는 방향으로 예방한 개발자 분이 작성한 아티클이에요.

reference: https://kasterra.github.io/preventing-useEffect-infinite-loop/

  • 아이디어: 동일한 API 요청이 짧은 시간에 몰릴 경우 큐(Queue)에 쌓아두고 하나만 실행하거나 순차적으로 처리하는 방식이에요.
  • 픽잇에서의 판단: 픽잇은 현재 복잡한 데이터 동기화보다는 실시간 응답성이 중요한 서비스입니다. 만약 무한 루프 상황에서 요청을 큐에 쌓기만 한다면, 브라우저의 메모리 점유율이 급격히 상승하여 결국 탭이 먹통이 되는 현상을 막을 수 없습니다.
  • 결정: 요청을 '지연'시키는 것보다, 비정상 상황임을 감지하는 즉시 호출을 중단하고 상태를 초기화하는 것이 시스템 안정성과 비용 방어 측면에서 가장 확실한 해법이라고 판단했습니다.

에러 모니터링

이제 백엔드의 429 인지 무한루프인지에 따라 상황을 기록해 Report 를 Sentry 로 전송해요.

사용자가 정확히 어떤 페이지에서 어떤 에러를 겪었는지, 어떤 API 의 문제였는지 등을 보고 받아 빠른 버그 추적이 가능하도록 마련했어요.

보고 항목

[공통]

rate_limit_source : rate limit이 어디서 걸렸는지 구분. client = 클라이언트(우리 코드)에서 막음, server = 서버가 429로 막음.

page : 어느 페이지에서 발생했는지. window.location.pathname + window.location.search 

api_method: 어떤 HTTP 메서드로 요청했는지

 api_endpoint : 어떤 API 경로로 요청했는지. 예: /v1/rooms/123/v1/rooms/123/menus

[클라이언트 무한루프 의심 시]

  • rate_limit_timestamps: 해당 api_method + api_endpoint 조합으로 언제 몇 번 호출됐는지. number[] — 각 요청 시점의 Date.now()(ms) 배열. 윈도우 내 호출 이력 스냅샷.
  • rate_limit_request_count : 위 타임스탬프 배열의 길이 = 해당 API로 윈도우 내에 기록된 요청 횟수. 이 값이 한도(예: 20)에 도달해서 막힌 상황.

[서버 429 시]

server_message : 서버가 429 응답 body에 넣어 준 메시지 (있는 경우만)


백엔드의 429 Error 대응

백엔드에서 Too Many Request 에 대한 대응을 처리해주셨는데요, 이에 따라 저희의 getErrorMessageByCodeERROR_CODE 에러 메세지 객체에도 429 에러 상황을 추가해주었습니다.


3. 결과

도입 전 후 성능 비교

☝️ 성능 벤치마크

fetch 에 mock 을 적용해 rate limit 가 적용된 apiClient와 적용되지 않은 baseline 의 Rate Limit 로직 추가로 인한 오버헤드는 요청당 약 0.0019ms로 확인했어요. 이는 실제 네트워크 환경의 평균 응답 속도(약 50ms) 대비 0.004% 수준의 연산량으로, 사용자 체감 성능에 미치는 영향은 사실상 제로에 가까워요.

오히려 1000회 연속 호출 시에도 총 소요 시간이 2ms 내외로 관리되는 점을 보아, 비정상적인 상황(무한 루프 등) 발생 시 시스템을 안정적으로 방어할 수 있는 저비용·고효율 안전장치라고 생각해요.

또한 단순 연산 오버헤드 확인을 넘어, 실제 무한 루프 상황 재현 테스트를 진행했는데요, 초당 수십 번의 요청이 발생하는 환경에서, 로직은 임계치 도달 즉시 비정상 패턴을 감지하고 차단에 성공했습니다. 즉, 서버에 유의미한 부하가 가기 전 프론트엔드 최전방에서 방어 기능을 수행함을 검증했어요.

✌️ CPU 점유율

Bottom-Up 프로파일링 결과, 전체 API 요청 프로세스에서 Rate Limit 핵심 로직(appendTimestamp)이 차지하는 CPU 점유율은 단 5.4%(0.2ms)에 불과해요.

이는 브라우저의 기본 fetch 동작(56.7%) 대비 약 10분의 1 수준으로, 시스템 자원을 거의 소모하지 않는 안전한 설계임을 확인했어요.


결론

1. 정량적 오버헤드 검증

  • Rate Limit 로직 도입 시 발생하는 연산 오버헤드를 요청당 약 0.0019ms(네트워크 지연 시간 대비 0.004% 수준)로 억제하여, 서비스 성능 저하 없이 시스템 안정성 확보

2. 비즈니스 비용 보호

  • 클라이언트 단 선제적 차단 로직(5초 내 20회 초과 시)을 통해, 프론트엔드 버그로 인한 불필요한 API 호출을 차단하여 클라우드 인프라 비용 낭비 리스크 방어

3. 사용자 경험 유지

  • 서버 측 IP 차단(429 Too Many Requests) 전 단계에서 비정상 요청을 감지하고 전용 에러 페이지 및 피드백 루프를 제공
  • 최악의 상황에서도 사용자 이탈을 방지하고 서비스 신뢰도 유지

4. 모니터링 및 운영 체계 구축

  • Sentry 커스텀 태그 및 User Feedback 연동을 통해 무한 루프 발생 시 실시간 상황 스냅샷 수집 체계 구축
  • 장애 대응 시간(MTTR) 단축

4. 남은 기술 부채

프론트엔드에서 최악이지만 흔하다면 흔할 수 있는 무한 루프에 대한 대응을 해보았는데요,

적절한 방법을 찾기위해 공부하다보니 , 특히 구글 SRE 에서 고안한 처리 과부하 방법이 꽤 인상적이었어요. 클라이언트와 백엔드가 협력해서 과부하 상황을 원활하게 처리하는 아이디어가 꽤 흥미로웠고, 실제 저희 서비스가 대규모 트래픽을 갖게된다면 꼭 도입하면 좋은 방식이라고 생각했어요. 또, 데이터센터의 대표적인 로드 밸런싱 기법 중 하나인 클라이언트 쓰로틀링도 전역적인 API 요청에 도입할지 팀과 함께 논의해보고 싶네요.

이 방어 코드가 도입됨으로써 이제 우리 픽잇 팀은 무한 루프로 인한 '비용 폭탄' 걱정 없이 더 과감하게 리팩터링하고 기능을 확장할 수 있게 되었어요.

profile
문제의 근본적인 원인을 탐구하고 해결하는 것을 좋아하는 프론트엔드 개발자, 진나영입니다!

0개의 댓글