Document Picture in Picture API는 아직 실험적 기능입니다. ( 2025-07 )
일부 브라우저에서 동작하지 않을 수 있습니다.
CanIUse 에서 브라우저별 지원 현황을 확인할 수 있습니다.
Secure Context 에서만 사용할 수 있습니다.
BitPage - Document PIP 를 먼저 읽으시면 내용을 이해하는 데 도움이 됩니다.
BitPage - vue pip에서도 확인할 수 있습니다.
vue-pip를 이용하면, 구현된 컴포넌트를 쉽게 사용할 수 있습니다.
Document PIP - HTML에서는 DOM 요소를 직접 선택하여 PIP 창으로 이동시켰습니다.
const element = document.getElementById("my-element");
pipWindow.document.body.appendChild(element);
하지만 Vue의 반응성이나 이벤트를 활용하기 위해서는 다른 방법을 사용해야 합니다.
PIP 창에 독립적인 Vue 앱을 마운트 할 수 있습니다.
다음과 같은 모습이 될 것 같습니다.
export const useDocumentPIP = (
width: number,
height: number,
component: Component
) => {
//...
const openPIPWindow = async () => {
const pip = await window.documentPictureInPicture.requestWindow({
width,
height,
});
const appDiv = pip.document.createElement('div');
appDiv.id = 'pip-app';
pip.document.body.appendChild(appDiv);
const pipApp = createApp(component);
pipApp.mount(pip.document.getElementById("pip-app")!);
};
return {
openPIPWindow,
};
};
이 방법으로도 어떤 컴포넌트에서는 충분할 것 같습니다.
하지만 외부 상태에 영향을 받는 컴포넌트라면 다음 방법을 사용하는 것이 나을 것 같습니다.
Teleport를 이용하면 동일한 인스턴스에서 컴포넌트를 렌더링 할 수 있습니다.
동일한 인스턴스에 존재하기 때문에 Store에도 간단히 접근할 수 있고,
다음과 같이 간단하게 props를 전달하고 event를 청취할 수 있습니다.
<template>
<MyComponent @someEvent="handleSomeEvent" :someProp="someProp" />
<Teleport :to="pipTarget">
<MyComponent @someEvent="handleSomeEvent" :someProp="someProp" />
</Teleport>
</template>
재사용성을 위해 DocumentPip 컴포넌트를 만들고, slot을 활용해 PIP 창에 렌더링하겠습니다.
이렇게 사용하는 것이 목표입니다.
<template>
<DocumentPip :isPipOpen="isPipOpen" :size="{ width: 500, height: 200 }">
<PipContent />
</DocumentPip>
</template>
먼저 PIP 창을 열고 컴포넌트가 Teleport될 요소를 준비해야 합니다.
<script setup lang="ts">
const openPIPWindow = async () => {
const pip = await window.documentPictureInPicture.requestWindow({
width: 500,
height: 200,
});
const rootDiv = pip.document.createElement("div");
rootDiv.id = "pip-root";
pip.document.body.appendChild(rootDiv);
const pipRoot = pip.document.getElementById("pip-root"); // Teleport Target
};
</script>
우리는 이 pip 창이 열렸을 때 다음과 같이 컴포넌트를 조건부로 렌더링해야 합니다.
<template>
<slot v-if="!pipRoot"></slot>
<Teleport v-else :to="pipRoot">
<slot></slot>
</Teleport>
</template>
pipRoot가 변경되었을 때 컴포넌트가 다시 렌더링되어야 합니다.
다음과 같이 수정하겠습니다.
<script setup lang="ts">
const pipWindow = ref<Window | null>(null);
const pipRoot = computed(() => {
return pipWindow.value?.document.getElementById("pip-root") || null;
});
const openPIPWindow = async () => {
const pip = await window.documentPictureInPicture.requestWindow({
width: 500,
height: 200,
});
const rootDiv = pip.document.createElement("div");
rootDiv.id = "pip-root";
pip.document.body.appendChild(rootDiv);
pipWindow.value = pip;
};
</script>
이제 props의 변화를 감지하고, 그 값에 따라 PIP 창을 열고 닫습니다.
<script setup lang="ts">
// ...
watch(
() => props.isPipOpen,
(newVal: boolean) => {
togglePictureInPicture(newVal);
}
);
const togglePictureInPicture = (isPipOpen: boolean) => {
if (isPipOpen) {
openPIPWindow();
} else {
closePIPWindow();
}
};
const openPIPWindow = async () => {
// ...
};
const closePIPWindow = () => {
pipWindow.value.close();
pipWindow.value = null;
};
</script>
전체 코드는 BitPage에서 확인할 수 있습니다.