픽잇은 react-router v7 의 비긴급 업데이트와 Suspense 의 startTransition 시 fallback 표시 안함 의 조합으로 인해 백그라운드에서 조용히 API 를 무한 호출한 사건이 있었어요.
(자세한 트러블 슈팅은 여기에서)
해당 사건은 해결이 됐지만, 백그라운드에서 사용자 몰래 조용히 API 가 초에 수십번 요청됐던 버그는 저희의
서버 비용 폭탄!으로 이어질 수 있던 심각한 버그였죠.
그래서 이런 비정상적인 서버 요청에 대해 서버 측 뿐만 아니라 프론트엔드에서도 방어 조치가 있으면 좋을 것 같다는 생각이 들었어요.
당장은 해당 문제를 Suspense retry 버그로만 겪었지만, 상태 업데이트 리렌더링이 잦은 리액트 프로젝트를 하면서 예기치 못한 무한 루프는 앞으로도 언제든지 벌어질 수 있다고 생각해요. 안티 패턴을 다시 한 번 리마인드 할 겸, 실수하기 쉬운 무한 루프의 두 가지 예시를 나열해볼게요.
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 요청이 섞여있다면 실제 서버 요금 폭탄으로 이어질거에요😱
그래서 핵심 문제 해결의 목표는 위와 같이 정의했어요.
DoS 공격이나 서버의 과부하에 초점을 맞춘 해결 과정은 아닙니다!
앞으로 예기치 못한 클라이언트의 무한 루프가 발생했을 때 적절한 대처를 즉시할 수 있도록 고안하는 글이에요.
제가 구상한 rate limit 방법은 ‘수 초내 수십번의 요청이 갈 경우 비정상적인 API 요청이라고 판단, 에러 토스트 안내 후 에러 페이지로 이동’이에요.
그런데 백엔드에서도 429 Too Many Request 로 방어해주시기로 했는데요, 서버에서 이미 막아주고 있는데 프론트에서도 막으면 좋을 이유는 뭘까요?
제가 생각했을 때 가장 좋은 점은, 예상치 못한 버그 상황 시에도 사용자 경험을 해치지 않게해주는 점이에요.
서버에서 429 에러를 주었다는 건 해당 클라이언트의 요청이 서버가 설정한 시간만큼 블락되는 것을 의미하는데요, 429 에러가 항상 사용자의 악의적인 테러에만 발생하는 것이 아니라 저희 개발자들의 실수로 벌어진 에러일 경우에도 사용자들은 해당 시간을 기다려야해요. 이는 곧 사용자 탈주 및 서비스 불신으로 이어질 수 있어요. 그래서 서버에서 사용자를 차단해버리기 전에 클라이언트에서 최대한 서버에 무리한 요청이 가지 않도록 방어해주는 게 좋다고 판단했어요.
서버에서 만약 같은 사용자가 같은 API 요청에 제한을 10분에 100회할 경우에 블락을 하기로 했다고 가정해볼까요? 그럼 서버에서 결정한 정책은 개인의 무리한 사용에 대한 제재가 아닌 서버 부담 완화에 대한 목적일 수 있어요. 또한 서버는 이미 99회 불필요한 요청을 받고 나서야 블락을 하게 돼요.
그렇다고 10분에 100회 라는 기준을 마음대로 줄일 순 없어요. 보통 사용자의 IP 를 기반으로 블락을 하기 때문에 같은 공용 네트워크를 사용 중인 사용자들은 같은 사용자의 요청으로 카운트되거든요.
하지만 프론트에서 rate limit 를 도입하게 된다면 주 목적은 프론트엔드에서의 무한 루프로 인한 수 초 내 수십번의 요청이 가는 것에 대한 차단이므로 서비스 정책 결정에 영향을 주지 않고 개인에 대해 유연하게, 더 엄격한 기준으로 방어할 수 있어요.
모든 API 요청에 일괄적인 기준을 적용할 경우, 정상적인 서비스 이용 시나리오에서도 차단이 발생할 수 있는 오탐 가능성이 존재해요.
따라서 비정상적인 동작이라고 의심할 수 있는 충분한 기준(현재로는 5초 내 20번 이상의 같은 API 요청)을 세우고, 추후 기능이 확장될 때에도 우리 서비스는 client rate limit 가 동작하고 있음을 의식하고 있어야해요.
결과적으로, 이러한 특수 사례들을 '예외 처리'하기 위해 API별로 rate limit 옵션을 세분화해야 할 수 있으며, 이는 프로젝트 규모가 커질수록 유지보수해야 할 화이트리스트 관리 포인트가 늘어남을 의미합니다.
프론트엔드의 이러한 방어코드는 개발자 도구로 쉽게 우회할 수 있어요. 따라서 이 코드 반영은 DoS 공격 등에 대한 방어코드는 아닌 점을 명심해야해요.
그럼에도 불구하고, 픽잇의 현재 서비스 규모와 데이터 처리 특성을 고려했을 때 Client Rate Limit 도입의 실익이 더 크다고 판단했어요.
특히 '5초 내 20회'라는 임계치는 다음의 실측 지표를 바탕으로 설정되었습니다:
따라서 정상적인 사용 범주에 충분한 간격을 두면서도, 비정상적인 루프를 즉각 감지할 수 있는 최적의 지점으로 5초 내 20회라는 기준을 도출했습니다. 추후 대량 데이터 처리가 필요한 기능 확장 시에는 API별로 임계치를 세분화하여 대응할 계획이에요.
위의 문제 정의에 따라 저는 사용자의 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 요청이 일어나 서버에 부담이 갈 것이라고 판단, 사용자에게 피드백 메세지 후 오류 페이지로 이동시키는 결정입니다.
apiClient 에서 rate limit 가 발생할 시 window.location.replace() 를 이용해 error/too-many-requests 페이지로 직접 이동
rate limit 에 대한 관리는 전역적으로 하나만 하면되므로 싱글톤처럼 작성 (하나의 store)
☝️ 쓰로틀링(Throttling) / 디바운스(Debounce)
Rate Limit은 '비정상적인 시스템 동작'을 감지하는 시스템 전체의 안전장치(Fail-safe)로 이원화하여 운영하기로 했습니다.✌️ 요청 큐잉(Request Queueing) 및 중복 제거
아래는 요청 큐잉을 관리하는 방향으로 예방한 개발자 분이 작성한 아티클이에요.
reference: https://kasterra.github.io/preventing-useEffect-infinite-loop/
이제 백엔드의 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에 넣어 준 메시지 (있는 경우만)
백엔드에서 Too Many Request 에 대한 대응을 처리해주셨는데요, 이에 따라 저희의 getErrorMessageByCode 와 ERROR_CODE 에러 메세지 객체에도 429 에러 상황을 추가해주었습니다.

