공지의 외부 링크를 누르면 데스크톱 브라우저에서는 새 탭이 열렸지만 iPhone의 PWA에서는 아무 반응이 없거나 앱 창 안에서 페이지가 바뀌었다. 비동기 요청을 마친 뒤 window.open을 호출해 팝업 차단에 걸리는 경우도 있었다.
브라우저는 사용자의 직접 클릭과 나중에 실행된 코드를 다르게 취급했다. 링크를 여는 시점과 대상 URL을 함께 봐야 했다.
사용자 동작 안에서 창을 열었다
링크 정보를 API로 확인한 뒤 새 창을 열던 흐름을 바꿨다. 클릭 이벤트 안에서 빈 창을 먼저 열고 검증이 끝나면 위치를 바꾸거나, 가능하면 링크 URL을 미리 준비했다.
function openExternal(url) {
const target = validateExternalUrl(url);
if (!target) return;
const opened = window.open(target, '_blank', 'noopener,noreferrer');
if (!opened) showOpenLinkGuide(target);
}
브라우저가 새 창을 막은 경우 사용자가 직접 누를 수 있는 안전한 링크를 제공했다.
허용할 URL을 먼저 검증했다
서버 데이터나 입력 문자열을 그대로 window.open에 넘기지 않았다. https 프로토콜과 허용된 도메인 정책을 확인하고 javascript: 같은 스킴을 거부했다.
const parsed = new URL(rawUrl, location.origin);
if (parsed.protocol !== 'https:') return null;
if (!allowedHosts.has(parsed.hostname)) return null;
return parsed.toString();
모든 외부 사이트를 허용해야 한다면 최소한 위험한 프로토콜을 막고 외부 이동 안내를 표시했다.
PWA와 일반 Safari를 따로 확인했다
홈 화면에서 실행한 standalone 모드, Safari 탭, 앱 안 WebView는 링크 처리 방식이 달랐다. display-mode와 실제 실행 환경을 확인하되 사용자 에이전트 문자열만으로 모든 정책을 결정하지 않았다.
로그에는 URL 전체의 쿼리 값을 남기지 않고 대상 도메인과 열기 결과만 기록했다.
외부 링크도 경계를 넘는 기능이었다
링크 하나는 단순 UI처럼 보였지만 앱에서 브라우저, 다시 다른 사이트로 사용자를 이동시켰다. 팝업 정책과 보안 검증, 실패 안내가 모두 필요했다.
이후 외부 이동 기능을 만들 때 내 환경에서 새 탭이 열리는지만 보지 않는다. 클릭 시점, 허용 URL, 플랫폼별 동작, 창이 막혔을 때의 대안을 함께 확인한다.