Document Picture-in-Picture API를 사용한 웹 위젯 생성
요약
Firefox 151에서 Document Picture-in-Picture API가 출시되어 웹 위젯 생성이 가능해졌습니다. 이 API는 일반적인 PiP와 달리 HTML, CSS, JavaScript를 포함한 모든 콘텐츠를 플로팅 창에 담을 수 있게 합니다. 개발자는 JS를 사용하여 브라우저 지원 여부를 확인하고 DPIP 창을 생성하는 방법을 학습할 수 있습니다.
핵심 포인트
- Document Picture-in-Picture API는 웹 위젯 제작에 활용 가능합니다.
- HTML, CSS, JavaScript 등 모든 요소를 플로팅 창에 담을 수 있습니다.
- 브라우저 지원 여부는 JS의 `documentPictureInPicture` 속성으로 확인해야 합니다.
- 데스크톱 환경 전용 API이며, Safari 지원은 아직 미흡합니다.
Firefox 151에서 최근 Document Picture-in-Picture API가 출시되었습니다. 이것은 브라우저 탭이나 OS 창을 전환한 후에도 보이는 크기 조절 가능한 창으로 비디오를 밀어내는 (일반적인?) Picture-in-Picture API와는 다릅니다. 아니요, Document Picture-in-Picture API는 우리가 모든 것을 그 창에 넣을 수 있게 해줍니다.
이러한 창들을 웹 위젯이라고 생각할 수 있습니다. 플로팅 주식 시세표, 실시간 채팅 대화, 재생 목록, 할 일 목록, 메모, 스프레드시트 등 항상 화면에 유지하고 싶은 모든 것에 사용할 수 있습니다.
일반적인 아이디어는 Document Picture-in-Picture 창(DPIP window)을 생성한 다음, 그 안에 HTML, CSS, JavaScript를 넣는 것입니다. 생각해보면 꽤 간단하지만, Document Picture-in-Picture API가 어떻게 작동하는지 탐구하면서 여러분이 아마 마주칠 약간 더 복잡한 시나리오를 다룰 것입니다.
우리는 메인 문서의 주식 시세표를 DPIP 창 안으로 클론할 것입니다. 이것은 관련 미디어 쿼리(media queries) 및 가상 클래스(pseudo-classes)에 대해 이야기할 기회를 제공할 뿐만 아니라, HTML 컴포넌트를 컨텍스트에서 분리하는 것이 CSS를 손상시킬 수 있다는 명확한 상기시켜주므로 이 점을 염두에 두어야 합니다.
이것이 주식 시세표입니다:
하지만 작동하려면 데모를 디버그 모드로 열어야 합니다. 이는 picture-in-picture가 CodePen <iframe>과 같은 중첩된 브라우징 컨텍스트(nested browsing contexts)에서는 작동하지 않기 때문입니다.
게다가 Safari는 아직 DPIP API를 지원하지 않으므로 Chrome 또는 Firefox를 사용하고 있는지 확인하십시오.
시작할 준비가 되셨나요?
모든 것의 JavaScript
먼저 브라우저가 Document Picture-in-Picture API를 지원하는지 확인해야 합니다. 저는 이것이 있으면 좋은 기능이라고 생각하기 때문에, 왜 Safari 지원을 기다리나요? 불행하게도, feature query (@supports)를 사용하여 @media (display-mode: picture-in-picture)가 지원되는지 여부를 쿼리할 방법이 없습니다. 왜냐하면 at-rule()
function은 Chrome에서만 지원되며, 프리랩(preludes), 즉 (display-mode: picture-in-picture)를 지원하려는 계획은 어쨌든 폐기된 것으로 보입니다.
이것을 할 수 있다면 정말 좋을 텐데요:
@supports at-rule(@media; display-mode: picture-in-picture) {
/* DPIP 지원 */
}
참고: Safari Technology Preview 251 릴리스 노트에는 @supports에서 at-rule 감지 지원이 언급되어 있지만, 언제 배포될지는 불분명합니다. 그리고 Firefox 155는 이 글이 게시된 지 하루 만에 이를 지원한다고 발표했습니다.
대신 우리는 JavaScript를 사용하여 브라우저 지원을 확인해야 합니다. DPIP가 지원되지 않으면 버튼을 제거하거나, 지원되면 DPIP 창을 생성하도록 만들 수 있습니다):
if (!("documentPictureInPicture" in window)) {
/* DPIP 미지원 (버튼 제거) */
document.querySelector("button").remove();
...
문서 Picture-in-Picture API는 데스크톱 전용 API라는 점을 염두에 두십시오. 따라서 위의 확인 코드는 이 점도 고려하며, 완전한 기능의 at-rule() function이 얼마나 유용할지 정확히 보여줍니다.
DPIP 창 생성과 관련하여, 우리가 먼저 처리하고 싶을 한 가지가 있습니다. 즉, 이미 존재하는 DPIP 창을 처리하는 것입니다. DPIP 창은 기존 DPIP 창을 대체하므로 그 부분에 대해서는 걱정할 필요가 없지만, 버튼이 두 번째로 클릭되었을 때 무슨 일이 일어날지 결정해야 합니다. 아래 코드는 DPIP 창이 이미 열려 있다면 닫아버리므로, 효과적으로 버튼을 토글(toggle) 버튼으로 만듭니다:
document.querySelector("button").addEventListener("click", async () => {
/* DPIP 창이 열려 있으면 닫기 */
if (window.documentPictureInPicture.window) {
...
문제는 포커스가 항상 DPIP 창으로 전환된다는 점입니다. 따라서 DPIP 창을 끄려면 두 번의 버튼 클릭이 필요할 수 있습니다. 이에 대한 한 가지 해결책은 버튼을 DPIP 창 내부로 복제하는 것이지만, DPIP 창에는 이미 “닫기(Close)” 아이콘 버튼이 있으므로 그럴 필요가 없습니다. 개인적으로는 아무것도 하지 않고, 이후의 버튼 클릭으로 DPIP 창을 다시 생성하도록 두는 것이 좋다고 생각합니다. 실제로 사용자가 DPIP 창을 이동하거나 크기를 조정하더라도, 이후의 버튼 클릭은 이를 원래 위치와 크기(적절한 옵션과 함께)로 재설정해 줍니다.
이러한 맥락에서, DPIP 창 생성 및 관련 옵션에 대해 이야기해 보겠습니다. width와 height 옵션이 어떤 역할을 하는지는 매우 명확하지만, 둘 중 하나만 설정할 수는 없으며, 둘 다 설정하지 않으면 브라우저가 결정합니다. preferInitialWindowPlacement 옵션을 true로 설정하면 브라우저가 DPIP 창의 위치와 크기를 저장하는 것을 방지합니다. disallowReturnToOpener 옵션(여기서는 사용되지 않음)을 true로 설정하면 “탭으로 돌아가기(Back to tab)” 아이콘 버튼이 숨겨집니다 (이는 “닫기” 아이콘 버튼과 동일한 기능을 수행하지만, 사용자에게 원래 탭으로도 되돌려 보냅니다).
/* DPIP 창 생성 */
const DPIP = await window.documentPictureInPicture.requestWindow({
width: 600,
...
DocumentPictureInPicture 인터페이스의 requestWindow() 메서드는 Promise를 반환합니다 (그래서 우리가 async와 await를 사용하는 것입니다). 이는 창이 준비되는 동안 다른 모든 작업을 처리할 수 있다는 의미입니다.
HTML을 DPIP 창 내부로 복제하는 방법은 다음과 같습니다:
/* 컴포넌트 선택 */
const stock = document.querySelector("#stock");
/* 컴포넌트를 클론하여 DPIP의 <body>에 추가 */
...
하지만 여러 요소를 복제하려면 다른 접근 방식이 필요합니다. 이것이 우리가 할 일입니다. 모든 <style>과 <link rel=stylesheet> (필요하다면 <script>도, 또는 DPIP 창이 요구하는 모든 리소스)을 클론하기 때문입니다.
비교적 간단합니다. querySelectorAll()을 사용하면 됩니다.
그리고 NodeList 객체 배열을 생성하고, 임의의 DOM 트리를 만들기 위해 createDocumentFragment()를 사용합니다.
forEach()를 사용하여 이 배열을 반복하면서 각 노드를 해당 오프스크린(off-screen) 문서 조각(document fragment)으로 복제합니다. 마지막으로, 전체 문서 조각을 DPIP 창의 <head>에 추가하면 여러 번 발생하는 리플로우 대신 단 한 번만 발생하므로 성능이 더 좋습니다.
그리고 기억하세요, 모든 것을 복제하는 것은 아마 필요하지 않을 수 있으니 필요에 따라 조정하세요.
/* 모든 <style>과 <link rel=stylesheet>를 선택 */
const styles = document.querySelectorAll("style, [rel=stylesheet]");
/* 문서 조각 생성 */
...
데모에서 가져온 전체 JavaScript 스니펫이며, (적어도 오류 처리를 추가하는 등) 확장하고 싶을 것입니다:
if (!("documentPictureInPicture" in window)) {
/* DPIP를 지원하지 않음 (버튼 제거) */
document.querySelector("button").remove();
...
CSS 처리하기
기억하세요, HTML을 문맥에서 분리(CSS와 함께)하여 DPIP 창에 넣는 경우, CSS 선택자가 너무 구체적이지 않고 두 컨텍스트 모두를 위해 작성되었는지 확인해야 합니다.
그렇긴 하지만, 둘 중 하나의 창만을 위해 특정 CSS를 작성하고 싶을 수도 있고, 바로 여기에 display-mode 미디어 쿼리가 사용됩니다. 매우 자명하므로, 컨테이너를 조정하기 위해 데모에서 사용하는 방법을 알려드리겠습니다:
#stock {
width: fit-content;
border-radius: 0.7rem;
...
또한 :picture-in-picture 가상 클래스는 일반 Picture-in-Picture API용이며, Document Picture-in-Picture API가 아닙니다.
마무리하기
DPIP 창이 열릴 때 발생하는 enter 이벤트는 (일반적인 Picture-in-Picture의 enterpictureinpicture 이벤트와 혼동하지 마세요) 사용처를 생각할 수 없었습니다:
documentPictureInPicture.addEventListener("enter", (event) => {
/* DPIP 창 열림 */
});
그 외에는, 이것으로 Document Picture-in-Picture API에 대한 이야기는 끝난 것 같습니다. 아주 크거나 복잡한 API는 아니지만, 정말 유용할 것 같지 않나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 CSS-Tricks의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기