석우주 8회기 아동 화면 미표시·AI 세션 실패
— iPad WebKit ICE 후보 미수집 원인 분석 분석 완료 · 수정 미적용

마지막 업데이트 2026-09-12

쉬운 설명 한 장 보기 · 사전지식 없이 읽는 요약

작성일: 2026-09-02사건: 2026-09-01 20:00:02 ~ 20:04:47 (약 4분 45초)room: 7f7291c4…_8 (8회기)단말: iPad · iOS 26.6.1 · Chrome Mobile iOS 152 (WebKit)자료: 아동/모니터 세션로그 12,299건 · ppi-livekit · ppi-socket-prod

결론

서버(LiveKit·mediasoup)와 회선 문제가 아니라, 아동 iPad의 WebKit WebRTC 층이 약 4분 45초 동안 ICE 후보를 하나도 만들어 내지 못한 상태였다. 그 시간 동안 카메라 mediasoup transport·LiveKit AI 세션·기기 내부 loopback 세 종류의 피어연결이 전부 ICE 단계에서 멈췄고, 소켓·API·S3 다운로드 같은 비WebRTC 통신은 전부 정상이었다. 결정 증거는 LiveKit 서버가 남긴 removing participant without connectionpublisherCandidatesiPad에서 온 [remote] 후보가 0개라는 것이다(정상 접속 룸은 18개). 페이지 새로고침 2회·재진입 3회·진행자 강제퇴장 3회는 모두 무효였고, 탭을 벗어났다가 back_forward로 돌아온 6번째 진입에서 200ms 만에 정상화됐다.

현장 대응은 새로고침이 아니라 크롬 앱 완전 종료 후 재실행이다. 새로고침은 WebContent(JS) 층만 다시 띄우고, ICE를 담당하는 WebKit Networking 프로세스는 그대로 살아남기 때문에 이번 사례에서 5회 모두 15초씩만 소모했다. TURN·TCP 폴백을 추가해도 이 유형은 막지 못한다(기기 내부 연결조차 실패).

시간순 인과 다이어그램

20:00:02 iPad reload 진입 (navigationType=reload, reentryCount 1)
  → 20:00:05 HTTP /api/health 3초 타임아웃 → NETWORK_QUALITY "서버 도달 실패" WARN   ← 단일 단말 측정, 20:00:43 회복 (함정, 아래 참조)
  → 20:00:21 mediasoup send transport "connecting" … 이후 "connected" 없음             ← 진행자 모니터에 아동 카메라 안 뜸
  → 20:00:58 기기 내부 loopback PC 5초 타임아웃                                          ← 라우터도 서버도 안 거치는 연결이 실패 = 단말 안 고장
  → 20:01:09 LiveKit connect → 20:01:24 "could not establish pc connection" (15초)         ← AI 세션 생성 실패
      서버: signal만 붙고 ICE 미도달 → "removing participant without connection", remote 후보 0개
  → 진행자 kick ×3 + 아동 reload/navigate ×4 (20:01:43 / 02:35 / 03:08 / 04:11) — 매번 같은 실패
  → 20:04:24 카메라 트랙 muted (탭 이탈) → 20:04:39 back_forward 진입
  → 20:04:47 transport connected (33ms) · 20:04:50 LiveKit connected (200ms)             ← 정상화, 이후 20:21 종료까지 안정

1. 카테고리 분류 — 어느 층의 문제인가

후보판정근거
ppi-livekit 서버배제20:00~20:10 사이 다른 참가자 170명 정상 접속. removing participant without connection은 2시간(19:00~21:00) 중 6건이고 그중 5건이 이 아동
ppi-socket(SFU) 서버배제20:00~20:05 게스트 transport 상태가 기록된 41개 룸 중 이 룸만 장기 connecting, 나머지 40개는 connected. 서버측 ICE/DTLS 에러 로그 0건
네트워크 환경 (공유기·ISP·UDP 차단)배제ICE 서버 없이 같은 페이지 안 PC 두 개를 잇는 기기 내부 loopback(apps/web/shared/lib/webrtc-audio-loopback.ts:80, new RTCPeerConnection())까지 타임아웃. 패킷이 기기 밖으로 나가지 않는 연결이 실패하면 회선으로 설명 불가. 비WebRTC 통신(소켓·API·S3 25파일 다운로드)은 전부 정상
우리 JS 코드배제PC 생성·offer·localDescription·시그널링·transport 생성이 6회 전부 성공. 새로고침마다 코드는 새로 실행됐는데 결과가 같음
게스트 기기 — iOS WebKit WebRTC 층확정위 넷을 소거하면 남는 유일한 층. 서버가 받은 remote ICE 후보 0개가 직접 증거

