본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
모던JS의 브라우저 파트부터 심화 파트를 다루면서 부쩍 최신 웹 프론트엔드 경향과 다소 동떨어져 있는 기능을 소개하게 되는 경우가 잦아졌다. 그럼에도 불구하고 해당 기능들을 모두 소개한 까닭은, 현대 웹 개발 트렌드에서는 더 이상 찾지 않는다고 하더라도 그 이면에는 관련 기술이 여전히 사용되고 있거나 또는 이를 근간으로 다음 레벨 수준의 기술이 사용되고 있기 때문이었다. 그러나 해당 챕터에서 다룰 팝업 윈도우는 정말이지 오늘날의 웹 트렌드와 잘 맞지 않는 녀석이다.
팝업 윈도우란 사용자에게 추가적인 문서를 보여주는 가장 오래된 방식 중에 하나로, 다음과 같은 코드로 실행할 수 있다.
window.open('https://javascript.info/');
해당 코드를 실행하면 주어진 URL을 새 창으로 하는 탭이 열리게 될 것이다. 요즘 대부분의 모던 브라우저는 새로운 윈도우 창을 브라우저 내부 탭에서 열기 때문이다. 물론 새로운 브라우저로 열 수 있기도 한데, 이에 대해서는 밑에서 살펴보자.
팝업은 정말 고대 웹의 역사에서부터 함께한 개념이라고 해도 무방할 만큼 오랜 전통을 가진 기법이다. 현재 페이지와 별개로 추가적인 창을 통해 관련 또는 관련 없는 데이터를 보여주는 기법으로, 이는 오늘날 웹에서는 모달(Modal
)이란 형식으로 많이 변경되었다.
팝업은 현재 보고 있는 창을 중단하거나 닫지 않고 추가적인 정보를 받아보기 위한 용도에서 탄생했다. 하지만 오늘날 웹 페이지는 AJAX
통신부터 SPA
라는 형태로 발전하면서, 추가적인 창을 띄워 정보를 표시하는 방식은 거의 취하지 않는다. 간단하게 AJAX
통신으로 페이지 새로고침 없이 서버로부터 데이터를 받아올 수 있으며, 앞서 이야기한 Modal
은 현재 페이지 내부에서 팝업과 비슷하게 추가적인 정보를 보여줄 수 있다. 때문에 과거에 쓰이던 팝업 윈도우 창은 오늘날 더 이상 매력적이지 않다. 무엇보다 모바일 디바이스에서는 새로운 창을 PC 환경에서와 같이 자유자재로 띄우기에 제한이 많기 때문에, 더욱 더 쇠퇴로 발걸음을 재촉하는 원동력이 되기도 했다.
그럼에도 불구하고 팝업 윈도우 창을 짚고 넘어가는 이유는, 아직까지 이러한 형태의 팝업을 유지하는 몇몇 사이트가 있기 때문이기도 하며, 구글이나 페이스북과 같은 사이트와 계정 연동을 통해 로그인 기능을 구현하는 OAuth
인가 방식에서는 여전히 팝업 윈도우 창을 유효하게 사용하기도 한다. 그 외에도 휴대폰 인증, 전자 결제와 같은 분야에서도 종종 팝업 윈도우 창으로 접근할 수 있음을 볼 수 있다. 이는 팝업 윈도우 창이 다음과 같은 특징을 가지기 때문이다.
팝업창은 별개의 윈도우 창으로 자신만의 자바스크립트를 실행할 수 있는 독립된 환경이다. 따라서 팝업창을 서드 파티나 신뢰가 가지 않는 사이트에서도 안전하게 열 수 있다. 때문에 특히 보안과 민감한 계증 인증이나 결제와 관련해서는 팝업 윈도우 창이 쓰이고 있다.
일단 팝업을 띄우는 과정이 매우 간단하다.
팝업창에서 URL을 변경하거나, 윈도우 오프너(opener
, 새로운 윈도우 창을 열고 관리하는 매개체)에 메시지를 전송할 수 있다.
그렇기에 오늘날 잘 쓰이지 않는 기능임에도 불구하고, 관련 내용을 간단하게라도 살펴보고 넘어가도록 하자.
과거의 악의적인 의도를 가진 몇몇 사이트는 팝업을 이용해서 많은 혼란을 야기한 적이 있었다. 요즘은 보기 어렵지만, 간혹 뉴스 사이트나 또는 불법 사이트에서 무수히 많은 광고성 팝업창을 계속 띄우는 경우를 경험한 적 있을 것이다. 때문에 이제는 대부분의 브라우저가 자체적으로 이러한 팝업창을 차단하여 사용자를 보호하려는 시도를 하고 있다.
때문에 오늘날 대부분의 브라우저는 유저에 의해 발생하는 경우가 아닌 팝업창의 경우에는 대부분 원천 차단한다. 유저에 의해 발생하는 팝업창은 onclick
이벤트 리스너에 의해 클릭을 통한 팝업창 열기가 있을 수 있다.
// 다음은 대부분의 브라우저에서 차단됨
window.open('https://javascript.info');
// 다음의 경우엔 허용됨
button.onclick = () => {
window.open('https://javascript.info');
};
이러한 방식을 통해 브라우저는 최소한의 영역에서 사용자를 보호하려 한다. 그러나 브라우저 측에서 완벽하게 이를 통제하는 것은 불가하다. 예를 들어 onclick
을 통해 팝업창이 열린다면, setTimeout
비동기함수를 통해 팝업창을 여는 경우엔 어떻게 될까?
setTimeout(() => window.open('http://google.com'), 3000);
이는 유저의 인터랙션에 의해 발생하는 팝업창이라고 보기 힘들다. 그렇다면 브라우저는 이로 인해 발생하는 팝업창을 차단하게 될까?
크롬의 경우에는 다음과 같이 코드를 작성하면 정상적으로 팝업창이 열린다. 그러나 파이어폭스의 경우에는 차단된다. 즉 브라우저 별로 적용하는 원칙이 조금씩 상이하다.
그러나 파이어폭스의 경우에도 대기시간을 2000ms
로 설정한다면 정상적으로 팝업이 열리는 것을 확인할 수 있다. 이는 파이어폭스 브라우저에서 2초 보다 작거나 같은 지연시간은 유효한 요청이라고 판단하기 때문이다.
// 2초보다 작은 지연시간에서는 정상적으로 팝업 윈도우 창이 열린다
setTimeout(() => window.open('http://google.com'), 1000);
때문에 브라우저 자체적으로 실시하는 차단 정책은 완벽하지는 않다.
먼저 새로운 윈도우 창을 열게끔 하는 메서드에 대해 알아보자. 해당 메서드는 다음과 같은 문법으로 사용한다.
window.open(url, name, params);
이때 각각의 인수는 다음을 의미한다.
url
새로운 윈도우 창에 로드할 URL 주소를 의미한다.
name
새로운 윈도우 창이 가지는 이름을 설정한다. 각각의 윈도우 창은 보이지는 않지만 저마다의 이름을 가지고 있고, 해당 값은 window.name
으로 접근할 수 있다. 이를 이용해 우리는 어떤 윈도우 창을 팝업창으로 사용할 지 특정할 수 있다. 만약 동일한 이름을 가지고 있는 윈도우가 이미 존재한다면, 입력된 URL 경로의 윈도우 창은 해당 이름을 가진 윈도우에 창이 열리고, 그렇지 않은 경우엔 새로운 창이 열린다.
params
새로운 윈도우 창을 위한 설정값으로 문자열이다. 각각 세팅값은 콤마(,
)로 구분하고, 각 params
의 옵션은 항상 공백이 없어야 한다. (eg. width=200,height=100
)
params
에서 설정할 수 있는 옵션은 다음과 같은 것들이 있다.
위치관련
left/top
: 스크린 상에서 윈도우 창의 top
과 left
의 좌표로 새로운 창은 스크린의 밖에 배치할 수 없다는 제한사항이 존재width/height
: 새 윈도우 창의 너비와 높이를 결정하며 최소 너비/높이 제한이 있기 때문에 보이지 않는 창을 만들 수는 없음윈도우 창 기능 관련
menubar
: 새 윈도우 창의 브라우저 메뉴 출력 여부 결정toolbar
: 새 윈도우 창의 브라우저 네비게이션 바(뒤로가기, 앞으로가기 등) 출력 여부 결정location
: 새 윈도우 창의 URL 필드 영역 출력 여부 결정 (파이어폭스와 인터넷 익스플로러는 해당 값을 숨길 수 없음)status
: 새 윈도우 창의 상태바 출력 여부 결정 (대부분의 브라우저는 해당 UI를 출력하도록 강제)resizable
: 새 윈도우의 사이즈 조절 여부를 결정scrollbars
: 새 윈도우의 스크롤 여부를 결정이때 특정 브라우저들만이 제공하는 별도의 기능이 있는데, 애초에 params
옵션은 브라우저마다 무조건 강제하는 값이 있어 잘 지정하지 않거니와 특별 기능 역시 대부분 잘 쓰이지 않는다. 해당 기능 목록은 MDN문서에서 확인할 수 있다.
브라우저가 위에서 언급한 params
옵션을 사용해 어떤 기능을 불능화할 수 있는지 다음 코드를 통해 살펴보자.
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=0,height=0,left=-1000,top=-1000`;
open('/', 'test', params);
이때 크롬브라우저에서 열리는 창은 다음 이미지와 같다.
상당 부분의 윈도우 기능이 제거된 것을 볼 수 있다. 이때 width/height
는 모두 0
으로 지정되어 있는데, 윈도우 창은 최소값이 정해져 있어 0
은 유효하지 않은 값으로 인식된다. 대부분의 브라우저는 position
과 관련된 params
옵션값은 마치 HTML 문서를 파싱할 때처럼 유효하지 않은 값을 자체적으로 수정하여 인식한다. 크롬은 해당값을 풀스크린으로 인식하고, 따라서 새롭게 열리는 윈도우 창은 전체화면이 될 것이다.
이번에는 width/height/left/top
에 유효값을 넣어서 실행해보자. 지정한 위치와 크기로 윈도우 창이 열리는 것을 확인할 수 있을 것이다.
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=600,height=300,left=100,top=100`;
open('/', 'test', params);
params
에 설정되는 세팅값들은 다음의 특징을 가지고 있다.
open
메서드에 3번째 인자가 없다면, 기본으로 지정된 윈도우 파라미터가 지정되어 실행
params
문자열이 있지만 yes/no
기능이 누락되어 있는 경우 빠져있는 기능들은 모두 no
라고 간주. 따라서 어떤 윈도우 기능을 추가하고 싶다면 꼭 명시해주어야 함
left/top
옵션이 없는 경우 브라우저는 가장 마지막에 열렸던 윈도우 창과 가장 근접한 위치에 윈도우 창을 실행
width/height
값이 없는 경우 새 윈도우 창은 가장 마지막에 열렸던 윈도우 창과 동일한 크기로 실행
open
메서드가 호출되면 새로운 윈도우 창에 대한 참조를 반환한다. 해당 참조를 이용해서 새 윈도우 창의 프로퍼티를 조작하거나, 위치를 변경하는 등의 작업을 수행할 수 있다.
다음 예시에서는 자바스크립트를 이용해 팝업창의 내용을 생성하고 있다.
let newWin = window.open('about:blank', 'hello', 'width=200,height=200');
newWin.document.write('Hello, World!');
다음은 로딩 이후에 내용을 수정하는 예시이다.
let newWindow = open('/', 'example', 'width=200,height=30');
newWindow.focus();
alert(newWindow.location.href);
newWindow.onlaod = function() {
let html = `<div style="font-size:30px">Welcome!</div>`;
newWindow.document.body.insertAdjacentHTML('afterbegin', html);
};
위 예시를 통해 알 수 있는 점은 window.open
이 호출된 직후 바로 새로운 윈도우 창이 생성되지 않는다는 점이다. 이는 바로 밑의 alert
메서드를 통해 알 수 있다. 때문에 우리는 새로운 윈도우 창이 로드를 마치기까지 기다려야 하는데, 이는 윈도우 창에 onload
이벤트 핸들러를 등록해서 캐치할 수 있다. 또는 DOMContentLoaded
이벤트를 통해서도 가능하다.
생성되는 새로운 윈도우 창은 동일 오리진 정책(
Same Origin Policy
)이 적용된다. 따라서 같은 프로토콜/도메인/포트를 가진 오리진에 대해서는 자유롭게 접근이 가능하다.
그러나 다른 오리진이라면 이러한 왕래가 불가능하다. 예를 들어site.com
에 메인 윈도우 창이 있고,gmail.com
에서 만들어진 팝업창이 있을 경우 보안의 이유로 두 창은 서로 통신이 불가하다.
팝업창은 window.opener
참조를 사용해서 자신을 생성한 윈도우 창에 접근할 수 있다. 이때 자신을 생성한 윈도우를 오프너(opener
) 윈도우라고 부르기도 한다. 생성된 팝업창을 제외한 모든 윈도우 창에서의 window.opener
는 null
값을 가진다.
아래 코드를 실행하면, 현재 오프너 윈도우의 내용을 Test
문자열로 교체하게 된다.
let newWin = window.open('about:blank', 'hello', 'width=200,height=200');
newWin.document.write(
"<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
);
이를 통해 오프너 윈도우와 팝업창은 서로 양방향 통신이 가능하다는 것을 알 수 있다. 물론 동일 오리진 정책을 준수하는 경우 하에서 양방향 통신이 유효하다.
윈도우 창을 종료하기 위해서는 window.close()
메서드를 호출하면 된다. 그리고 해당 윈도우 창이 정상적으로 종료되었는지 확인하기 위해서는 window.closed
프로퍼티를 확인하면 된다.
기술적으로 close()
메서드는 어느 윈도우 창에서도 사용이 가능하며 팝업창에 국한된 기능이 아니다. 그러나 대부분의 브라우저에서는 자체적으로 window.open
을 통해 생성된 윈도우 창이 아닐 경우에는 window.close()
메서드를 무시한다. 때문에 팝업창 역할을 하는 윈도우 창은 정상적으로 해당 메서드로 종료가 가능하지만, 메인 윈도우 창은 불가하다.
closed
프로퍼티는 당연히 윈도우 창이 정상적으로 종료되었다면 true
값을 가진다. 이는 팝업창이 아직 살아있는지 아니면 종료되었는지 체크할 수 있기에 유용하다. 사용자는 윈도우 창을 언제든지 종료할 수 있고 때문에 코드는 창이 종료되는 상황을 항상 고려해야 한다. 다음 예시는 창을 로드함과 동시에 종료하는 코드이다.
let newWindow = open('/', 'example', 'width=300,height=300');
newWindow.onload = function() {
newWindow.close();
alert(newWindow.closed); // true
};
윈도우 창을 스크롤링 하거나 리사이징 할 수 있는 메서드가 있다.
win.moveBy(x, y)
윈도우 창을 현재 위치에서 상대적으로 오른쪽으로 x
픽셀만큼, 그리고 y
픽셀만큼 아래쪽으로 이동. 음수값도 사용 가능(= 왼쪽/위쪽방향)
win.moveTo(x, y)
윈도우 창을 명시된 (x, y)
좌표값으로 이동
win.resizeBy(width, height)
윈도우 창을 현재 사이즈를 기준으로 주어진 width/height
값으로 재조정. 음수값도 사용 가능
win.resizeTo(width, height)
윈도우 창을 주어진 width/height
값으로 재조정
윈도우 창의 크기변화를 감지할 수 있는 onresize
이벤트 핸들러도 있다는 것을 함께 알아두자.
악의적인 개발자가 오남용하는 것을 막기 위해서, 브라우저는 메인 윈도우 창에서는 이러한 메서드를 차단한다. 따라서 부가적인 탭이 없는 팝업창에서만 해당 메서드를 사용할 수 있다.
최소화/최대화는 OS 레벨에서 지원하는 기능이기 때문에 자바스크립트를 사용해서 브라우저의 최소/최대화에 관여할 수 없다.
앞서 문서 Document (4) 챕터에서 스크롤을 사용하는 방법을 살펴보았다. 이와 동일한 메서드를 이용해 윈도우의 스크롤 위치를 지정할 수 있다. 해당 메서드들의 기능을 다시 한 번 짚고 넘어가도록 하자.
win.scrollBy(x, y)
윈도우 창의 스크롤을 x
픽셀만큼 오른쪽으로, y
픽셀만큼 아래쪽으로 이동. 음수값 사용 가능(왼쪽/위쪽)
win.scrolTo(x, y)
윈도우 창의 스크롤을 주어진 (x, y)
좌표로 이동
elem.scrollIntoView(top = true)
윈도우 창의 스크롤이 elem
요소가 브라우저 뷰포인트의 상단에 위치하도록 이동. false
인 경우 뷰포인트 하단에 위치하도록 이동. (true
가 기본값)
역시 window.onscroll
이벤트 리스너를 통해 스크롤 이벤트를 감지할 수 있다.
이론적으로 window.focus()
와 window.blur()
메서드를 사용해 윈도우 창 자체를 focus/blur
처리할 수 있다. 또한 이를 이용해 focus/blur
이벤트를 통해서 방문자가 윈도우 창을 포커싱하거나 해제하는 순간을 잡아낼 수도 있다.
그럼에도 불구하고 실제로 해당 메서드는 윈도우 창 레벨에서 잘 사용되지 않는다. 해당 기능 역시 과거 악의를 가진 개발자에 의해 악용될 여지가 많았기 때문이다. 예를 들어 다음과 같은 코드를 살펴보자.
window.onblur = () => window.focus();
매우 단순하지만 다분히 의도적인 코드이다. 사용자가 해당 윈도우 창을 떠나려고 할 때 다시 포커싱을 시킴으로, 사용자를 현재 창에 가두려는 목적이다.
때문에 브라우저들은 이러한 행위를 방지하기 위해서 많은 제약사항들을 제안하고 있다. 이를 통해 최소한의 사용자를 악의적인 의도로부터 보호하고자 한다. 관련 제약사항은 브라우저별로 모두 상이하며 전적으로 브라우저가 책임을 지고 있다.
예를 들어 모바일 브라우저의 경우에는 대개 window.focus()
메서드를 완전히 무시한다. 또한 포커싱은 팝업창이 별개의 탭의 형태로 열리는 경우에는 작동하지 않고, 오직 새창으로 팝업창이 열릴 때만 작동한다.
그럼에도 불구하고 focus
를 통해 여전히 유용하고 효과적인 작업을 처리할 수 있기에, 해당 기능을 원천 차단하는 것은 다소 어려움이 있다. 때문에 브라우저별로 어느 정도 차단할 지가 모두 다른 것이다.
팝업창이 열렸을 때, window.focus()
를 실행하는 것은 사실 좋은 생각이다. 여러 개의 브라우저가 동시에 돌아가고 있는 상황에서, 사용자가 현재 새로 생성된 윈도우 창에 접근하고 있음을 가장 명시적으로 알릴 수 있기 때문이다.
방문자가 실제로 웹 앱을 사용하고 있는지 추적하고자 한다면, window.onblur/onfocus
이벤트 리스너를 통해 쉽게 구현할 수 있다. 이는 페이지 내의 활동이나 애니메이션들을 지연 또는 재개할 수 있도록 설정할 수 있다. 이때 blur
이벤트는 사용자가 현재 윈도우 창 외부로 나갔음을 의미하지만, 여전히 관측되고 있다는 것에 주의하자. 윈도우 창은 백그라운드에서 떠 있지만 여전히 visible
한 상태이다.
동일 오리진 정책 (same site
) 윈도우 창과 다른 프레임이 상호 통신하는 것을 제한한다. 앞서 교차 출처 오리진을 다루며 동일 오리진 관련 내용 역시 함께 보았지만, 다시 한 번 핵심 아이디어를 정리하면 다음과 같다.
한 유저가 두 개의 페이지에 접근하고 있다고 가정하자. 하나의 페이지는 john-smith.com
URL이고, 다른 하나는 gmail.com
URL이라고 해보자. 이때 두 개의 URL은 모두 각자 다른 URL에 있는 스크립트가 서로에게 접근하는 것을 원하지 않을 것이다. 예를 들어 john-smith.com
에서 gmail.com
을 통해 메일에 접근할 수 있다면 심각한 보안위협일 뿐만 아니라 사생활을 침해할 수 있는 문제로까지 이어질 수 있다.
이러한 사용자 정보 도난 문제를 보호하기 위해 도입된 정책이 바로 동일 오리진 정책이다.
두 개의 URL이 서로 같은 도메인(domain
), 프로토콜(protocol
) 그리고 포트번호(port
)를 가지고 있다면 이들은 동일 오리진으로 판단할 수 있다. 즉 다음과 같은 URL은 모두 동일 오리진이다.
http://site.com
http://site.com/
http://site.com/my/page.html
그러나 다음과 같은 URL은 서로 동일 오리진이 아니다.
http://www.site.com
: www
는 다른 도메인을 의미http://site.org
: 완벽히 다른 도메인https://site.com
: https
로 프로토콜이 다름http://site.com:8080
: 포트번호가 다름다음의 상황에서는 동일 오리진 정책이 어떻게 적용이 될까? 만약 window.open
을 통해 새로운 팝업창을 열거나 또는 윈도우 창 내부에 있는 <iframe>
과 같은 별도의 브라우저 환경에 대한 참조를 가지고 있다고 가정해보자. 이때는 기준이 되는 윈도우 창과 새로 열린 팝업창 그리고 <iframe>
은 서로 동일 오리진으로 취급된다. 때문에 각각 서로 완전한 접근이 가능하다.
만약 동일 오리진 정책에 위배된다면, 다른 페이지에서 또 다른 페이지로 접근할 수 없다. 즉 다른 페이지의 내용이나, 변수, 문서 등 어떤 것도 접근이 불가하다. 이때 단 하나의 예외가 존재하는데 location
은 가능하다. location
을 이용해서 유저를 리다이렉트 시키는 것은 가능한데, 그렇지만 location
을 읽어와 현재 유저가 어디에 위치하는지에 대한 것은 여전히 접근할 수 없다.
<iframe>
태그를 통해 내장 형식으로 윈도우 창을 관리할 수 있다. 이때 iframe
은 독립된 환경의 document
와 window
객체를 가진다. iframe
의 프로퍼티를 통해서 해당 객체에 접근이 가능하다.
iframe.contentWindow
: iframe
내부에 위치한 window
에 접근iframe.contentDocument
: iframe
내부에 위치한 document
에 접근 (iframe.contentWindow.document
로도 동일한 값 접근이 가능)만약 내부에 내장된 윈도우에 접근을 하려고 하면, 브라우저는 당연히 iframe
이 동일 오리진을 가지고 있는지 체크한다. 만약 서로 다른 오리진이라면 당연히 접근은 거부될 것이다. 즉 동일 오리진 내에서만 여전히 iframe
에 접근할 수 있음을 말한다. (그러나 location
은 항상 예외에 해당한다)
아래 예시는 다른 오리진으로부터 iframe
을 읽거나 쓰려는 코드이다. 이때 다음과 같은 에러가 발생하는 것을 볼 수 있다.
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function () {
// 내부 window에 대한 참조 접근은 가능하다
let iframeWindow = iframe.contentWindow;
try {
// 그러나 동일 오리진이 아니면 document 접근은 불가
let doc = iframe.contentDocument; // ERROR
} catch (e) {
alert(e); // Security Error (another origin)
}
// 또한 iframe 내 페이지 URL을 읽는 것은 불가
try {
let href = iframe.contentWindow.location.href; // ERROR
} catch (e) {
alert(e); // Security Error
}
// 그러나 location을 쓰는 것은 가능하다. 항상 예외 취급
iframe.contentWindow.location = '/';
iframe.onload = null;
};
</script>
위 예시를 통해 다시 한 번 동일 오리진 정책에서 예외사항에 해당하는 항목을 정리하면 다음과 같다.
iframe.contentWindow
를 통해 내부 윈도우 참조를 가져오는 것location
을 조작하는 것반면 동일 오리진이라면 현재 페이지와 마찬가지로 모든 것을 제어하고 접근할 수 있다.
<iframe src='/' id='iframe'></iframe>
<script>
iframe.onload = function () {
iframe.contentDocument.body.prepend('hello');
};
</script>
iframe.onload
vsiframe.contentWindow.onlaod
본질적으로 두 이벤트는 서로 동일하다. 두 이벤트 모두 내부에 있는 윈도우가 완전히 로드되었을 때 발생하는 이벤트이다. 그렇지만 동일 오리진 정책에 의해iframe.contentWindow.onload
는 접근이 불가할 수 있다. 떄문에 해당 이벤트가 필요하다면iframe.onload
를 통해 접근하는 것이 더 안전하다.
일반적으로는 두 개의 URL이 서로 다른 도메인을 가지고 있는 경우 이는 서로 다른 오리진을 의미한다.
그러나 이때 second-level domain
이 같은 경우가 있을 수 있다. second-level domain
이란 하위 레벨의 도메인을 의미하는데 이는 도메인이 다르더라도 서로 공유하고 있을 수 있다.
john.site.com
peter.site.com
위 두 개의 URL에서 second-level domain
은 site.com
이 되며, 두 URL이 서로 동일하게 가지고 있다. 원칙적으로 두 URL은 서로 다른 오리진으로 취급되지만, 이를 동일 오리진으로 판단할 수 있도록 만들 수 있다.
document.domain = 'site.com'
두 페이지에서 모두 위의 코드를 실행시키면, site.com
을 second-level domain
으로 공유하는 두 페이지는 서로 동일 오리진으로 취급된다. 때문에 어떤 제한 없이 상호간의 통신이 가능하다. 만약 두 개가 아닌 여러개의 페이지가 이를 공유한다면, 각 페이지 모두 위의 코드를 실행시켜주어야 한다. 이는 앞서 쿠키 챕터에서 옵션 중에 domain
옵션과 유사한 역할을 한다.
만약 서로 다른 도메인이지만, 서브 도메인은 동일한 경우 이들간 교차 통신을 지원하게 하려면 다음과 같이 도메인 설정을 통해 가능하다.
iframe
을 통해 내부에서 윈도우를 통해 document
에 접근할 때 주의해야 할 사항이 몇 가지 있다. 이는 직접적으로 크로스 오리진과 관련되지는 않지만, 중요한 사항이기 때문에 같이 살펴보도록 하자.
iframe
은 생성되는 즉시 하나의 document
를 가지게 된다. 그런데 이때 document
는 실제로 iframe
내부에서 로드되는 document
와는 다르다. 다음 코드를 통해 이를 살펴보자.
<iframe src='/' id='iframe'></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function () {
let newDoc = iframe.contentDocument;
alert(oldDoc === newDoc); // false
};
</script>
때문에 iframe.contentDocument
를 통해 바로 doucment
에 접근하여 이벤트 리스너를 등록하고나, 노드를 조작하는 등의 행위는 원하는대로 동작하지 않을 수 있다. 이는 잘못된 document
이기 때문에, 실제로 iframe
내부에 진짜 document
로드가 완료되면 모든 조작이 무시될 것이다.
그렇다면 iframe
의 진짜 document
로드가 완료되는 순간을 어떻게 캐치할 수 있을까. 기본적으로는 iframe.onload
이벤트가 발생할 때 접근하는 iframe.contentDocument
는 진짜 document
임을 보장한다. 이는 iframe
이 관련된 모든 리소스와 함께 로드가 완료되었을 때 발생하는 이벤트이기 때문이다.
때로는 관련 리소스가 너무 많은 경우 로드가 완료되기 까지 오랜 시간이 걸릴 수 있다. 이때 이와 관계없이 진짜 document
더 빠르게 접근하고자 한다면 setInterval
스케쥴링 함수를 이용해 다음과 같이 처리할 수 있다.
let oldDoc = iframe.contentDocument;
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc === oldDoc) return;
alert('New Document is here!');
claerInterval(timer);
}, 100);
또 다른 방법으로 iframe
내부 객체에 접근할 수 있는 방식이 있다. 폼(form
)에서 폼 관련 컬렉션을 지원하는 것처럼, iframe
역시 관련 컬렉션을 지원하는데, 이를 통해 원하는 window
전역객체에 접근이 가능하다. 해당 컬렉션은 window.frames
라는 이름으로 사용할 수 있다. 이때 인덱스로 접근 또는 iframe
의 이름으로 접근이 가능하다.
window.frames[0]
- 현재 문서에서 존재하는 첫 번째 iframe
의 window
window.frames.iframeName
- 현재 문서에서 iframeName
과 일치하는 iframe
의 window
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
이때 하나의 iframe
안에 여러개의 iframe
이 또 존재할 수 있다. 그리고 내부의 iframe
역시 안에 다른 iframe
을 가질 수 있는 중첩구조가 가능하다. 이때 iframe
의 각 window
전역객체는 계층적으로 구성된다.
window.frames
: (중첩된 프레임 포함) 현재 윈도우 객체 내의 iframe
(children
)window.parent
: 현재 참조된 프레임의 바깥 윈도우 객체window.top
: 가장 최상단에 위치한 윈도우 객체예를 들면 다음이 성립한다.
window.frames[0].parent === window; // true
top
프로퍼티를 이용하면 가장 상단에 있는 document
가 iframe
내부에 있는지 아닌지 검사할 수 있다.
if (window === top) {
alert('최상단 window');
} else {
alert('iframes 내부');
}
sandbox
개념은 가상머신 등을 다룰 때 등장하는 개념 중에 하나로 독립된 별개의 공간을 의미한다. 어딘가에 종속되어 있지 않기 때문에, 샌드박스 내에서 실행되는 작업의 결과는 외부로 영향을 주지 않는다. 이와 유사하게 iframe
에서도 샌드박스 개념을 적용시킬 수 있다.
iframe
은 sandbox
속성을 제공하는데, 해당 속성을 사용하게 되면 iframe
내부에서 일어나는 특정 동작을 막을 수 있다. 이는 iframe
에서 신뢰할 수 없는 코드가 실행되어 외부로 치명적인 영향을 끼치는 것을 방지하는 용도로 활용할 수 있다. 예를 들어 동일 오리진이라고 할 지라도 신뢰할 수 없다고 판단되는 경우에 적용할 수 있다.
기본적인 설정은 <iframe sandbox src="...">
와 같은 형태로 선언한다. 해당 설정은 기본적으로 가장 엄격한 제한조건이다. sandbox
속성은 해당 iframe
을 기본적으로 다른 오리진에서 온 것으로 만들어버리기 때문에, 동일 오리진 정책으로 인해 대부분의 접근이 차단된다. 그러나 몇 가지 옵션을 전달해주는 것으로 특정 기능은 허용이 가능하다. 이때 허용해 줄 수 있는 옵션은 다음과 같은 것들이 있다.
allow-same-origin
기본값 sandbox
는 iframe
을 무조건 다른 오리진으로 간주한다. 때문에 src
가 서로 동일한 오리진이더라도 동일 오리진 정책에 의해 차단된다. 해당 옵션을 사용하면 동일 오리진인 경우에는 허용한다.
allow-top-navigation
iframe
이 parent.location
을 변경하는 것을 허용한다.
allow-forms
iframe
으로부터 폼을 제출하는 것을 허용한다.
allow-scripts
iframe
으로부터 스크립트 실행을 허용한다.
allow-popups
iframe
으로부터 window.open
을 통해 팝업창을 띄우는 것을 허용한다.
sandbox
속성은 현재 iframe
을 기준으로 제한을 추가하는 것이다. 즉 애초부터 다른 오리진을 사용하는 iframe
에게 sandbox="allow-same-origin"
을 허용한다고 해서 동일 오리진 정책이 적용되지 않는다.
postMessage
인터페이스를 사용하면 윈도우가 어느 오리진에서 왔건 상관없이 서로 문제없이 통신을 주고받을 수 있다. 하지만 무조건적으로 모든 코드를 허용하는 것은 역시 심각한 보안 위협을 야기할 수 있기 때문에, 이를 위해서는 상호간의 동의가 꼭 필요하다. 그리고 동의한 내용에 걸맞은 자바스크립트 코드가 호출되어야 한다.
메시지를 전송하기를 원하는 윈도우 창은 수신할 창의 postMessage
메서드를 호출한다. 순서가 약간 헷갈릴 수 있는데, 만약 win
이라는 윈도우에게 메시지를 전송하고 싶다면 win.postMessage(data, targetOrigin)
과 같은 형식으로 사용한다. postMesage
메서드의 인자는 다음과 같다.
data
보내고자 하는 데이터를 의미한다. 어떠한 객체도 데이터가 될 수 있으며, 객체 직렬화를 위해 structured cloning
알고리즘을 사용한다. IE
의 경우는 오직 문자열만 지원하기 때문에, JSON.stringify
를 통해 객체를 문자열로 전환해서 전송해야 한다.
targetOrigin
타겟으로 삼은 윈도우의 오리진을 지정한다. 지정된 오리진과 일치하는 윈도우만 메시지를 받을 수 있다.
targetOrigin
프로퍼티는 안전성을 위한 수단으로 사용한다. 만약 타겟 윈도우가 다른 오리진에서 오는 경우에는 해당 윈도우의 location
을 전송측의 윈도우에서 읽을 수가 없다. 때문에 현재 어떤 사이트가 열려있는지를 정확하게 검증하기 힘들다.
targetOrigin
을 명시하게 되면, 해당 오리진과 일치하는 경우에만 데이터를 수신할 수 있다. 만약 데이터가 보안에 민감한 정보일 경우에는 해당 옵션을 활성화하여 사용하는 것을 추천한다.
예를 들어 다음 예시에서 win
윈도우 창은 http://example.com
오리진으로 부터 온 document
를 가지고 있는 경우에만 메시지 수신이 가능하다.
<iframe src="http://example.com" name="example"></iframe>
<script>
let win = window.frames.example;
win.postMessage('message', 'http://example.com');
</script>
만약 특별하게 오리진 제한을 두지 않으려면 *
를 지정해주면 된다. 이 경우엔 오리진이 서로 다른 경우까지 모두 메시지 전송이 가능하다.
메시지를 수신하기 위해서 타겟 윈도우는 message
이벤트 핸들러를 관리해야 한다. 해당 이벤트는 postMessage
가 호출되었을때 발생한다. 이때 만약 targetOrigin
이 활성화 되었다면 해당 조건까지 모두 통과한 뒤에 message
이벤트가 발생한다. message
이벤트는 다음과 같은 프로퍼티를 가진다.
data
postMessage
를 통해 전달된 데이터
origin
송신측의 오리진을 의미
source
송신측 윈도우의 참조를 의미. 이를 통해 source.postMessage(...)
를 통해 즉시 송신 윈도우에게 답장 가능
message
핸들러를 등록하기 위해서는 항상 addEventListener
를 사용해 등록해주어야 한다. 요소에 직접 할당하는 onmessage
방식은 동작하지 않음에 주의하자.
window.addEventListener('message', function(event) {
if (event.origin !== 'http://javascript.info') {
// 알려지지 않은 도메인일 경우 무시
return;
}
alert('received: ', event.data);
// 송신측에 바로 답장...
// event.source.postMessage(...);
});
클릭잭킹(clickjacking
)이란 사용자의 클릭을 가로채는 의미이고, 클릭재킹 공격은 가로챈 사용자의 클릭으로 악의적인 의도를 가진 행위를 가하는 유형의 공격을 말한다.
과거 많은 유명한 사이트들도 이러한 방식의 클릭재킹 공격을 당한 사례가 있다. 클릭재킹은 기본적으로 버그로 인한 공격이 아닌, HTML과 CSS 스펙을 교묘하게 악용해서 가하는 공격이기 때문에 버그를 잡아 차단할 수 있는 스펙의 공격은 아니다. 그렇지만 오늘날 대부분의 모던 브라우저에선 이러한 클릭재킹 공격을 방어하기 위한 방법이 많이 고안되었고 적용되어 있다.
클릭재킹 프로세스는 매우 심플하다. 페이스북을 이용한 클릭재킹 사례를 살펴보자.
transparent
속성으로 숨겨진 iframe
을 해커가 배치한다. 해당 iframe
은 src
를 facebook.com
으로 설정하고, '좋아요' 버튼을 링크 바로 위에 올려놓고 z-index
를 사용해 클릭할 수 있도록 최상단에 배치한다.페이지를 숨기는 방법은 약간의 CSS 지식만 알고 있다면 매우 간단하게 처리할 수 있다. 아래 예시에서는 iframe
을 버튼 위에 CSS를 통해 보이지 않도록 처리하고 있다.
<style>
iframe {
width: 400px;
height: 100px;
position: absolute;
top: 0; left: -20px;
opacity: 0; /* html 상 존재하지만 투명처리로 보이지 않음 */
z-index: 1;
}
</style>
<div>부자가 되는 법을 알려드립니다!</div>
<!-- 실제로 존재하는 URL이 아닌 예시이다 -->
<iframe src="/clickjacking/facebook.com"></iframe>
<button> 클릭하세요 </button>
아무 생각없이 유저는 클릭버튼을 누르게 되면, 실제 눌리게 되는 것은 iframe
에 담겨있는 어떤 버튼 또는 링크가 될 것이고 따라서 유저의 의도와는 다른 동작이 수행될 것이다. 이처럼 클릭재킹은 아주 교묘한 방법을 통해 유저의 클릭 인터랙션을 가로채가는 행위이다.
요즈음엔 대부분의 사용자가 자동로그인 기능을 많이 사용하고 있기 때문에, 만약 이러한 클릭재킹 공격에 대한 방비가 되어있지 않다면 악용될 여지가 매우 많다. 사용자의 로그인 정보를 이용해서 할 수 있는 대부분의 행위는 개인정보를 다루기 때문에 보안과 매우 밀접한 관련을 맺고 있기 때문이다. 이를 위해서는 기본적으로 사용자의 인터랙션, 특히 클릭 이벤트가 가장 먼저 발생해야 한다. 따라서 클릭재킹 공격의 관건은 CSS 스타일링을 이용하거나, 또는 어떤 다른 방법을 이용해서라도 악의적인 의도를 가진 레이아웃을 숨기고 유저의 클릭을 이끌어내는 것이다.
이때 유저와 상호작용 할 수 있는 이벤트는 단순히 클릭뿐만이 아닌데 클릭재킹이라는 이름으로 불리는데는 단순한 이유가 있다. 유저의 클릭 이벤트는 악의적인 해커가 가로채더라도 유저가 무언가 잘못되었음을 가시적으로 느끼기가 어렵기 때문이다.
만약 키보드 이벤트 중 keydown
이벤트를 이와 동일한 방법으로 가로채보려고 한다 생각해보자. 역시 CSS와 iframe
등을 이용해서 레이아웃을 숨기고, 유저로부터 입력을 유도할 수 있다. 그러나 keydown
이벤트는 항상 유저의 입력값이 가시적으로 즉각 화면에 출력된다. 그러나 기본적으로 악의적인 의도를 가지고 있는 iframe
등의 레이아웃은 숨김처리 되어있기 때문에, 입력된 키값이 유저에게 보이지 않을 것이다. 따라서 유저는 해당 동작이 악의적인 공격에 이용당할 수 있다는 점까지는 파악할 수 없을지 몰라도, 무언가 잘못됨을 느끼고 금방 해당 페이지를 벗어나려는 시도를 쉽게 할 수 있다.
클릭재킹은 오래전부터 브라우저가 보안에 취약하던 시절 많이 사용되던 공격 방법이었다. 때문에 과거에도 이러한 공격을 막기 위해 자체적으로 클릭재킹을 방어하고자 보통 자바스크립트를 이용했다. 대부분의 방식은 오늘날 그대로 이용하기에 비교적 보안 강도가 약하지만, 어떻게 자바스크립트를 통해 클릭재킹을 막으려 했는지 전체적으로 훑어보도록 하자.
가장 오래된 방식 중에 하나는 framebusting
이라고 불리는 방법으로 아래와 같이 구현할 수 있다.
if (top !== window) {
top.location = window.location;
}
이는 만약 현재 보여지는 페이지가 가장 상단의 페이지가 아니라면, 자동으로 상단 페이지로 이동하도록 하는 코드이다. 보통 클릭재킹 공격이 내부 iframe
을 이용해 시도된다는 점에서 착안하여 이를 막고자 한 방법이다.
그러나 이는 신뢰할 수 있는 방어수단이 되지 못한다. 해당 수단 역시 우회적으로 뚫을 수 있는 공격 방법이 존재하기 때문이다. 이를 조금 더 보완한 방법들을 살펴보자.
top.location
을 변경하는 것으로 발생하는 전환은 beforeunload
이벤트를 통해 블락을 걸 수 있다. 해커가 심어둔 무언가를 포함하고 있는 상단 페이에 다음과 같은 차단용 핸들러를 등록할 수 있다.
window.onbeforeunload = function () {
return false;
}
해커가 심어둔 iframe
이 top.location
을 변경하려고 할 때, 페이지 방문자는 해당 페이지를 떠나고 싶어하는 지를 묻는 브라우저의 메시지를 받게 될 것이다. 해당 이벤트는 앞서 문서와 리소스 로딩을 다룬 챕터에서 자세히 설명한 바가 있다. beforeunload
이벤트의 기본동작을 취소하게 되면, 브라우저 자체적으로 설정된 안내메시지를 출력하도록 할 수 있다.
이는 대부분의 유저가 해당 메시지에 '취소' 버튼을 클릭하리라고 예상하고 만든 방어책이다. 방문자는 자신도 모르는 사이에 의도치 않은 해커의 iframe
을 클릭하게 되었을 것이기 때문에, 기본적으로 현재 페이지로부터 벗어나게 된다. 하지만 이는 사용자의 의도와 벗어난 행동이기 때문에, 갑자기 현재 페이지를 떠날 것인지 의중을 물어보는 메시지가 뜨게 된다면 사용자는 의아해 할 수 밖에 없다. 이러한 심리를 이용해서 악의적인 클릭재킹 공격을 차단하려는 시도이다.
앞서 iframe
에 대한 보안을 다룰 때 sandbox
속성을 살펴보았다. 해당 속성을 이용하면 iframe
에 보다 강력한 제한을 설정할 수 있었다. 이때 지원되는 옵션 중에는 네비게이션(navigation
)과 관련된 제한도 있었던 것을 떠올리자. sandbox
가 설정된 iframe
은 기본적으로 top.location
을 변경할 수 없다.
만약 iframe
을 사용하고 있지만, 의도치 않은 악의적인 공격을 방어하기 위해서는 sandbox
속성을 사용할 수 있다. 이때 허용할 기능만 sandbox
의 옵션으로 지정해주면 원천적으로 location
변경을 가하는 동작은 방지하면서 iframe
과 상호 통신할 수 있는 환경 조성이 가능하다.
<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>
이 밖에도 또 다른 방식들로 클릭재킹을 막을 수 있다. 다음으로는 오늘날 사용하는 조금 더 강력한 보안을 자랑하는 방식들을 살펴보도록 하자.
서버 사이드에서 헤더 설정을 통해 페이지 내에 iframe
을 보여주는 것을 막을 수 있다. 이때 사용하는 헤더는 X-frame-Options
로 서버 사이드에서 응답하는 헤더로 정의되어 있다. 해당 해더는 오늘날 대부분의 모던 브라우저에 기본 사양으로 적용되어 있다. 다만 해당 헤더는 정확히 HTTP 통신을 통해 전달되어야 한다. 만약 HTML 내부에서 HTTP 헤더가 제공하는 정보와 동일한 사양을 적용할 수 있는 meta
태그를 이용하더라도, X-frame-Options
는 적용되지 않는다는 것에 주의하자. 예를들어 <meta http-equiv="X-Frame-Options" ...>
는 제대로 동작하지 않는다.
해당 헤더는 다음과 같이 3가지 값을 가질 수 있다.
DENY
페이지 내부에 있는 iframe
을 절대 보여주지 않는다.
SAMEORIGIN
동일 오리진의 경우에만 내부 iframe
을 표시하고, 그 외에는 보여주지 않는다.
ALLOW_FROM domain
명시된 domain
에 해당하는 소스를 가지고 있는 내부 iframe
만을 보여준다.
예를 들어 트위터의 경우에는 X-Frame-Options: SAMEORIGIN
이 응답헤더로 설정되어 있다. 만약 해당 헤더를 통해 특정 iframe
이 차단되는 경우에는 브라우저별로 조금씩은 다르지만 유저가 이를 알아볼 수 있도록 별도의 디자인이나 메시지를 보여준다. 아래는 크롬 브라우저에서 차단된 iframe
을 보여주는 양식이다.
상기 언급한 X-Frame-Options
는 부작용이 있을 수 있다. 만약 우리의 페이지가 어쩔 수 없이, 또는 꼭 iframe
을 내부에 사용해야 하는 경우라도 응답헤더에 따라 iframe
을 전혀 사용할 수 없게 될 수 있다.
때문에 이를 해소하는 다른 해결책도 있다. 가장 대표적으로, 현재 페이지 전체를 뒤덮는 <div>
태그를 하나 선언한다. 해당 태그는 width: 100%; height: 100%
과 같이 현재 렌더링 되는 페이지를 모두 뒤덮는 일종의 커버 역할을 수행한다. 그리고 해당 태그는 HTML의 가장 앞단에 배치되어 사용자의 클릭을 가장 먼저 우선적으로 가로챌 수 있다. 이러한 블락용 태그는 window === top
조건을 만족하는 경우에만 페이지로부터 제거해 사용자의 클릭을 보호하도록 설정할 수 있다. 아래 예시를 살펴보자.
<style>
#protector {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 99999999;
}
</style>
<div id="protector">
<a href="/" target="_blank">이동</div>
</div>
<script>
if (top.document.domain === document.domain) {
protector.remove();
}
</script>
이러한 방식은 사실 해커가 클릭재킹을 위해 시도하는 방식을 그대로 응용하는 것과 같다. 해커는 숨겨진 iframe
을 원하는 버튼위에 숨겨 가장 상단에 배치해서 클릭을 가로채는데, 이와 동일하게 HTML 최상단에 보호용 태그를 전체크기로 적용시켜 해커가 심어둔 iframe
보다 앞단에 서서 유저의 모든 요청을 먼저 가로채는 것이기 때문이다.
앞서 쿠키에 대해 다룰때 이미 samesite
옵션에 대해 살펴본 적이 있다. 해당 옵션을 이용해서도 클릭재킹을 막을 수 있다. 해당 옵션이 적용된 쿠키는 옵션에 따라 약간의 차이가 있지만, XSRF
공격을 막기 위해 외부에서의 요청 시 절대 전송되지 않는다.
물론 samesite
는 클릭재킹 공격을 막기 위해 고안된 방법이 아니기 때문에, 만약 쿠키를 사용하지 않는 환경에서는 그 쓸모가 없을 수 있다. 유저 인증정보 등을 쿠키에 저장하지 않는 퍼블릭한 환경의 웹 페이지라면 그닥 효과적이지 못 한 방법이다.
그렇지만 요즘 대부분의 사이트에서는 보안 정보를 쿠키와 세션을 이용해서 저장하고 관리하는 경우가 많고, 클릭재킹 공격으로 보안상 큰 위협을 가하는 경우 또한 이런 쿠키에 저장된 개인 보안정보를 이용해 악용하는 경우가 많다. 따라서 쿠키를 안전하게 관리하는 것 역시 클릭재킹 공격으로부터 소중한 우리의 개인정보와 보안을 지키는 초석이 될 수 있다.
보안과 관련해서 항상 개발자가 염두에 두어야 하는 격언과 같은 말이 있다. 누가 최초로 주장했는지는 모르겠지만 절대 사용자를 믿지 말라
라는 말은 한 번쯤을 들어보았을 것이다. 대부분의 브라우저에서 보안상 위협이 되는 부분들은 오늘날의 모던 브라우저에서 자체적으로 차단하고 있는 경우가 많지만, 그럼에도 불구하고 항상 보안에 신경써야 한다.