당신이 만든 웹 도구가 크롬에서 다 통과했다고 하자. 다운로드 버튼을 누르면 PDF 가 저장되고, 로컬 서버로 띄운 화면은 멀쩡하고, 검색엔진 알림(IndexNow) 요청은 200 을 돌려받았다. 그런데 iPhone 사파리 사용자는 버튼을 누르자 화면이 PDF 뷰어로 바뀌어 원래 작업을 잃고, 같은 로컬 빌드를 사파리로 열면 CSS·JS 가 하나도 안 붙고, 다른 검색엔진의 IndexNow 엔드포인트는 같은 요청을 422 로 돌려보낸다. 오류 로그는 어느 쪽에도 없다 — 당신이 시험한 쪽에서는 아무것도 틀리지 않았기 때문이다.
한 구현에서 통과했다는 것은 무엇을 증명하나
같은 표준을 구현한 쪽이 여럿이면, 한 구현에서의 통과는 그 구현이 표준의 빈칸을 메운 방식을 통과한 것일 뿐이다. 표준은 모든 경우를 정하지 않는다. <a download> 를 받은 브라우저가 "저장"을 어떻게 보여 줄지, localhost 를 안전한 출처로 볼지, 요청 본문의 선택 칸을 어떻게 검사할지는 구현마다 다르게 메워져 있다. 그래서 확인은 "표준에 맞는가"가 아니라 **"실제로 받을 구현 각각이 받아 주는가"**로 해야 한다.

흔한 예: 어디서 갈리나
- 파일 내려받기. 쇼핑몰의 영수증 PDF, 사내 도구의 보고서 내보내기를
blob:주소와<a download>로 처리하면 데스크톱 크롬·엣지는 저장한다. iPhone 사파리는 blob PDF 를 저장 대신 현재 페이지를 PDF 뷰어로 바꿔 연다 — 파일은 안 남고 사용자는 작성하던 화면을 떠난다. - 보안 헤더와 로컬 시험. CSP 의
upgrade-insecure-requests는 http 요청을 https 로 올린다. 크롬은localhost를 예외로 두지만 iOS 사파리는 localhost 요청까지 https 로 올려, 로컬 http 서버에서 CSS·JS 가 전부 실패한다. 크롬으로만 로컬 확인을 하면 "사파리 로컬 시험이 안 된다"를 영영 모른다. - 검색엔진 알림 API. IndexNow 는 여러 검색엔진이 공유하는 규약이지만 엔드포인트마다 검사가 다르다. 키 파일이 사이트 루트에 있는데 선택 칸
keyLocation을 같이 보내면, 한 엔드포인트는 받아 주고 다른 엔드포인트(네이버)는 422 로 거절한다. - 줄바꿈 규칙.
word-break: keep-all을 켜도 크로미움은 숫자와 한글 사이(117,000|원)에서 줄을 바꾼다. 브라우저마다 줄을 끊는 자리가 다르니 "한 브라우저에서 예쁘다"는 레이아웃의 증거가 아니다.
처방은 무엇인가
빈칸을 피해 가는 것이 첫째, 피할 수 없으면 받는 쪽마다 시험하는 것이 둘째다.
- 내려받기: iOS 에서는
navigator.share({ files })로 공유 시트(「파일에 저장」)를 연다. 공유는 사용자 탭 직후에만 열리므로File을 미리 만들어 두고 클릭 핸들러에서await없이 바로 부른다.navigator.canShare({ files })가 거짓이면<a download>로 물러난다. - 로컬 시험: 사파리로 볼 때는 개발 서버를 https 로 띄우거나, 그 헤더만 빼 주는 프록시를 거친다.
- 선택 칸: 여러 수신자가 공유하는 API 에는 꼭 필요한 칸만 보낸다. 키 파일이 루트에 있으면
keyLocation은 빼라 — 선택 칸은 구현마다 해석이 갈리는 첫 자리다. - 줄바꿈: 낱말마다
white-space: nowrap으로 묶고 공백은 묶음 밖에 둔다. 값 전체를 묶으면 좁은 칸에서 넘친다.
받는 쪽 목록은 어떻게 만드나
에이전트나 QA 에게 주는 지침에 이렇게 적어 둔다.
"동작한다"고 보고하기 전에, 이 기능을 실제로 받을 구현을 전부 적고 각각에서 확인한다.
- 브라우저 기능(다운로드·공유·클립보드·보안 헤더·줄바꿈): 크로미움 하나 + iOS 사파리(시뮬레이터라도) + 파이어폭스
- 여러 서비스가 공유하는 규약(IndexNow, OAuth, 웹훅): 보낼 엔드포인트마다 한 번씩 실제 요청
- 한 곳만 확인했으면 보고에 "A 에서만 확인"이라고 적는다. "동작한다"고 쓰지 않는다
확인 순서:
- 이 기능이 기대는 표준·규약을 적는다(
<a download>, CSP, IndexNow…). - 그 표준이 정하지 않은 부분을 쓰고 있는지 본다 — 선택 칸, 예외 출처, 저장 화면 같은 UI.
- 받는 쪽 목록을 만든다. 사용자 비중이 큰 쪽(모바일 사파리)이 목록에서 빠지기 가장 쉽다.
- 각 구현에서 결과물(저장된 파일, 붙은 스타일, 200 응답)을 직접 본다.
우리가 겪은 일
- 브라우저 PDF 도구를 크롬에서 다 시험한 뒤 iOS 시뮬레이터 사파리로 열었더니, 내려받기 버튼이 파일을 저장하지 않고 페이지를 PDF 뷰어로 바꿨다(2026-11). 같은 날 로컬 빌드는 CSP 헤더 때문에 사파리에서만 스타일이 하나도 안 붙었다. 둘 다 크롬에서는 재현되지 않았다.
- 매달 바뀐 쪽 주소를 IndexNow 로 알리는 작업에서, 같은 본문이 공용 엔드포인트에서는 받아지고 네이버 엔드포인트에서는 422 였다(2026-11).
keyLocation을 빼자 둘 다 받았다. 한쪽 응답만 봤다면 "붙였다"로 끝났을 것이다. - 모바일 화면 점검에서
keep-all만 믿었다가 가격이 숫자와 '원' 사이에서 끊겼다(2026-10).