iPad에서는 "크롬"이라도 WebRTC 구현은 크롬 코드가 아니다. Apple 정책상 iOS 브라우저는 모두 WKWebView(WebKit)를 쓰므로 RTCPeerConnection·ICE·DTLS는 iOS에 내장된 WebKit(libwebrtc 포팅)이 별도 프로세스에서 처리한다. 우리 코드도 크롬 앱도 이 프로세스를 재시작할 수 없다.

2. 딥다이브 ① — 6회 진입 전부 같은 자리에서 멈췄다

#진입 시각navigationTypemediasoup transportLiveKit종료 계기
120:00:02reloadconnecting (connected 없음)20:01:24 pc connection 실패진행자 kick 20:01:37
220:01:43navigateconnecting20:02:03 실패진행자 kick 20:02:29
320:02:35navigateconnecting20:02:55 실패아동 이탈 20:03:02 (kick은 NoGuest)
420:03:08navigateconnecting20:03:29 실패아동 이탈 20:04:01
520:04:11reloadconnectingconnect 시작 후 이탈탭 이탈 20:04:24 (카메라 muted)
620:04:39back_forwardconnected 33msconnected 200ms정상 진행 → 20:21 수업 종료

FACT LiveKit 실패 5건은 모두 livekit_connect_begin에서 정확히 약 15초 뒤 실패했다(iOS ICE failed 타임아웃과 일치). 아동은 2·4번째 인스턴스에서 "화면/친구가 멈췄어요" 이슈 리포트를 5회 보냈다.

추정 접속 직후 20초간 우리 서버 HTTP 응답만 느렸던 흔적(아래 함정 절)은 그 시점에 네트워크 인터페이스 전환 같은 흔들림이 있었을 가능성을 시사한다. 이것이 WebKit ICE 층을 멈추게 한 방아쇠였을 수는 있지만 로그로 확정할 수 없고, 20:00:43 이후 회선은 정상인데 WebRTC는 20:04:47까지 계속 실패했으므로 회선 자체는 원인이 아니다.

3. 딥다이브 ② — WebRTC 4단계 중 어디서 끊겼나

첫 실패 인스턴스의 AI 세션 시도(20:01:09)를 기준으로 단계별 로그를 대조했다.

단계하는 일로그결과
① PC 생성·SDPRTCPeerConnection 생성, offer, localDescriptionpeer_connection_created 09.313 → offer_create_end 09.362 → local_description_set_end 09.414성공
② 시그널링SDP 서버 전달, 방·transport 생성create_session_request_end 09.589 · LiveKit 서버 starting RTC session 20:01:11.7 · mediasoup Send transport created 10.210 → 서버 send transport created 20:01:12.4 · producer 2개 생성성공
③ ICE/DTLS후보 수집 → 연결성 검사(STUN) → DTLS클라이언트 connecting 이후 connected 없음 · 서버 RTT: nullms 반복 · LiveKit 유저 참가자 participant active 없음여기서 멈춤
④ 실패 확정15초 ICE 타임아웃disconnected 24.751 → could not establish pc connection 24.762 → session_start_failed실패

③ 단계 원본 로그 (세 출처)

