ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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
Designed by Tistory.