-
[HTML+JS] window.postMessage() - <iframe> 과의 통신Front-End(Web)/HTML, CSS 2022. 2. 24. 22:27반응형
현재, 회사의 서비스 분리 및 병합 등이 이루어지는 과정이어서, 선배 개발자님과 통합 멤버십 페이지를 개발하고 있다.
지금은 멤버십 개발 및 QA는 마무리된 상태이며, 이를 각 서비스에 연동하는 과정이며 여기서 많은 정책의 변화가 수반되고 있다.
그 중 하나가, 서비스에서 본인인증이 필요할 시, 멤버십 본인인증 창을 외부 팝업(일종의 모달) 형태로 띄우고자 하는 것이다.
팝업을 띄우는 다양한 방법이 있겠지만, <iframe> 태그를 통해 내부에 웹페이지를 삽입하는 형태를 구상했다.
이 때, 내부 페이지의 X버튼을 클릭하면 외부 페이지가 이를 인식하고 창을 닫는 로직이 필요했으며, 이를 위해 window.postMessage() 라는 브라우저 이벤트 및 리스너를 활용하게 되었다.
내부에 웹을 띄우는 방법은 다방면에서 적용되고 있으며(IAMPORT 휴대폰 인증, 온라인뱅크 등), 이를 간단히 구현해볼 수 있었던 위 경험을 정리하면서 학습해보고자 이 포스팅을 시작했다.
📙 <iframe> 태그?
먼저, 외부 팝업창을 띄우기 위해 <iframe> 태그를 사용할 필요가 있었다.
HTML Inline Frame의 약자로, 현재 페이지에 다른 HTML 페이지를 포함시키는 중첩된 브라우저의 목적으로 사용된다.
iframe은 Frame의 일종이다. Frame은 브라우저 창을 여러 세그먼트로 분할해 복수의 페이지들을 한 페이지에 표현하기 위한 태그였다.
하지만, 보안 등 이슈로 W3C에서 사용을 지양하면서 HTML5 규격에서는 제외되었으나, 웹사이트 내에서 배너 혹은 플러그인등을 제공하
기 위해 <iframe> 태그는 계속 지원하고 있는 것이다.
- <iframe> 문법
<iframe src="url"></iframe>
이처럼, <iframe> 태그와 표현하고자 하는 웹페이지 url주소를 넣어 간단하게 사용할 수 있다. 또, 태그에 관한 여러 attribute 들이 있다.
- name : iframe 이름
- src : 페이지 경로
- srcdoc : html을 직접 넣는다.(srcdoc="<b>test</b>")
- width/height : 프레임 크기
- frameborder : 프레임 테두리 선 (0은 없음, 1은 있음)
- scrolling : 스크롤바 표시 여부 (auto는 자동, yes는 무조건)
- align : 정렬방식 (left, right, middle, absmiddle)
- seamless : 경계선 없이 문서의 일부인 것처럼 렌더
- allow : fullscreen, payment API 등의 액세스 허용 여부 (allow="payment")
- referrerpolicy : iframe을 가져올 때 리퍼러 정보 (origin, origin-when-cross-origin, strict origin, same-origin 등)
- target : 문서가 나타날 프레임을 설정한다.
- _self : 현재 포커스가 있는 프레임에서 연다.(default)
- _blank : 새창에서 열기
- _parent : 부모 프레임 영역에 열기
- _top : 무조건 전체 영역에 열기
- 프레임명 : 해당 프레임 영역에 열기
- sandbox : 보안을 위해 iframe에서 <form> 태그나 Javascript 등의 실행을 막을 수 있다.
- 기본 : 모든 제한사항을 적용
- allow-forms : <form> 허용
- allow-modals : window modal을 열도록 허용 (alert, prompt 등)
- allow-same-origin : 도메인이 달라도 CORS를 통과한 것처럼 허용
- allow-scripts : 스크립트를 실행할 수 있도록 허용
- allow-popups : window.open 등의 팝업 허용
- allow-storage-access-by-user-activation : Storage API 허용
- <iframe> 과 Browsing Context
Browsing Context는 브라우저가 표시하는 환경이며, 이는 고유한 Session History와 Document를 가진다.
<iframe> 역시 독립적인 Browsing Context를 가지며, 각 Browsing Context는 연관된 WindowProxy 객체를 가진다. (window)
- window : 본인의 Browsing Context
- window.top : 최상위 Browsing Context
- window.parent : 부모 Browsing Context
- window.frameElement : 부모 Browsing Context 내 window elements를 반환
<iframe>에 로드된 페이지는 부모 페이지와 별개이며, 그렇기에 별도의 window context를 가지게 된다.
부모는 documents.getElementByTagName('iframe') 혹은 window.frames로 내부 <iframe> 을 확인할 수 있으며,
이는 frameList(ArrayLike) 이기 때문에 각 요소의 contentWindow로 접근하면 된다.
또한, <iframe>은 부모 window에 접근하기 위해 window.parent로 접근하는데 이 부분은 뒤에 구체적으로 다루겠다.
* <iframe> 과 DOM
브라우저가 HTML을 전달받으면 Parsing을 통해 Dom Tree(Render Tree)를 구축한다.
이 때 <iframe>을 만나면, Parsing은 중단되지 않고 Node에 Iframe을 추가하며 새로운 Render Layer를 만든다.
- <iframe> 의 장단점
HTML5에서도 <iframe>의 사용을 지양할 것을 권고한다. 페이지를 손쉽게 추가하나, 아래와 같은 단점들이 그 이유일 것이다.
- 보안 위험을 유발한다. <iframe>으로 구현된 사이트는 XSS(사이트 간 공격) 에 취약하다. (데이터 피싱, 도용 등)
- 사용성 문제를 일으킨다. 뒤로가기 버튼이 간헐적으로 작동하지 않거나, 해상도에 따라 이상하게 보일 수 있다. URL이 변경되면 표현되지 않는다.
- 웹 크롤링을 지연시킨다. 웹 크롤링에 문제를 일으킬 수 있으며, <iframe> 내부 페이지는 웹 크롤링 및 스크린리더에 읽히지 않는다.
📒 window.postMessage()
window 객체간에 안전하게 cross-origin 통신을 하기 위한 메서드이다. 주로, 페이지에서 생성된 팝업 혹은 iframe 간 통신에 활용된다.
- 메세지 송신 : window.postMessage()
[targetWindow].postMessage(message, targetOrigin, [transfer])
- targetWindow : 메세지를 전달받을 window를 참조.
- window.open : 새 창을 만들고, 해당 창을 참조할 때.
- window.opener : 새 창을 만들고, 해당 창을 만든 window를 참조할 때.
- HTMLIFrameElement.contentWindow : 부모 window에 임베디드된 <iframe> 을 참조할 때.
- window.parent : 임베디드된 <iframe> 에서 부모 window를 참조할 때.
- window.frames[index] : index번째 Frame을 참조할 때.
- message : 다른 window로 보내질 데이터. 객체를 직렬화하여 보낸다.
- targetOrigin : 메시지를 전달할 윈도우의 출처를 명시한다. 스키마, 호스트명, 포트가 맞지 않으면 이벤트를 전송하지 않는다. "*" 를 통해 별도로 지정하지 않을 수 있지만, 특정값을 설정하지 않으면 악의적으로 데이터가 공개될 위험성이 있다.
- transfer : Optional 프로퍼티. 일련의 Transferable 객체들로, 메세지와 함께 수신되며 전달이 완료되면 송신측에선 사용이 불가함.
- 메세지 수신 : message 이벤트
window.addEventListener("message", receiveMessage, false); const receiveMessage = (e) => { // 내부 로직 }
이처럼, 수신할 페이지에서 이벤트 리스너를 등록한다. 이 때, 해당 이벤트를 제어할 핸들러 함수를 등록한다.
여기로 입력되는 event 객체의 내부 프로퍼티는 아래가 있다.
- data : postMessage() 로 보내진 데이터. 객체 데이터는 stringify() 되어있으므로 파싱해서 사용해야한다.
- origin : 메세지를 보낸 출처 페이지의 도메인.
- source : 메세지를 보낸 출처 페이지의 window 객체.
- ports : 메세지를 보낸 출처 페이지의 포트번호.
💻 구현예시
이론이 긴 내용은 아니므로, 내가 서비스에서 구현한 코드를 예시로 들며 포스팅을 마무리하겠다.
1) 내부 컨텍스트(멤버십) : postMessage() 설정
// PopupCloseButton.vue <template> <button v-if="isIframe" popup-close-button @click="closePopup"> <svg-x /> </button> </template> <script> // impot ... export default { // ... props: { needRefresh: { type: Boolean, default: false }, }, computed: { isIframe: state('common', 'isIframe'), // self !== top message() { return { message: this.needRefresh ? 'refresh' : 'close' }; }, }, methods: { closePopup() { window.parent.postMessage(this.message, '*'); }, }, }; </script>
- <PopupCloseButton> 컴포넌트는 Iframe 에서만 조건부로 노출되는 닫기 버튼이다.
- self와 top이 다르면 현재 컨텍스트는 어딘가에 종속된 <iframe> 인 것이다. 이를, store에서 추적하며 true인 경우 이 컴포넌트를 노출한다.
- message 속성은 props(needRefresh)에 따라 'refresh' 혹은 'close' 메세지를 선택한다. 창이 닫친 뒤 새로고침할지 여부이다.
- closePopup() 메서드는 버튼을 클릭하면 메세지를 보내는 동작이다. window.parent로 postMessage() 메서드를 발생시킨다.
2) 외부 컨텍스트(LVUP 서비스) : <iframe> 및 MessageEvent 설정
// PopupAuthorization.vue <template> <div popup-auth @click="close"> <iframe :src="iframeSrc" :width="mediaWidth" :height="mediaHeight" /> </div> </template> <script> export default { // ... computed: { iframeSrc() { return process.env.VUE_APP_MEMBERSHIP_AUTH_URL; }, isModalSize() { return this.matchedMediaDevice === "D" || this.matchedMedia === "TL" }, mediaWidth() { return this.isModalSize ? '410px' : '100%' }, mediaHeight() { return this.isModalSize ? '680px' : '100%' } }, methods: { closeAuth(event) { const { data: { message } } = event; if (message === 'refresh') location.reload(); this.close(); }, close() { this.$emit('close'); }, }, mounted() { window.addEventListener('message', this.closeAuth); }, beforeDestroy() { window.removeEventListener('message', this.closeAuth); }, };
- <PopupAuthorization> 컴포넌트는 본인인증을 시작하는 버튼 클릭시 열리는 모달 형태의 컴포넌트이다.
- <div>는 전체 딤영역, 그 안에 <iframe>을 띄운다. iframeSrc 속성은 env에 저장된 멤버십 본인인증 url 링크이다.
- 또한, 전역에서 브라우저 사이즈를 감지하는 속성으로, 폭이 좁을땐 100%, 넓을땐 특정 사이즈를 가진 width, height를 전달했다.
- close()는 'close'를 $emit 해서 모달을 닫는 메서드, closeAuth()는 event.data.message를 읽고 'refresh' 혹은 'close' 를 선택하는 메서드이며 이를 이벤트 리스너에 인자로 전달한다.
- 가장 중요한 건, 이 컴포넌트가 모달로 열리면(mounted) 이벤트 리스너를 추가하고, 닫힐땐 destroy 전에 이벤트 리스너를 지운다.
📎 출처
- [iframe 통신] Tamm 님의 블로그 : https://medium.com/@tamm_/%EA%B0%9C%EB%B0%9C-window-postmessage%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-iframe-%ED%86%B5%EC%8B%A0-a542b8536518
- [iframe] okayoon 님의 블로그 : https://okayoon.tistory.com/entry/%EC%95%84%EC%9D%B4%ED%94%84%EB%A0%88%EC%9E%84iframe
- [iframe] pks2974 님의 블로그 : https://pks2974.medium.com/iframe-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0-1cd866b71c8f
- [iframe 단점] syudal 님의 블로그 : https://syudal.tistory.com/entry/html-iframe%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0-%EC%9D%B4%EC%9C%A0-%EB%8B%A8%EC%A0%90.
- [postMessage] MDN 공식문서 : https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage
- [Message Event] MDN 공식문서 : https://developer.mozilla.org/ko/docs/Web/API/MessageEvent
- [postMessage 교차출처 통신] noritersand 님의 블로그 : https://noritersand.github.io/javascript/javascript-window-postmessage-%EA%B5%90%EC%B0%A8-%EC%B6%9C%EC%B2%98%EA%B0%84-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%86%B5%EC%8B%A0/
반응형'Front-End(Web) > HTML, CSS' 카테고리의 다른 글
[CSS/Side Lib.] Emotion.js (0) 2022.07.21 [HTML] 이메일 폼 퍼블리싱 (4) 2022.03.30 [CSS/Side Lib.] Tailwind CSS (0) 2022.01.23 [CSS/Lang] LESS (0) 2021.08.12 [CSS/Side Lib.] Styled Components (0) 2021.01.24