마지막 업데이트 2026-07-22
/client-guest) 콘솔에 10초마다 HEAD https://httpbin.org/status/200 net::ERR_FAILED 503 +
CORS policy: No 'Access-Control-Allow-Origin' header 에러가 반복적으로 찍히는 케이스.
분석 결과 원인은 외부 무료 서비스(httpbin.org)의 503 다운이며,
리소스 다운로드 · AI 세션 · 미디어 서버 어디에도 영향이 없는 콘솔 노이즈로 확인됨.
최종 결정(2026-06-05 갱신): C안 채택 — "사용자 인터넷 전체 다운 vs 우리 서버 다운"을 구분하는 진단 가치를
살리기 위해, 불안정한 httpbin.org 를 독립적이고 신뢰성 높은 연결성 엔드포인트(예: gstatic.com/generate_204)로
교체하고, 현재 죽어 있는 determineIssue/issue 신호를 실제 소비처에 연결한다.
| 증상 | 게스트 콘솔에 10초 주기로 httpbin.org/status/200 503 + CORS 차단 에러 반복 |
| 발생 위치 | apps/web/shared/lib/use-network-quality.ts — measureExternalLatency() |
| 호출 주기 | setInterval(checkQuality, 10000) — 게스트 페이지 동안 10초마다 |
| 요청 | fetchWithAlert("https://httpbin.org/status/200", { method: "HEAD", mode: "cors", cache: "no-store" }) |
| 실측 근거 | LogRocket 세션(019e922b…) — httpbin 503 구간 중에도 S3 리소스 25개 전부 정상 캐싱 |
| 관련 시스템 | V2 (FSD) 게스트 페이지 — 네트워크 품질 표시 위젯 |
근본 원인 외부 무료 테스트 서비스 httpbin.org 의 503 Service Unavailable. httpbin.org 는 공개 무료 서비스라 과부하·다운이 잦고, 현재 503 을 반환 중. 우리 코드 버그가 아님.
CORS 는 503 의 부수 증상
httpbin 이 503 에러를 낼 때 그 에러 응답에는 Access-Control-Allow-Origin 헤더가 없다.
그래서 mode: "cors" 요청의 응답 읽기가 브라우저에 의해 차단 → CORS 에러 + ERR_FAILED 까지 같이 찍힘.
즉 CORS 에러는 별개 문제가 아니라 503 응답의 헤더 부재에서 파생된 것.
서비스 영향 없음
리소스 다운로드 · AI 세션 · 미디어 서버 어디에도 영향 없음.
호출은 try/catch 로 감싸져 실패해도 throw 하지 않고 externalOk=false 로만 처리됨.
게다가 이 외부 체크 결과는 코드 어디서도 읽지 않는 죽은 데이터(아래 4·5장).
유일한 실질 피해
콘솔 오염(10초마다 에러 스택) 과, 외부 연결이 정상이어도 항상 비정상으로 판정되는
issue 필드 오판 — 그런데 그 issue 를 읽는 코드도 없어 사용자 영향 0.
useNetworkQuality 는 "사용자 네트워크 문제"인지 "우리 서버 문제"인지 구분하기 위해 두 곳을 동시에 측정한다:
// apps/web/shared/lib/use-network-quality.ts
async function measureExternalLatency() { // 외부 인터넷 도달성
await fetchWithAlert("https://httpbin.org/status/200",
{ cache: "no-store", method: "HEAD", mode: "cors" });
}
async function measureServerLatency() { // 우리 서버 도달성
await fetchWithAlert("/api/health",
{ cache: "no-store", method: "HEAD" });
}
// determineIssue(externalOk, serverOk):
// external O / server X → "server" (우리 서버 문제)
// external X / server X → "user-network" (사용자 망 문제)
// external O / server O → "none"
mode:"cors" 요청의 응답을 브라우저가 차단.use-network-quality.ts 의 try/catch 가 잡아 externalLatency=null, externalOk=false 로만 처리. 앱 흐름 영향 없음./api/health(서버 체크)는 동일 origin이라 CORS 무관하며 정상 동작. 문제는 외부 체크의 httpbin 의존 하나뿐.
| 대상 | 영향 | 근거 |
|---|---|---|
| 리소스(S3 영상) 다운로드 | 영향 없음 | use-resource-cache.ts 의 /api/resources/urls + fetchResumable(presignedUrl) 경로는 httpbin 과 완전 별개. CORS 는 요청별·origin별 격리라 전파 안 됨. |
| AI 세션 (OpenAI Realtime) | 영향 없음 | 별도 WebRTC/토큰 경로. 세션 로그상 AI audio producer 정상 생성. |
| 미디어 서버 (mediasoup SFU) | 영향 없음 | 별도 소켓/transport 경로. 세션 로그상 transport connected 유지, producer 정상. |
| 네트워크 품질 UI | 경미 | quality/latency 표시는 /api/health 기반이라 정상. httpbin 결과(issue)만 오판되나 읽는 곳 없음. |
CORS 차단은 해당 요청 하나에만 적용된다. httpbin.org 로의 fetch 가 막혀도 다른 origin(S3)으로의 fetch 에는 영향이 없다. "한 origin 이 막혔으니 다른 것도 막는다" 같은 전파(cascade)나 연결 풀 공유로 인한 차단은 존재하지 않는다.
| httpbin.org | 503 응답 + CORS 헤더 없음 → 브라우저가 응답 읽기 차단 → 콘솔 에러 |
| S3 presigned URL | 200 응답 + 정상 CORS 헤더(버킷 설정) → 정상 다운로드 |
httpbin 이 503 을 반복하던 동일 시간대에 S3 영상/이미지 리소스가 전부 정상 캐싱됨:
[RESOURCE_CACHE] Successfully cached video_firstopeningJW_yoona.mp4
[RESOURCE_CACHE] Successfully cached video_pingpong01_bridge_jw01~04.mp4
... (총 25개 전부 성공, 실패 0건)
[GUEST_PAGE_SESSION] Resources cached successfully
httpbin 호출이 만들어내는 값이 어디서 소비되는지 전수 추적한 결과:
| 필드 | 출처 | 소비처 |
|---|---|---|
quality | /api/health (serverLatency) | UI 표시 |
latency | /api/health (serverLatency) | UI 표시 + 네트워크 알림 |
externalLatency | httpbin | 아무도 안 읽음 |
issue | httpbin + health 조합 | 아무도 안 읽음 |
소비처 3곳(app/client-guest/page.tsx, widgets/guest/guest-layout/ui/guest-layout.tsx, components/pages/guest-entry.tsx)은 모두 .quality 와 .latency 만 사용하며, 이 둘은 전부 /api/health 기반이다.
externalLatency/issue 는 계산만 되고 어디서도 읽히지 않는다. 따라서 외부 체크를 통째로 제거해도 기능 손실이 0이다.
issue가 미사용이니 외부 체크를 제거(A안)"로 정리했으나,
"사용자 인터넷 전체가 죽었는가 vs 우리 서버 문제인가"를 실제로 구분하고 싶다는 요구(C안)가 확정되어
제거 → 교체 + 신호 활성화로 방향을 변경한다.
/health)로는 안 되는가소켓 서버(socket-prod./health는 Redis가 degraded면 503을 내므로(apps/socket/src/sfu-api/restHandlers.ts:441) 503 노이즈가 형태만 바뀐다. → C안의 기준점으로 부적합.
채택 외부 체크 대상만 httpbin → 연결성 체크 전용 제3자 엔드포인트로 교체한다. 본문을 읽지 않고 도달성·지연만 재므로 mode: "no-cors"(opaque)로 호출 → CORS 차단 없음. 503이 사실상 없는 대형 인프라라 노이즈도 제거된다.
https://www.gstatic.com/generate_204 (Google, 204 즉시 응답, 캡티브 포털 감지용 표준)https://1.1.1.1/cdn-cgi/trace (Cloudflare). 단발 제3자 장애 오탐이 걱정되면 2곳 핑 후 다수결.async function measureExternalLatency() {
const start = performance.now();
// no-cors: 응답 본문은 못 읽지만 "도달 성공/실패 + 시간"은 확보, CORS 차단 없음
await fetchWithAlert("https://www.gstatic.com/generate_204", {
cache: "no-store",
method: "GET", // 204 엔드포인트는 GET 권장 (HEAD 미지원 가능)
mode: "no-cors",
timeoutMs: 3000, // 100% 패킷 손실 시 무한 대기 방지
skipSlowAlert: true,
});
return Math.round(performance.now() - start);
}
issue 신호 활성화 (죽은 데이터 → 실사용)외부+서버 조합으로 determineIssue를 살려 원인을 가르고, 그 issue를 UI/알림 등 실제 소비처에 연결한다(현재는 계산만 되고 미사용 — 5장).
| external (제3자) | server (/api/health) | 판정 |
|---|---|---|
| OK | OK | none — 정상 |
| OK | 실패 | server — 우리 서버 문제 |
| 실패 | 실패 | user-network — 사용자 인터넷 전체 다운 |
| 실패 | OK | unknown — 제3자 일시 장애 등 (드묾) |
timeoutMs 부여 + skipSlowAlert: trueuseNetworkQuality를 단일/공유 인스턴스로 dedup (10초 × 인스턴스 수 중복 요청 제거)/health로 교체(B안) — 우리 인프라라 사용자 망 다운을 구분 못 하고 Redis-gated 503 잔존(6-1).gstatic.com/generate_204 같은 독립·고신뢰 엔드포인트로 no-cors 교체하고(503/CORS 노이즈 제거),
죽어 있던 determineIssue/issue 신호를 실제 소비처에 연결한다. 소켓 /health는 우리 인프라라 부적합.
관련 문서: 아동274 아동 3회 접속 실패 분석 · 리소스 다운로드 HTTP Range 이어받기 · 아동004 29회기 수업 중단 — 단말 인터넷 단절 (NETWORK_QUALITY 실증)