← 블로그 목록
iframesandboxCSP웹 보안웹 게임

웹 게임·데모를 iframe으로 임베드할 때 막히는 것들: sandbox와 CSP 정리

iframe 안에서만 화면이 비거나 저장이 안 되는 이유를 sandbox 토큰, opaque origin, frame-ancestors 헤더로 나눠 설명하고 해결 방법을 정리했습니다.

Vibeollio 팀-

링크는 되는데 iframe에서만 안 될 때

만든 웹 게임이나 데모를 다른 페이지에 끼워 넣으려다 막히는 경우가 많습니다. 새 탭에서 열면 멀쩡한데 <iframe> 안에서는 화면이 비거나, 저장 기능이 동작하지 않거나, 콘솔에 낯선 보안 에러가 뜹니다.

원인은 대부분 셋 중 하나입니다. 넣는 쪽이 건 sandbox 제약, 넣기는 쪽이 건 응답 헤더, 그리고 origin이 사라지면서 깨지는 저장소 API입니다. 하나씩 정리합니다.

sandbox는 화이트리스트다

sandbox 속성을 붙이는 순간 iframe 안의 문서는 거의 모든 권한을 잃습니다. 그리고 잃은 권한은 토큰으로 하나씩 되돌려 받는 구조입니다.

<iframe src="/games/plinko/" sandbox="allow-scripts"></iframe>

위처럼 쓰면 스크립트만 돌아가고 폼 제출, 팝업, 최상위 이동은 전부 막힙니다. 자주 쓰는 토큰은 다음과 같습니다.

  • allow-scripts — 스크립트 실행. 게임이라면 사실상 필수
  • allow-same-origin — 원래의 출처를 유지
  • allow-forms — 폼 제출
  • allow-popupswindow.open
  • allow-pointer-lock — 마우스 포인터 잠금. 1인칭·드래그 조작 게임에 필요
  • allow-top-navigation-by-user-activation — 사용자 클릭에 한해 최상위 페이지 이동 허용

sandbox 속성 자체를 아예 쓰지 않으면 제약이 걸리지 않습니다. 즉 값을 비워 sandbox=""로 두는 것이 가장 강한 제약이고, 속성을 생략하는 것이 가장 느슨한 상태입니다. 이 둘을 헷갈리면 정반대의 결과가 나옵니다.

localStorage가 실패하는 진짜 이유

가장 흔하게 걸리는 함정입니다. sandbox="allow-scripts"만 걸면 게임은 뜨는데 최고 점수가 저장되지 않습니다.

allow-same-origin이 없으면 문서는 불투명한 출처(opaque origin)를 갖게 됩니다. 출처가 없는 문서로 취급되므로 출처에 묶인 API가 전부 막힙니다. localStoragesessionStorage 접근은 예외를 던지고, 쿠키도 IndexedDB도 쓸 수 없습니다.

// opaque origin 에서는 접근 자체가 예외를 던진다
try {
  localStorage.setItem('highScore', String(score));
} catch {
  // 저장 불가 환경 — 메모리에만 유지하고 조용히 넘어간다
}

임베드될 가능성이 있는 페이지라면 저장소 접근은 항상 try/catch로 감싸는 편이 안전합니다. 저장이 안 되는 것과 게임 전체가 죽는 것은 다른 문제입니다.

allow-scripts와 allow-same-origin을 같이 주면

두 토큰을 함께 부여하면 iframe 안의 스크립트가 자기 출처를 그대로 가진 채 실행됩니다. 이 경우 문서가 자신의 sandbox 속성을 제거하는 것이 가능해져, 샌드박스가 사실상 무력화됩니다.

그래서 이 조합은 안에 들어가는 콘텐츠를 신뢰할 수 있을 때만 써야 합니다. 자기가 만든 콘텐츠를 자기 도메인에서 서빙하는 경우가 여기 해당합니다. 반대로 사용자가 올린 파일을 임베드한다면, 아예 별도 도메인에서 서빙해 출처를 분리하는 방식이 정석입니다. 같은 도메인에 두고 sandbox로만 막으려는 시도는 대체로 뚫립니다.

넣기는 쪽에서 막는 경우

내 페이지 설정은 다 맞는데도 화면이 비어 있고, 콘솔에 "프레임에 표시할 수 없다"는 취지의 에러가 뜬다면 이번엔 대상 사이트가 거부한 것입니다.

두 가지 방법이 쓰입니다. 하나는 오래된 X-Frame-Options 헤더로, DENY 또는 SAMEORIGIN 값을 응답에 실어 임베드를 차단합니다. 다른 하나는 CSP의 frame-ancestors 지시자입니다.

Content-Security-Policy: frame-ancestors 'self' https://example.com;

이건 상대 서버가 정한 정책이라 임베드하는 쪽에서 우회할 방법이 없습니다. 자기 사이트를 임베드 가능하게 만들고 싶다면 반대로 자기 응답 헤더에서 이 설정을 풀거나 허용 목록에 상대 도메인을 넣어야 합니다.

임베드를 염두에 둔 페이지 만들기

임베드될 것을 알고 만들면 몇 가지가 달라집니다.

  • 고정 픽셀 크기 대신 부모 크기에 맞추기. 100% 기준으로 캔버스를 잡고 리사이즈에 대응합니다.
  • 저장소 접근은 전부 실패 가능한 것으로 취급하기.
  • 새 탭에서 전체화면으로 여는 링크를 함께 제공하기. 좁은 프레임 안에서는 조작이 불편한 경우가 많습니다.
  • 외부로 나가는 링크에는 target="_blank"rel="noopener"를 붙이기.

sandbox 없이 그냥 쓰면 안 되나요?

자기가 만들고 자기가 서빙하는 콘텐츠라면 실용적으로는 문제가 없습니다. 다만 속성을 명시해두면 나중에 그 페이지에 외부 스크립트가 들어왔을 때 피해 범위가 제한됩니다. 습관적으로 최소 권한부터 시작하고 필요한 토큰만 더하는 편을 권합니다.

반응형 iframe은 어떻게 만드나요?

부모에서 종횡비를 고정하는 방식이 가장 간단합니다. 컨테이너에 aspect-ratio를 주고 iframe을 width: 100%; height: 100%로 채우면 별도 스크립트 없이 비율이 유지됩니다. 게임처럼 세로·가로 레이아웃이 크게 다른 경우에는 화면 폭에 따라 종횡비 자체를 바꿔주면 됩니다.

부모 페이지와 데이터를 주고받으려면?

postMessage를 씁니다. 다만 받는 쪽에서 event.origin을 반드시 확인해야 합니다. 확인 없이 받은 메시지를 그대로 신뢰하면, 임베드된 문서를 통해 부모 페이지가 조작될 수 있습니다. 점수 전송처럼 단순한 용도라도 출처 검사는 생략하지 마세요.

직접 확인해보는 게 빠르다

이 주제는 문서만 읽어서는 감이 잘 안 옵니다. 토큰을 하나씩 빼면서 무엇이 깨지는지 보는 게 가장 빠릅니다.

만든 게임이나 데모가 있다면 Vibeollio에 프로젝트 등록해보세요. 브라우저에서 바로 실행되는 프로젝트는 방문자가 설치나 회원가입 없이 그 자리에서 체험할 수 있어, 임베드 설정이 제대로 됐는지도 함께 확인하게 됩니다. 등록된 다른 프로젝트들이 어떤 방식으로 실행되는지 살펴보는 것도 참고가 됩니다.

웹 게임·데모를 iframe으로 임베드할 때 막히는 것들: sandbox와 CSP 정리 | Vibeollio