본문 바로가기
빌드노트 검색

빌드노트

새 탭의 window.opener가 null이면 여는 코드를 먼저 본다

새 탭의 opener가 null인 이유를 링크의 암묵적 noopener와 window.open 옵션으로 구분한다. 새 프로필에서 다섯 조건을 클릭해 참조·부모 표식·반환값을 비교하고, 보호를 유지할지 통신 계약을 바꿀지 판단한다.

target="_blank" 링크로 연 페이지에서 window.opener가 null이라면, 부모 페이지의 로딩을 기다리는 코드부터 고칠 일은 아니다. 현재 HTML 표준에서는 rel="opener"가 없는 _blank 링크에 암묵적으로 noopener를 적용한다. 같은 출처의 페이지를 열어도 관계가 생기지 않을 수 있다. 새 탭을 연다는 선택과 그 탭에 원래 창의 참조를 주는 선택은 별개다. HTML 표준의 noopener 결정 절차 (새 탭에서 열림)

여기서 부모라고 부르는 것은 새 창을 연 원래 창이다. iframe의 parent와는 다르다. 우선 링크로 열었는지, 클릭 처리 함수의 window.open()으로 열었는지 확인해야 한다. 둘의 기본 동작을 같다고 생각하면 rel을 지워도 해결되지 않거나, 필요하지 않은 창 접근 권한을 되살리게 된다.

새 창이 없는 것과 opener가 없는 것을 나눈다

이미 열린 페이지의 개발자 도구 콘솔에서 다음 식으로 관계 유무를 확인한다.

window.opener === null

true면 현재 페이지에서 원래 창을 가리키는 opener 참조가 없다. 이 상태에서 window.opener.postMessage(...)를 호출해도 전송할 대상 참조가 생기지 않는다. 반대로 false인데 window.opener.document 접근에서 SecurityError가 난다면, 참조 유무와 다른 출처의 DOM 접근 제한을 나눠 조사해야 한다. MDN은 다른 출처의 opener에 허용되는 기능이 제한된다고 설명한다. Window.opener 문서 (새 탭에서 열림)

새 창 자체가 열리지 않았다면 이 글의 관찰과는 다른 상황이다. window.open()이 실제 클릭 처리 중 실행됐는지, 팝업 차단 안내가 있는지를 먼저 본다. 특히 호출 반환값이 null이라는 이유만으로 팝업 차단을 확정하면 안 된다. noopener 옵션도 반환값을 null로 만든다. window.open의 옵션과 반환값 (새 탭에서 열림)

같은 주소를 다섯 방식으로 열었다

차이를 확인하려고 새 Chrome 프로필에서 합성 부모·자식 페이지를 같은 loopback 출처로 제공했다. 실제 서비스의 장애를 재현한 것은 아니다. 부모에는 untouched라는 텍스트를 놓고, 자식은 opener가 있을 때만 부모의 그 텍스트를 synthetic:조건이름으로 바꾸도록 했다. 주소 이동, 계정 정보, 실제 업무 데이터는 사용하지 않았다.

링크 조건은 다음 세 가지였다. 자신의 링크와 비교할 때는 원본 템플릿뿐 아니라 브라우저에 만들어진 요소의 target과 rel을 본다. 아래 /child.html은 실험의 동일 출처 자식 주소이며, 자신의 코드에서는 실제 열 주소에 해당한다.

<a href="/child.html" target="_blank">기본</a>
<a href="/child.html" target="_blank" rel="noopener">참조 차단</a>
<a href="/child.html" target="_blank" rel="opener">참조 요청</a>

자동화가 링크와 버튼을 클릭해 연 결과는 다음과 같았다. 모든 클릭은 브라우저에서 isTrusted=true였고, 창을 열기 전 사용자 활성 상태도 true였다.

  • _blank 링크, rel 없음: 자식의 openerIsNull=true. 부모 표식은 untouched였다.
  • _blank 링크, rel="noopener": 자식의 openerIsNull=true. 부모 표식은 untouched였다.
  • _blank 링크, rel="opener": 자식의 openerIsNull=false. 부모 표식이 synthetic:opener로 바뀌었다.
  • 클릭 처리에서 window.open(url, "_blank"): 자식의 openerIsNull=false. 부모 표식이 synthetic:open-default로 바뀌었고, 호출 반환값도 null이 아니었다.
  • 클릭 처리에서 window.open(url, "_blank", "noopener"): 자식의 openerIsNull=true. 부모 표식은 untouched였고, 호출 반환값은 null이었다. 자식 페이지는 실제로 열려 결과를 보고했다.

