개념
원인이 다르면 앱이 보여야 할 화면도 다르다
레슨 1에서 Limited Service를 별도 상태로 세운 이유를 다시 짚자. 단절은 신호가 명확하지만, Limited는 애매한 채로 오래 끌기 때문에 어렵다고 했다.
그런데 한 겹 더 들어가면, Limited Service 안에도 서로 다른 원인이 섞여 있다. 그리고 원인마다 앱이 보여줘야 할 화면이 다르다.
"인터넷이 안 돼요" 라는 같은 증상 뒤에
A 회선이 극단적으로 느리고 손실이 많다
→ 오래 기다리면 될 수도 있다 → "느린 연결" 안내 + 재시도
B 이름 해석(DNS)이 안 된다
→ 서버 주소를 못 찾는다 → "네트워크 오류" 안내
C Wi-Fi는 붙었는데 그 위로 인터넷이 없다 (포털형·차단)
→ OS가 "인터넷 사용 불가" 를 이미 알고 있다 → 오프라인 배너 + 캐시 화면
D 패킷이 100% 사라진다 (연결 상태는 유지)
→ 즉시 실패가 아니라 타임아웃까지 기다린다 → 입력값 보존이 관건네 가지를 모두 통과해야 "사용 불가 상태 대응이 되었다"고 판정한다. 하나만 테스트하고 끝내면 나머지 셋에서 사고가 난다.
공통 전제 — 이것부터 안 맞으면 아무것도 재현되지 않는다
□ 테스트 기기의 모바일 데이터 OFF
→ 켜져 있으면 Wi-Fi 품질 저하 시 셀룰러로 자동 전환된다
→ Limited 재현 실패의 1순위 원인 (레슨 2 원인 ⑤)
□ 앱 캐시 삭제 후 시작
→ 이전 응답이 남아 있으면 정상으로 보인다
□ 조건 적용 직후 속도 측정 결과를 스크린샷으로 남긴다
→ 레슨 2의 6단계 절차레시피 A — 극단 저속 + 손실
만드는 법
| 방법 | 설정 |
|---|---|
| iOS NLC | 사용자 정의 — In Bandwidth 최소, Delay 1000ms, Dropped 30% |
| tc netem | rate 100kbit delay 1000ms 200ms loss 30% |
| 프록시 | 최소 대역폭 + 지연 |
목표 수치는 다운 50200 kbps · 지연 1,000 ms 이상 · 손실 2040% 다.
기대 동작
- 무한 스피너 없이 일정 시간 뒤 상태를 알리는 문구가 나온다
- 자동 재시도 횟수와 간격이 통제된다 (무한 재시도 아님)
- 수동 재시도 수단이 있다
- 버튼을 연타해도 중복 요청이 생성되지 않는다
- 일부만 받은 화면에서 레이아웃이 깨지지 않는다
"무한 스피너 없이"가 이 레시피의 핵심 판정 기준이다. 얼마나 기다려야 하는지 모르는 상태가 가장 나쁜 사용자 경험이고, 동시에 가장 흔하다.
관찰 포인트
이 조건에서는 요청이 성공도 실패도 아닌 상태로 오래 머문다. 그 시간 동안 앱이 무엇을 하는지 보는 것이 목적이다. 로그를 함께 보면 재시도가 몇 번 나가는지 셀 수 있다.
레시피 B — DNS만 지연·차단
왜 별도 레시피인가
DNS는 모든 요청의 앞단 에 있다. 여기가 막히면 서버가 멀쩡해도 아무것도 안 된다. 그리고 앱은 이 상황을 종종 엉뚱하게 처리한다 — "서버 점검 중입니다" 같은 잘못된 안내를 띄우거나, 아예 크래시한다.
만드는 법
| 방법 | 설정 |
|---|---|
| iOS NLC | High Latency DNS 프로파일 (가장 간단) |
| NLC 사용자 정의 | DNS Delay 값을 크게 |
| 공유기 | DNS 서버를 응답 없는 주소로 지정 |
| 리눅스 AP | dnsmasq를 중지하거나 응답 없는 상위 DNS 지정 |
iOS NLC의 High Latency DNS 가 이 레시피를 사실상 한 번에 해결한다.
다른 방법으로는 DNS만 골라 막기가 의외로 까다롭다.
기대 동작
- 도메인 해석 실패를 네트워크 오류로 안내 한다 (서버 오류로 오인하지 않는다)
- 앱이 강제 종료되지 않는다
- 재시도 시 DNS 캐시 때문에 영구히 실패하지 않는다
- 다른 도메인(예: 이미지 CDN)만 실패할 때 화면이 부분적으로라도 동작한다
두 번째 항목이 실제로 자주 깨진다. DNS 해석 실패 예외를 처리하지 않아 크래시하는 앱이 생각보다 많다.
레시피 C — 인터넷 없음 (포털형·상위망 차단)
이 상태의 특징
Wi-Fi는 정상적으로 붙어 있고 로컬 통신도 된다. 그런데 그 위로 나가는 인터넷이 없다. 공용 Wi-Fi에서 로그인 페이지를 안 거친 상태, 사내 게스트망, 결제하지 않은 호텔 Wi-Fi가 전부 여기 해당한다.
OS가 이걸 어떻게 아는가
운영체제는 Wi-Fi에 붙을 때마다 정해진 주소로 작은 요청을 보내 인터넷이 살아 있는지 확인한다.
Android http://connectivitycheck.gstatic.com/generate_204
→ 204 (본문 없음) 가 오면 정상
→ 다른 응답이 오거나 안 오면 "인터넷 사용 불가"
Apple http://captive.apple.com/hotspot-detect.html
→ 예상한 내용이 오면 정상
Chrome http://www.gstatic.com/generate_204이 요청이 로그인 페이지로 가로채이면 OS는 캡티브 포털 로 판단하고 로그인 창을 띄운다. 응답이 아예 없으면 "인터넷 사용 불가" 로 표시한다.
QA가 알아야 할 실무적 함의가 있다. 이 확인 주소가 막힌 네트워크에서는 로그인 포털이 아예 안 뜬다. 사내 방화벽이 gstatic.com을 차단하면 게스트 Wi-Fi에 붙은 손님이 로그인 화면을 못 보고 "와이파이가 고장났다"고 말하게 된다.
만드는 법
| 방법 | 설정 |
|---|---|
| Windows 핫스팟 | 핫스팟은 켜두고 PC의 인터넷 연결만 해제 |
| 공유기 | 해당 기기의 외부 통신만 차단 (MAC 기반) |
| 리눅스 AP | AP는 유지하고 업링크(eth0)를 내린다 |
핫스팟은 켜고 PC의 인터넷만 끊는 방법이 가장 간단하다. 폰은 여전히 Wi-Fi에 연결되어 있고 DHCP도 받았는데 밖으로는 못 나간다.
기대 동작
- Android에서 '인터넷 사용 불가' 표시가 뜬다 (OS 레벨 확인)
- 앱에 오프라인 배너가 노출된다
- 캐시된 화면을 보여준다 (있다면)
- 사용자 입력값이 보존된다
- 연결이 돌아왔을 때 자동으로 복구되거나, 최소한 재시도 수단이 있다
추가 확인 — 캡티브 포털 대응
우리 앱이 공용 Wi-Fi에서 로그인 전 상태 를 만났을 때 어떻게 되는지도 확인 대상이다. 이때 서버 응답 대신 포털의 HTML 로그인 페이지가 돌아온다.
앱이 기대한 것: {"data": [...]}
실제로 받은 것: <html><head><title>Wi-Fi 로그인</title>...
→ JSON 파싱 실패 → 앱이 어떻게 반응하는가?이 상황에서 크래시하는 앱이 드물지 않다. 레슨 4의 프록시로 "응답 본문을 HTML로 바꾸기"를 하면 쉽게 재현할 수 있다.
레시피 D — 완전 손실 (연결 상태는 유지)
단절과 어떻게 다른가
비행기 모드는 OS가 "네트워크 없음"을 즉시 알려준다. 앱은 오프라인 분기를 바로 탄다.
레시피 D는 연결 상태가 유지된 채로 패킷만 100% 사라진다. OS는 정상이라고 믿고, 앱은 요청을 보내고, 응답은 영원히 오지 않는다. 타임아웃까지 기다리는 경로 가 열린다.
만드는 법
| 방법 | 설정 |
|---|---|
| iOS NLC | 100% Loss 프로파일 |
| tc netem | loss 100% |
| Clumsy | Drop 100% |
기대 동작
- 즉시 실패가 아니라 타임아웃 경로를 탄다 (그리고 그 타임아웃이 합리적이다)
- 타임아웃까지의 시간 동안 사용자에게 진행 상태를 알린다
- 입력값이 보존된다 — 작성 중이던 글, 입력한 폼이 사라지지 않는다
- 타임아웃 후 재시도 수단을 제공한다
- 재시도 폭주가 일어나지 않는다
입력값 보존이 이 레시피의 핵심이다. 긴 글을 쓰고 등록을 눌렀는데 타임아웃이 나면서 화면이 초기화되는 것은 사용자가 가장 화내는 결함 유형이다.
타임아웃이 합리적인가
여기서 자연스럽게 따라오는 질문이 "몇 초가 적절한가" 다. 정답은 기능마다 다르지만, 판단 기준은 있다.
사용자가 기다리는 화면(로그인, 결제 확인) → 짧게. 10~15초
백그라운드 동기화 → 길어도 된다. 30~60초
파일 업로드 → 전체 타임아웃 대신 진행률 정체로 판단타임아웃이 60초인데 사용자가 보는 화면이 스피너뿐이라면, 그건 타임아웃 값이 아니라 UI 설계의 문제 다. 레슨 7에서 타임아웃 예산을 더 자세히 다룬다.
네 레시피 한눈에 보기
| A 극단 저속 | B DNS | C 인터넷 없음 | D 완전 손실 | |
|---|---|---|---|---|
| 연결 표시 | 정상 | 정상 | 사용 불가 | 정상 |
| 요청 결과 | 아주 느리게 성공/실패 | 이름 해석 실패 | 연결 실패 | 무응답 → 타임아웃 |
| 앱이 알 수 있는가 | 어렵다 | 예외로 구분 가능 | OS가 알려줌 | 타임아웃으로만 |
| 핵심 판정 | 무한 스피너 없음 | 크래시 없음·올바른 안내 | 오프라인 배너·캐시 | 입력값 보존 |
| 가장 쉬운 재현 | tc netem | iOS NLC High Latency DNS | 업링크 차단 | NLC 100% Loss |
C만 OS가 상태를 알려준다는 점 이 중요하다. 나머지 셋에서는 앱이 스스로 판단해야 하고, 그래서 더 자주 틀린다.
실무에서 왜 필요한가
"오프라인 처리했습니다"를 검증할 수 있다
개발이 "오프라인 대응 완료"라고 하면 대개 비행기 모드만 확인한 상태다. 네 레시피를 돌려보면 셋 중 둘은 통과하지 못하는 경우 가 흔하다. 특히 B(DNS)와 D(완전 손실)가 자주 빠진다.
사고 유형별 원인을 구분할 수 있다
CS에 들어온 "인터넷이 안 돼요" 제보를 네 유형 중 어디인지 물어서 좁힐 수 있다.
"와이파이에 느낌표가 떠 있어요" → C
"다른 앱은 되는데 우리 앱만 안 돼요" → A 또는 D (또는 서버 문제)
"공용 와이파이에서만 그래요" → C (캡티브 포털)
"지하철에서만 그래요" → A크래시를 미리 잡는다
B(DNS 실패)와 C(HTML 응답)는 앱 크래시를 유발하는 대표적 조건 이다. 정상 응답만 상정하고 만든 파싱 코드가 여기서 터진다. 출시 전에 이 둘만 돌려봐도 값어치가 있다.