코딩해듀오 서비스는 개발과 동시에 사용해야 해서 중간마다 시간을 확인하기 번거롭다는 피드백이 있었다. 기존의 코딩해듀오 서비스에서는 이러한 번거로움을 해결하기 위해 다음과 같이 title 에 남은 타이머 시간을 넣어 확인할 수 있도록 구현했다.
이렇게 어느정도는 해결할 수 있었지만 만약 사용자의 탭이 많이 열려있다면 탭의 크기가 작아져 확인할 수 없다는 단점이 있었다. 또한 눈에 잘 띄지 않는다는 단점도 있었다.
그러다 한 팀원이 뽀모도로 앱 타이머 pip를 보여주었고 이 기능이 고민하던 불편함을 해결할 수 있겠다는 생각이 들어 구현하게 되었다.
Picture-in-PicturePIP는 가장 위에 떠 있는 비디오 윈도우를 만들 수 있는 기능이다.
예를 들어 유튜브를 보다가 다른 탭으로 이동했을 때 작은 유튜브 화면이 떠 있는 것을 볼 수 있을 것 이다. 이 창은 다른 탭에 가더라도 항상 가장 위에 떠 있다.
Picture-in-Picture API는 <video>
를 위한 기능이며 Firefox를 포함한 일부 브라우저에서는 호환되지 않는다.
[Picture-in-Picture 브라우저 호환성]
코딩해듀오에서는 비디오가 아닌 타이머를 pip로 보여주어야 한다. 물론 비디오 위에 canvas api로 그려넣을 순 있지만 * 러닝커브가 높아 이 api 를 확장한 Document Picture-in-Picture API를 활용했다. Document Picture-in-Picture API는 pip 윈도우에 임의의 html 콘텐츠를 넣을 수 있게 한다.
[Document Picture-in-Picture 브라우저 호환성]
모바일에서 pip기능을 사용할 이유가 없다고 판단했고 pip가 없더라도 메인 기능에는 영향을 주지 않기 때문에 브라우저 호환성을 크게 고려하지 않았다.
* canvas api로 실제 구현을 해 봤지만 다음과 같은 문제가 있었다.
- 각 요소마다 위치, 크기, 색 등을 각각 설정해 주어야 하고 해상도도 설정해 주어야 했다.
크롬 개발자도구
로 확인해 볼 수 없어서 번거로웠고 내가 원하는대로 잘 나오지 않았다.- 마찬가지로 타이머 시작, 중단 버튼과 인터랙션을 넣으려면 클릭 이벤트 시 버튼의 영역을 계산해야 했다.
- pip 창을 가장 작은 크기로 줄였는데 내 눈에는 커 보여서 안 예뻤다..
- canvas로 타이머를 그린 다음에 비디오 스트림을 켜서 그 위에 올리고 시간이 지남에 따른 렌더링과 화면 전환 등을 고려하니 상태가 복잡해졌다.
⬇️ video+canvas로 구현한 타이머
canvas에 대해 제대로 공부해보고 다시 구현해 봐야겠다
| 공식 레퍼런스 에서 구현하는 방법이 굉장히 자세히 나와있다.
declare global {
interface Window {
documentPictureInPicture: {
requestWindow: (options?: { width?: number; height?: number }) => Promise<Window>;
};
}
}
타입스크립트에서 documentPictureInPicture의 타입을 몰라서 전역으로 윈도우에 선언해 주었다.
// PairRoom.tsx
async () => {
const pip = document.querySelector('#pip');
const pipWindow = await window.documentPictureInPicture.requestWindow(창 크기 설정);
pip && pipWindow.document.body.append(pip);
}
PairRoom 안의 버튼 클릭 시 id 가 pip 인 컴포넌트(TimerCard.tsx)를 가져와서 pip로 띄워주도록 했다.
이때 1️⃣ css 가 적용되지 않고, 2️⃣ 원래 있던 타이머가 사라졌으며, 3️⃣ 타이머 자체는 동작했다. 그리고 4️⃣ 타이머 시작/중단 버튼은 동작하지 않았다.
여기서 알 수 있는 것은 다음과 같다.
1️⃣ 새로운 pip 윈도우를 열었기 때문에 이 html 파일에는 연결되어있는 css style 문서가 없다. 만약 스타일을 적용하고 싶다면 기존의 styleSheets 를 복사해서 추가해 주거나, 직접 css 코드를 작성해서 pip윈도우에 넣어주어야 한다.
2️⃣ append()로 DOM 요소를 가져왔기 때문이다. 이것도 DOM을 복제해서(cloneNode, 단 이벤트리스너는 복제되지 않음) 넣어주면 된다.
3️⃣ React 컴포넌트에서 HTML로 변환된 DOM 요소의 텍스트만 업데이트되기 때문이다.
4️⃣ 새로운 document 에 script 가 없기때문에 js가 동작하지 않는다. (게다가 script 가 연결되어있더라도 리액트는 이벤트를 document 레벨에서 위임하여 처리하기 때문에 pip 윈도우에서는 document 가 바뀌어 기존의 리액트 이벤트 위임이 제대로 동작하지 않게 된다.)
사실 당연한 얘기들인데 내가 컴포넌트 자체를 append 했다고 착각해서 이유를 찾는데 오래 걸렸다..🥲
PIP의 UI는 기존의 타이머와 같은 상태를 바라보아야 하지만, 조금 간편한 타이머 UI를 보여주어야 한다고 생각한다. 또한 PIP의 open, close 상태와 window 상태를 담아주는 새로운 상태가 필요하다. 그래서 컴포넌트를 복사하기보다는 TimerPip 파일로 새로 분리하여 새로운 html을 만들어주었다.
css는 기존의 styled-component 를 부분적으로만 복사하면 좋을 것 같아서 시도해보았으나 ServerStyleSheet를, collectStyles로 모든 컴포넌트 구조를 넣고, class id 로 연결해주어야 한다는 번거로움이 있었다.
물론 다음과 같이
sheet.collectStyles(
<TimerCard
accessCode="example"
defaultTime={25 * 60}
defaultTimeleft={25 * 60}
onTimerStop={() => {}}
/>
컴포넌트를 전부 가져오는것도 가능하나 TimerCard의 모든 의존성까지 가져오게 되어서 IconButton과 같은 공통컴포넌트와 theme 같은 외부 의존성도 처리해줘야하고 style reset 도 따로 해줘야해서 번거로웠다.
그리고 ui 를 PIP에 맞게 변경하기 어려워서 css-in-js 로 따로 구현해서 넣어주기로 했다.
try {
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 250,
height: 180,
});
pipWindowRef.current = pipWindow;
const styleElement = pipWindow.document.createElement('style');
styleElement.textContent = styles;
// styles 파일을 분리해서 새로운 ui를 구현해주었다.
pipWindow.document.head.appendChild(styleElement);
start-pause 버튼은 이벤트 리스너를 추가해서 버튼 클릭 시 props 의 함수가 실행되도록 구현했다.
타이머 시작-중단 함수 안에 네트워크 요청이 있어서(코듀오에서는 정확한 시간과 페어룸간의 시간 동기화를 위해 서버 타이머를 사용한다) 에러 처리를 하기 어려웠다.
하지만 타이머가 시작했는지, 중단되었는지의 boolean값을 받아와 중단이나 시작에 실패하더라도 다른 상태의 버튼 UI나 함수가 실행되지는 않게 구현했다.
if (!window.document.querySelector('.layout')) {
timerElement.innerHTML = ``
VM15489:1 PiP 윈도우 생성 실패: NotAllowedError: Failed to execute 'requestWindow' on 'DocumentPictureInPicture': Document PiP requires user activation
이러한 에러메세지를 확인 할 수 있으며, document pip를 열기 위해서는 사용자 인터렉션이 필요하다는 것을 알 수 있다.
또한 w3c나 mdn 에 찾아보면 사용자 상호작용(클릭 등) 없이 PIP 창을 열려고 하면 보안 관련 에러가 발생한다고 한다.
초기에 사용자 허가를 받아서 사용자 activation 없이 열리도록 하고 싶었으나 navigator.userActivation 은 읽기 전용 값이라 내가 바꿀 수 없어서 아직 해결하지 못했다..
전체적인 구현 로직은 다음과 같다.
openPiP 함수 : pip 윈도우 생성 함수
pip window가 있거나 열려있지 않다면pipWindow를 만들어서 style과 뼈대 div 를 넣어준다.
unload는 이벤트를 붙여주어 pip를 닫았을 때 윈도우 열림 상태를 reset할 수 있도록 하고 만들어진 pip윈도우에 update 함수를 넣었다.
updatePiPContent 함수 : 윈도우에 props에 따른 ui, text 보여주도록 update 하는 함수
만들어진 윈도우에 html layout 을 붙이고 시간 text, 버튼 svg, event 를 넣어준다.
브라우저 탭의 콘텐츠가 보여지거나 숨겨질 때(탭 이동) pip 창을 열어준다. -> isPipOpenRef(pip가 열려있는지) 확인해준다.
minutes, seconds, progress, isActive가 바뀔때마다 pip를 업데이트 시켜준다.
-> pipWindowRef(pip window가 있는지) 확인해준다.
즉 창이 바뀔때 pip윈도우를 열고 초기 update, props 가 바뀔때마다 상태 업데이트 해준다.
끗
레퍼런스
PIP를 처음 들어봤는데 브라우저의 다른 탭으로 이동해도 계속 띄울수도 있군요!
하나 배워갑니다 ㅎㅎ