fetch 에 mock 을 적용해 rate limit 가 적용된 apiClient와 적용되지 않은 baseline 의 Rate Limit 로직 추가로 인한 오버헤드는 요청당 약 0.0019ms로 확인했어요. 이는 실제 네트워크 환경의 평균 응답 속도(약 50ms) 대비 0.004% 수준의 연산량으로, 사용자 체감 성능에 미치는 영향은 사실상 제로에 가까워요.
오히려 1000회 연속 호출 시에도 총 소요 시간이 2ms 내외로 관리되는 점을 보아, 비정상적인 상황(무한 루프 등) 발생 시 시스템을 안정적으로 방어할 수 있는 저비용·고효율 안전장치라고 생각해요.
또한 단순 연산 오버헤드 확인을 넘어, 실제 무한 루프 상황 재현 테스트를 진행했는데요, 초당 수십 번의 요청이 발생하는 환경에서, 로직은 임계치 도달 즉시 비정상 패턴을 감지하고 차단에 성공했습니다. 즉, 서버에 유의미한 부하가 가기 전 프론트엔드 최전방에서 방어 기능을 수행함을 검증했어요.


Bottom-Up 프로파일링 결과, 전체 API 요청 프로세스에서 Rate Limit 핵심 로직(appendTimestamp)이 차지하는 CPU 점유율은 단 5.4%(0.2ms)에 불과해요.
이는 브라우저의 기본 fetch 동작(56.7%) 대비 약 10분의 1 수준으로, 시스템 자원을 거의 소모하지 않는 안전한 설계임을 확인했어요.
프론트엔드에서 최악이지만 흔하다면 흔할 수 있는 무한 루프에 대한 대응을 해보았는데요,
적절한 방법을 찾기위해 공부하다보니 , 특히 구글 SRE 에서 고안한 처리 과부하 방법이 꽤 인상적이었어요. 클라이언트와 백엔드가 협력해서 과부하 상황을 원활하게 처리하는 아이디어가 꽤 흥미로웠고, 실제 저희 서비스가 대규모 트래픽을 갖게된다면 꼭 도입하면 좋은 방식이라고 생각했어요. 또, 데이터센터의 대표적인 로드 밸런싱 기법 중 하나인 클라이언트 쓰로틀링도 전역적인 API 요청에 도입할지 팀과 함께 논의해보고 싶네요.
이 방어 코드가 도입됨으로써 이제 우리 픽잇 팀은 무한 루프로 인한 '비용 폭탄' 걱정 없이 더 과감하게 리팩터링하고 기능을 확장할 수 있게 되었어요.