임베딩과 iframe


가끔은 페이지에 넣고 싶은 것이 이미 다른 곳에 존재하는 경우가 있습니다. 유튜브에 있는 동영상, 지도 위의 한 위치, 다른 회사가 운영하는 결제 폼 같은 것들이죠. 이런 것을 처음부터 새로 만드는 대신, 그 페이지 전체를 여러분의 페이지 안에 통째로 배치할 수 있습니다. 이 역할을 하는 요소가 <iframe>이며, 가져온 콘텐츠의 크기와 접근성, 안전성을 조절할 수 있는 여러 속성이 함께 제공됩니다.
iframe 요소
**iframe**은 다른 웹 페이지를 여러분의 페이지 안에 끼워 넣습니다. 이 이름은 "inline frame(인라인 프레임)"의 줄임말인데, 이 표현이 딱 맞습니다. 페이지 안에 작은 창틀이 하나 있고, 그 창을 통해 완전히 별개의 페이지가 보이는 것이죠. 블로그 글에 있는 유튜브 동영상은 거의 항상 iframe입니다.
이미지가 파일을 가리킬 때 쓰는 것과 같은 src 속성으로 보여주고 싶은 페이지를 지정합니다.
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ"></iframe>이 태그 하나로 유튜브 플레이어를 페이지에 가져올 수 있습니다. 벽에 창문을 하나 걸어 놓았다고 생각해 보세요. 벽은 여러분의 페이지이고, 그 창문을 통해 완전히 다른 방이 보이는 셈입니다.
<iframe>은 별개의 완전한 웹 페이지를 불러와 여러분의 페이지 안에 사각형 형태로 보여줍니다. src 속성에는 그 페이지의 주소가 들어가며, 프레임 안의 모든 것—고유의 HTML, CSS, JavaScript—은 여러분의 페이지와 분리되어 독립적으로 동작합니다.
<iframe src="https://www.openstreetmap.org/export/embed.html"></iframe>여는 태그와 닫는 태그 사이에 작성한 내용은 대체 콘텐츠로, 프레임을 지원하지 않는 아주 오래된 브라우저에서만 표시됩니다. 실제로는 그런 브라우저가 거의 없습니다. 프레임은 전적으로 속성을 통해 설정되며, 다음 절에서 실제로 설정하게 될 속성들을 다룹니다.
<iframe>은 **중첩된 브라우징 컨텍스트(nested browsing context)**를 만듭니다. 즉, 여러분의 문서 안에 완전하고 독립적인 문서가 하나 더 들어 있는 형태입니다(브라우징 컨텍스트란 하나의 페이지가 실행되는 환경으로, 자신만의 히스토리, 자신만의 전역 스코프, 자신만의 DOM 복사본을 가집니다). 프레임은 어떤 의미로도 여러분의 페이지의 DOM 트리에 속하지 않습니다. 그저 여러분의 페이지 안 한 상자 안에 그려지는 별개의 페이지일 뿐입니다.
이 분리야말로 핵심이며, **동일 출처 정책(same-origin policy)**이 이를 강제합니다. 여러분의 페이지와 프레임 안의 페이지는 출처(origin)를 공유할 때만 서로를 읽거나 스크립트로 접근할 수 있습니다(출처란 scheme, host, port의 조합이므로, https://example.com과 https://other.com은 서로 다른 출처입니다). 동일 출처 프레임은 iframe.contentWindow를 통해 JavaScript로 접근할 수 있지만, 교차 출처 프레임은 겨냥할 수는 있어도 들여다볼 수는 없는 밀봉된 상자입니다. 여러분이 배치하는 임베드—동영상, 지도, 위젯—는 거의 전부 교차 출처이고, 바로 그래서 안전하게 배치할 수 있는 것입니다.
<iframe>은 여러분의 페이지 안에 완전히 다른 웹 페이지를 보여줍니다. 마치 다른 방을 들여다보는 작은 창문 같은 것이죠. 이미지가 그러듯 src로 어디를 가리킬지 지정합니다. 블로그에 있는 그 유튜브 플레이어? 거의 항상 이겁니다. <iframe>은 독립적이고 별개인 페이지를 불러와 여러분의 페이지 안 박스에 보여줍니다. 자신의 HTML, CSS, JavaScript는 여러분의 페이지와 벽으로 분리되어 있습니다. 설정은 src를 비롯한 속성들을 통해 전적으로 이루어집니다. 동영상, 지도, 위젯 임베딩
좋은 소식은, 임베드 코드를 직접 작성할 일은 거의 없다는 것입니다. 임베드하려는 사이트가 코드를 대신 써줍니다. 유튜브에서는 공유 버튼을 누르고 임베드를 선택하면 복사해서 쓸 수 있는 <iframe> 코드를 바로 건네줍니다. 지도, 음악 플레이어, 캘린더도 모두 같은 방식으로 동작합니다. 해당 코드 블록을 복사해서 여러분의 HTML에 붙여넣으면 됩니다.
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" width="560" height="315"></iframe>width와 height는 프레임의 크기를 픽셀 단위로 지정합니다. 이 두 숫자를 바꾸면 페이지에서 동영상이 커지거나 작아집니다.
대부분의 임베드 코드는 출처 사이트에서 그대로 가져옵니다. 공유나 임베드 버튼을 찾으면, 해당 서비스가 크기와 주소가 이미 맞춰진 <iframe> 블록을 건네줍니다. 그냥 붙여넣기만 하지 말고 이해해 둘 만한 속성 세 가지가 있습니다.
<iframe
src="https://www.youtube.com/embed/aqz-KE-bpKQ"
width="560"
height="315"
title="Big Buck Bunny short film"
loading="lazy"
></iframe>width와 height는 프레임의 크기를 픽셀 단위로 지정합니다. title 속성은 스크린 리더를 위해 프레임의 이름을 붙여줍니다. 스크린 리더는 링크의 텍스트를 읽어주는 것과 같은 방식으로 이를 읽어주는데, title이 없는 프레임은 이름 없는 "프레임"이라고만 읽혀서 아무에게도 도움이 되지 않습니다. loading="lazy"는 사용자가 프레임 근처까지 스크롤하기 전까지 프레임 안의 페이지를 다운로드하지 말라고 브라우저에 지시하며, 이렇게 하면 페이지 아래쪽에 있는 임베드가 초기 로딩을 느리게 만들지 않습니다.
고정된 픽셀 크기 대신 컨테이너에 맞춰 늘어나는 프레임을 원한다면, width와 height 속성을 빼고 CSS로 크기를 지정하되, 동영상의 비율을 올바르게 유지하려면 aspect-ratio를 사용하세요.
서비스가 건네주는 임베드 스니펫은 그럭저럭 괜찮은 기본값일 뿐, 완결된 결정은 아닙니다. 몇몇 속성이 실제 운영 환경에서의 동작을 결정합니다.
<iframe
src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
title="Big Buck Bunny short film"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
style="width: 100%; aspect-ratio: 16 / 9; border: 0;"
></iframe>실무에서 title은 선택 사항이 아닙니다. 보조 기술(스크린 리더처럼 페이지를 소리로 읽어주는 소프트웨어)은 프레임을 랜드마크로 취급하고 그 title을 읽어주므로, title이 없으면 읽는 순서에서 빈 영역이 남습니다. loading="lazy"는 프레임이 뷰포트(페이지에서 보이는 영역)에 가까워질 때까지 문서 로드를 미루는데, 프레임 하나하나가 단일 리소스가 아니라 페이지 전체를 로드하는 작업이라는 점에서 이는 특히 중요합니다. **referrerpolicy**는 임베드된 서비스에 Referer 헤더(요청이 어디서 왔는지 서버에게 알려주기 위해 브라우저가 붙이는 필드)를 통해 여러분의 페이지 URL을 얼마나 전달할지 제어하며, 이를 강화하면 사용자에 대한 정보가 더 적게 노출됩니다. 고정 픽셀보다는 aspect-ratio로 반응형 크기를 지정해서 작은 화면에서도 프레임이 자연스럽게 재배치되도록 하세요. 꼭 지켜야 할 습관 하나는, 스니펫을 붙여넣기 전에 읽어보는 것입니다. 어차피 제공자가 선택한 기본값들을 그대로 물려받게 되니까요.
<iframe>을 건네줍니다. width와 height를 바꿔서 크기를 조절하면 됩니다. 이게 거의 전부예요. title을 추가하고, 첫 렌더링을 느리게 만들지 않도록 페이지 아래쪽 프레임에는 loading="lazy"를 넣으세요. 유연한 크기 조정이 필요하면 픽셀 단위의 width, height를 빼고 CSS aspect-ratio를 쓰세요. title, 프레임이 개별 리소스가 아니라 페이지 전체 로드라는 점 때문에 필요한 loading="lazy", 사용자 정보 노출을 줄여주는 촘촘한 referrerpolicy, 그리고 반응형 크기를 위한 aspect-ratio까지 챙기세요. 붙여넣는 내용을 읽어보세요. 제공자가 고른 모든 기본값을 그대로 물려받게 되니까요. 샌드박스와 권한
프레임이 완전히 다른 페이지를 실행하는 것이기 때문에, 그 페이지가 할 수 있는 일을 제한하고 싶을 때가 있습니다. sandbox 속성이 바로 그 역할을 합니다. 프레임을 안전 울타리 뒤에 두는 것이죠.
<iframe src="widget.html" sandbox></iframe>sandbox만 단독으로 적용하면 프레임 안의 페이지는 가장 엄격한 설정에 묶입니다. 자신만의 스크립트를 실행할 수도, 폼을 제출할 수도, 새 창을 팝업으로 열 수도 없습니다. 그런 다음 임베드가 실제로 필요한 권한만 하나씩 다시 되돌려주는 식으로 풀어줍니다. 방을 빌려주긴 하지만 열리지 않았으면 하는 캐비닛은 잠가두는 것과 비슷합니다.
프레임이 할 수 있는 일을 통제하는 속성은 두 가지입니다. sandbox는 프레임을 제한하고, allow는 장치 및 브라우저 기능에 대한 접근을 허용합니다.
값 없이 sandbox만 추가하면 프레임 안 페이지가 할 수 있는 거의 모든 일—스크립트, 폼, 팝업, 자신을 동일 출처로 취급하는 것까지—이 막힙니다. 공백으로 구분된 권한 토큰을 나열해서 이를 풀어줄 수 있습니다.
<iframe
src="https://example.com/widget"
sandbox="allow-scripts allow-forms"
></iframe>allow-scripts는 프레임이 JavaScript를 실행하도록 허용하고, allow-forms는 폼 제출을, allow-popups는 새 창 열기를 허용합니다. 임베드에 실제로 필요한 것만 허용하세요. 별도의 allow 속성은 전체 화면, 카메라, 마이크처럼 민감한 브라우저 기능을 통제합니다.
<iframe src="https://example.com/call" allow="camera; microphone; fullscreen"></iframe>어떤 기능이 allow에 나열되어 있지 않다면, 프레임 안의 코드가 요청하더라도 그 기능을 사용할 수 없습니다.
여기에는 서로 다른 두 모델이 존재하며, 둘을 구분해 두는 것이 중요합니다. sandbox는 프레임에서 기능을 제거하고, allow는 제한된 기능에 대한 접근을 부여합니다. 같은 것을 표기하는 두 가지 방식이 아닙니다.
값 없는 sandbox는 모든 제약을 한꺼번에 적용합니다. 스크립트 불가, 폼 제출 불가, 팝업 불가, 미디어 자동 재생 불가에다, 중요하게도 프레임은 고유한 **불명확한 출처(opaque origin)**로 강제됩니다(다른 어떤 것과도 일치하지 않는 출처로, 이 때문에 프레임은 로드된 사이트를 포함해 누구와도 동일 출처라고 주장할 수 없습니다). 토큰을 추가해서 기능을 다시 되돌려줄 수 있습니다.
<iframe
src="https://example.com/widget"
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
></iframe>피해야 할 조합이 하나 있습니다. 여러분 자신의 출처에서 로드되는 프레임에 allow-scripts와 allow-same-origin을 함께 부여하면, 프레임 안의 페이지가 자신의 sandbox를 스스로 제거할 수 있는 코드를 실행할 수 있게 되어 sandbox를 두는 의미가 사라집니다. 이 둘을 함께 사용하는 것은 프레임 콘텐츠를 완전히 신뢰할 때만 하세요.
allow 속성은 Permissions Policy(카메라, 마이크, 위치 정보, 전체 화면 같은 민감한 기능을 어떤 페이지가 사용할 수 있는지 결정하는 브라우저 메커니즘)입니다. 기본값은 거부입니다. allow에 이름이 없는 기능은 프레임의 스크립트가 무엇을 요청하든 사용할 수 없습니다. 이런 방식으로, 카메라가 필요한 영상 통화 위젯을 배치하면서도 페이지의 다른 모든 프레임에까지 그 카메라를 넘겨주지 않을 수 있습니다.
sandbox를 추가하면 임베드된 페이지를 잠가버립니다. 스크립트도, 팝업도, 못된 짓도 없어지죠. 그런 다음 실제로 필요한 권한만 다시 되돌려주면 됩니다. 방을 빌려주되 일부 캐비닛은 잠가두는 것과 비슷합니다. sandbox는 프레임을 제한하고, allow-scripts나 allow-forms 같은 토큰으로 하나씩 풀어줍니다. 카메라, 마이크, 전체 화면을 허용하는 건 별도의 allow 속성입니다. 각각에는 임베드가 실제로 요구하는 것만 주세요. sandbox는 기능을 빼앗고, allow는 제한된 기능을 부여하며 기본값은 거부입니다. 함정은 동일 출처 프레임에 allow-scripts와 allow-same-origin을 함께 주는 것인데, 이렇게 하면 페이지가 자신의 sandbox를 스스로 해제할 수 있게 됩니다. 안의 내용을 완전히 신뢰할 때만 그렇게 하세요. 보안과 성능 관련 주의사항
임베드는 편리하지만, 프레임 하나마다 페이지 전체를 하나 더 로드하는 셈이라 여러 개를 쌓아두면 페이지가 느려질 수 있습니다. 두 가지 습관이 문제를 막아줍니다. 신뢰할 수 있는 사이트에서만 임베드하세요(그 페이지를 여러분의 페이지로 초대하는 것이니까요), 그리고 필요한 것보다 더 많은 프레임을 추가하지 마세요.
앞에서 본 loading="lazy" 속성도 여기서 도움이 됩니다. 사용자가 스크롤해서 다가갈 때까지 프레임 로딩을 미뤄주죠. 동영상이나 지도처럼 실제로 수고를 덜어주는 경우에 임베드를 사용하고, 평범한 링크로도 같은 일을 할 수 있다면 임베드를 건너뛰세요.
추가하는 프레임 하나하나가 페이지 전체를 로드하는 것과 같습니다. 자신만의 HTML, 자신만의 CSS, 그리고 종종 산더미 같은 JavaScript까지 전부 다른 서버에서 가져옵니다. 서드파티 임베드가 세 개 있는 페이지는 사실상 네 개의 페이지를 동시에 로드하는 것이나 다름없어서, 임베드가 많은 페이지가 무겁게 느껴지는 이유입니다. 화면 밖 프레임에 loading="lazy"를 쓰는 것이 가장 저렴한 개선책이고, 그다음은 세 개보다 하나의 임베드를 선호하는 것입니다.
속도 말고도 신경 써야 할 문제가 하나 더 있습니다. 여러분의 페이지가 다른 누군가의 프레임 안에 들어가지 못하도록 보호할 수 있는데, 이는 공격자가 여러분의 사이트를 프레임에 넣고 사용자가 보이지 않는 무언가를 클릭하도록 속이는 수법을 막아줍니다. 이 보호 조치는 서버에서 설정하며, Deep Dive 레벨에서 이를 담당하는 두 가지 헤더를 다룹니다.
여기서는 뚜렷하게 짚어볼 만한 두 가지 비용이 있습니다. 프레임이 성능에 미치는 영향과, 프레이밍이 보안에 미치는 영향입니다.
성능 측면에서, 서드파티 프레임은 단순한 요청 하나가 아니라 제공자의 전체 스택(마크업, 스타일, 폰트, 애널리틱스, 스크립트)을 여러분의 페이지 안에서 통째로 실행하는 **중첩 문서 로드**입니다. 같은 네트워크와 메인 스레드를 두고 경쟁하며, 그 스크립트는 자신만의 컨텍스트 안에서 실행되긴 하지만 같은 기기 위에서 돌아가기 때문에, 무거운 임베드는 여러분 자신의 콘텐츠가 상호작용 가능해지는 시점을 늦출 수 있습니다. loading="lazy"는 화면 밖 프레임의 로딩을 미뤄주며, 임베드 하나를 세밀하게 튜닝하는 것보다 임베드 자체를 통합하는 편이 더 낫습니다. 임베드가 공짜라고 가정하기 전에 실제 비용을 측정해 보세요.
보안 측면의 위험은 **클릭재킹(clickjacking)**입니다. 공격자가 자신의 사이트에서 프레임으로 여러분의 페이지를 불러온 뒤 보이지 않게 만들거나 다른 요소로 덮어씌워서, 로그인한 사용자를 유인해 여러분의 페이지에 있는 실제 컨트롤을 자신도 모르게 클릭하게 만드는 것입니다. 이를 방어하려면 누가(혹은 아무도) 여러분의 페이지를 프레임에 넣을 수 있는지 선언해야 하며, 이는 HTML이 아니라 서버의 응답 헤더로 설정합니다.
X-Frame-Options: DENY는 여러분의 페이지가 어떤 프레임에도 로드되지 못하게 완전히 거부합니다.SAMEORIGIN은 여러분 자신의 사이트만 프레임에 넣도록 허용합니다. 이는 더 오래되고 더 거친 방식의 제어입니다.Content-Security-Policy: frame-ancestors 'self'는 현대적인 대체 방식입니다. **frame-ancestors**는 여러분의 페이지를 프레임에 넣을 수 있는 출처를 정확히 나열합니다(Content-Security-Policy, 즉 CSP는 브라우저에게 어떤 콘텐츠와 동작의 출처를 허용할지 알려주는 헤더입니다). 이는X-Frame-Options를 대체하며, 하나의 일괄적인 규칙이 아니라 구체적인 허용 목록을 지정할 수 있고, 로그인이나 폼이 있는 모든 것에 설정해야 할 항목입니다.
방향을 기억해 두세요. sandbox와 allow는 여러분이 임베드하는 페이지를 통제하는 것이고, frame-ancestors와 X-Frame-Options는 누가 여러분을 임베드할 수 있는지를 통제하는 것입니다. 두 방향 모두 중요하며, 설정하는 위치도 서로 다릅니다.
loading="lazy"를 활용하세요. 평범한 링크로 같은 일을 할 수 있다면 링크를 쓰세요. loading="lazy"를 쓰는 것이 저렴한 개선책이고, 임베드 하나를 튜닝하는 것보다 개수를 줄이는 편이 낫습니다. 주의해야 할 프레이밍 위험도 있는데, Deep Dive 레벨에서 서버 헤더 두 가지로 이를 다룹니다. frame-ancestors가 현대적인 제어 방식이고, X-Frame-Options는 예전 방식입니다. 방향을 기억하세요. sandbox는 여러분이 임베드하는 페이지를 지키고, frame-ancestors는 누가 여러분을 임베드하는지를 지킵니다. 