[아동] mediasoup transport — connecting 이후 connected 없음, 송신 0바이트
20:00:21.130 [MEDIASOUP_PRODUCER] Send transport connection state {"state":"connecting"}
20:00:27.142 [MEDIASOUP_PRODUCER] Audio producer outbound stalled {"trigger":"producer-created","bytesSentDelta":0,"packetsSentDelta":0}
20:00:43.149 [MEDIASOUP_PRODUCER] Audio producer outbound stalled {"trigger":"stall-recheck","bytesSentDelta":0,"packetsSentDelta":0}
20:01:10.333 [MEDIASOUP_PRODUCER] Send transport connection state {"state":"connecting"}   ← AI 스텝 재생성, 역시 connected 없음

[아동] loopback — 기기 내부 PC 쌍, ICE 서버 없음
20:00:53.316 [VIDEO_AUDIO_LOOPBACK] Video audio routed into loopback graph {"loopbackState":"idle"}
20:00:58.372 [VIDEO_AUDIO_LOOPBACK] Video audio loopback setup failed {"message":"WebRTC loopback connection timeout","attempts":1}

[아동] LiveKit — connecting 09.600 → 15초 뒤 ICE 실패
20:01:24.751 [LIVEKIT_CLIENT_SESSION] LiveKit room connection state changed {"state":"disconnected"}
20:01:24.762 [LIVEKIT_CLIENT_SESSION] Failed to connect LiveKit browser session {"error":{"message":"could not establish pc connection"}}
20:01:26.772 [UNHANDLED_REJECTION] ConnectionError: could not establish pc connection

[ppi-socket] ICE 미성립 → RTT 측정 불가 1분 이상 반복
20:00:10.515 Guest transport state: guest-7f7291c4… is new, RTT: nullms
20:00:23.298 Guest transport state: … is connecting, RTT: undefinedms
20:00:24.051 ~ 20:01:39.230  … is connecting, RTT: nullms   (76회 연속)

[ppi-livekit] 결정 증거 — 서버 후보만 있고 iPad 후보(remote) 0개
20:01:26.761 "removing participant without connection" reason=SIGNAL_SOURCE_CLOSE connectionType=unknown
  publisherCandidates = [ [local] udp4 host 54.180.32.32:7882, [local] udp4 host 172.17.0.1:7882,
                          [local] tcp4 host 54.180.32.32:7881, [local] tcp4 host 172.17.0.1:7881 ]
  → [remote] 후보 0개, subscriberCandidates = null

비교) 성공 룸 20:04:52 "participant active" connectionType=udp
  [remote] 후보 18개 (host 192.168.200.114 ×17 + prflx 203.152.182.x), selected = udp prflx …:63564

FACT 시그널링 웹소켓은 살아 있었다(SIGNAL_SOURCE_CLOSE는 시그널 채널이 존재했다는 뜻). WebKit이 후보를 하나라도 만들었다면 trickle로 서버에 도달했어야 한다. 후보 0개는 ICE 후보 수집 단계 자체가 아무것도 내놓지 않았다는 뜻이며, 성공 룸은 TURN relay 없이 host+prflx로 직결됐으므로 이 환경은 원래 TURN이 필요하지 않은 환경이다.

4. 왜 새로고침이 무효였나 — 층이 다르다

iPad 크롬 앱 (WKWebView)
├─ WebContent 프로세스   ← 탭의 JS/DOM. 우리 코드(livekit-client, mediasoup-client)가 여기서 실행   ⟵ 새로고침이 초기화하는 범위
├─ Networking 프로세스   ← 소켓·UDP·ICE 후보 수집·STUN/DTLS (앱 전체 공용)                       ⟵ 고장이 있던 층(추정)
└─ GPU 프로세스          ← 카메라/마이크 캡처, 미디어 코덱 (앱 전체 공용)

5. 함정 — "서버 도달 실패" WARN은 서버 장애 증거가 아니다

20:00:05.473 [NETWORK_QUALITY] 서버 도달 실패 (외부 인터넷은 정상) — 우리 서버 문제로 진단 {"externalLatency":47,"serverLatency":null}
20:00:24.636 [NETWORK_QUALITY] 네트워크 정상 복구 {"externalLatency":46,"serverLatency":2175}
20:00:43.058 [GUEST_NETWORK_ALERT] Server-side network issue cleared {"serverLatency":18,"externalLatency":53}