첫째와 넷째의 차이가 진단에 중요하다. 링크의 _blank 기본 보호를 window.open()의 _blank에도 그대로 적용한다고 가정하면 안 된다. 자바스크립트로 여는 코드에서는 세 번째 인자의 옵션을 별도로 확인한다. 이번 관찰에서는 noopener를 지정한 호출만 관계가 끊겼다.

예를 들어 기존 클릭 처리 함수에서 다음 호출을 쓰고 있다면, child === null 분기만으로 “창 열기 실패”를 표시하는지 함께 확인한다. url은 그 함수가 이미 사용하는 열 주소다. 두 줄을 콘솔에서 따로 실행해 클릭 조건을 바꾸지 않고, 실제 클릭 처리 안의 호출·반환값을 확인한다.

const child = window.open(url, "_blank", "noopener");
console.log(child === null);

이 조건의 실험 출력은 true였지만 자식은 열렸다. noopener를 유지하면서 이 반환값으로 자식 창을 조작하거나 완료 여부를 읽는 계약은 성립하지 않는다. MDN의 noopener 옵션 설명 (새 탭에서 열림)

링크에서 명시적 noopener만 지우는 수정도 충분하지 않다. 첫 번째 조건처럼 _blank 자체가 암묵적 차단을 적용할 수 있기 때문이다. 또 rel="noreferrer"는 참조자 정보 전송을 막는 것에 더해 noopener 동작도 포함한다. opener와 noopener 또는 noreferrer를 함께 적는다고 참조 요청이 우선하지 않는다. 표준은 차단 키워드를 먼저 검사한다. HTML 링크 처리 규칙 (새 탭에서 열림)

참조를 되살리는 것은 통신 한 줄을 고치는 것보다 크다

외부 문서나 사용자가 입력한 링크를 새 탭으로 보여 주기만 한다면, 원래 창에 대한 참조가 없는 상태를 유지하면 된다. null을 없애려고 모든 링크에 rel="opener"를 붙일 이유는 없다. noopener는 열린 페이지가 opener를 통해 원래 문서를 건드리는 통로를 막기 위한 동작이다. MDN의 rel=noopener 설명 (새 탭에서 열림)

반면 직접 관리하는 보조 창이 원래 창에 완료를 알려야 한다면, 먼저 그 연동이 opener 참조를 필요로 하는지 결정한다. 관계를 허용하기로 했다면 링크의 명시적 rel="opener"나 window.open() 옵션을 그 경로에서 검토할 수 있다. 이번 동일 출처 실험에서는 참조를 허용하자 자식이 부모 DOM의 표식까지 바꿀 수 있었다. 완료 알림 하나를 받을 수 있게 됐다는 사실만으로 권한이 그 알림에 한정된 것은 아니다.

참조를 가진 창끼리 postMessage를 사용하더라도 허용 origin, 예상한 발신 창, 데이터 구조의 검사는 따로 필요하다. 그 검사가 opener 생성 조건을 바꾸지는 않는다. HTML 표준의 메시지 검증 지침 (새 탭에서 열림) 관계를 차단해야 하는 요구와 완료 확인 요구가 함께 있다면, 같은 탭의 완료 경로 또는 서버에서 완료 상태를 조회하는 방식처럼 opener에 의존하지 않는 연동을 별도로 설계해야 한다. 이 글은 그런 대체 연동을 구현·검증한 결과는 아니다.

rel="opener"를 요청했는데도 참조가 없다면 양쪽 문서의 최종 응답 헤더에서 Cross-Origin-Opener-Policy도 확인한다. COOP는 출처와 양쪽 정책 조합에 따라 창을 다른 브라우징 컨텍스트 그룹으로 분리하고 참조를 끊을 수 있다. same-origin이라는 값 하나만 보고 항상 끊긴다고 단정하거나, 이 문제를 해결하려고 보호 헤더를 일괄 삭제해서는 안 된다. COOP의 창 열기 규칙 (새 탭에서 열림)

관찰 환경은 Windows 11, Chrome 156.0.8078.47의 headless 모드, agent-browser 0.27.0이었다. 새 프로필과 같은 scheme·호스트·포트의 두 문서만 사용했고 COOP 헤더는 보내지 않았다. 다섯 조건을 확인한 뒤 실습 서버와 브라우저를 종료하고 두 포트의 종료도 확인했다. 다른 브라우저·일반 창 모드·물리 마우스 입력, 다른 출처, 이름 있는 창 재사용, COOP 조합, 실제 인증·결제 연동은 실행하지 않았다. 여기서 확인한 것은 새 창을 여는 다섯 조건의 참조 유무와 무해한 동일 출처 텍스트 변경까지다.

← 전체 글 보기