6. 아동측 복구 절차 (재사용)

  1. 크롬 앱 완전 종료 후 다시 열기 (1순위) — 화면 아래에서 위로 쓸어올려 앱 스위처 → 크롬을 위로 밀어 닫기 → 다시 열고 수업 링크 재진입. 고장난 WebKit 프로세스를 죽이는 유일하게 확실한 방법.
  2. 다른 앱으로 나갔다가 뒤로가기로 복귀 — 이번 사례에서 실제로 복구된 동작이나 1번보다 확실성이 낮다.
  3. 1~2가 안 되면 Wi-Fi 끄고 켜기 후 1번 반복 (방아쇠 추정에 대한 조치).
하지 말 것: 페이지 새로고침·링크 재클릭(5회 전부 실패, 매번 15초 소모), 진행자 강제퇴장(3회 전부 무효 — 아동 기기 문제라 서버가 할 수 있는 게 없다).

진행자 판별 신호: 아동이 입장은 됐는데(스텝 동기화·채팅 정상) 카메라 화면이 계속 안 뜨고 AI 세션 시작 실패가 반복되면 이 유형이다. 그때 아동에게 1번을 안내한다.

7. 웹 코드 대응 방안 (제안 · 미적용)

고치는 건 불가능하고, 빨리 알아채서 올바른 복구를 시키는 것까지가 웹 코드의 한계다.

#제안효과
1사전 프로브로 즉시 판별 — 입장 직후 loopback 유틸을 진단용으로 1회 돌리거나 RTCPeerConnection을 만들어 2초 안에 host 후보가 0개면 "단말 WebRTC 불가" 확정현재는 transport 대기 + LiveKit 15초 타임아웃을 매번 겪어야 알 수 있음
2아동 화면 전용 안내 — 현재 토스트 "인터넷이 끊겼어요! 나갔다 다시 들어와 주세요!"(widgets/guest/network-toast/ui/network-toast.tsx:28)는 새로고침만 반복시킴. 이 상태에서는 "크롬을 완전히 닫고 다시 켜 주세요"를 그림 순서로잘못된 복구 동작 차단
3진행자 모니터에 별도 상태 — 소켓은 살아 있으니 guest_webrtc_unavailable 류 이벤트로 카드 표시kick 대신 바로 안내
4빠른 실패 — 프로브 실패 시 AI 세션 시작·transport 재시도를 건너뜀15초 대기 제거
5확정용 로깅iceGatheringState, 수집된 후보 수/타입다음 재발 때 "후보 미수집" vs "패킷 미송신" 확정

우선순위는 1+2+3을 한 묶음으로 보는 것이 맞다. 원인 분석 세션이라 코드는 건드리지 않았다.

8. 판별법 (재사용) — 다음 재발은 L0에서 끝낸다

확인 순서보는 것이 유형이면
1. 아동 로그MEDIASOUP_PRODUCER Send transport connection stateconnecting만 있고 connected 없음, Audio producer outbound stalled bytesSentDelta 0
2. 아동 로그VIDEO_AUDIO_LOOPBACKWebRTC loopback connection timeout — 이 한 줄이 회선·서버를 한 번에 배제
3. 아동 로그LIVEKIT_CLIENT_SESSIONcould not establish pc connection, connect 시작 ~15초 뒤
4. 아동 로그소켓·API·RESOURCE_CACHE정상 (인터넷 끊김 아님)
5. ppi-socketGuest transport stateconnecting, RTT: nullms 장기 반복, 같은 시간 다른 룸은 connected
6. ppi-livekitremoving participant without connectionpublisherCandidates[remote] 후보 0개 — 결정 증거
미확정: 클라이언트 iceGatheringState 로그가 없어 gathering이 new에 머문 것인지 complete인데 후보 0개인지는 구분 못 한다. 방아쇠(네트워크 전환 여부)도 로그에 없다. 이 판별법은 Claude 메모리 ipad-webkit-webrtc-stack-stall-discriminator에도 등록했다.

관련 문서