-
Notifications
You must be signed in to change notification settings - Fork 0
스크롤 위치에 따른 비디오 타임라인 조정
저희 프로젝트에는 사용자가 조절하는 스크롤의 위치에 따라 비디오의 타임라인이 선형적으로 조절되는 기능 요구사항이 있습니다. 이 기능을 구현하기 위해, 크게 두 가지 세부 요구사항을 도출했습니다.
- 특정 반응형 상태 값에 따라 비디오의 타임라인이 조절되어야 한다.
- 이 상태의 값은 사용자의 스크롤 위치에 따라 실시간으로 알맞게 계산되어야 한다.
처음엔 뷰포트와 요소가 겹치는 것을 감지하는, 저희 프로젝트에서도 이미 몇 번 사용되었던 IntersectionObserver
API를 사용할까 고려도 했지만, IntersectionOberver
는 사전 설정한 겹치는 비율의 임계값을 넘거나 내려가는 그 '순간' 외의 상황은 핸들링하지 못하기 때문에 이런 설계를 도입했습니다.
일단 현재 비디오 타임라인을 정하는 상태를 정의해줍시다. 단위는 초(s)이며, 저희 프로젝트에서는 페이지 첫 진입이나 새로고침 시 무조건 스크롤이 맨 위로 이동되게 했으므로 초기값은 0으로 지정해줍시다.
const [videoTimeline, setVideoTimeline] = useState(0);
그 다음 다루고 싶은 비디오 요소의 타임라인과 이 상태를 연결시켜야 합니다. 저희가 조사한 결과, 비디오의 타임라인은 <video>
태그의 속성으로 접근할 수 없고, 직접 비디오 요소를 참조하여 currentTime
속성을 조작해야 합니다.
직접 비디오 요소를 참조하기 위해 ref를 하나 정의해줍시다.
const videoRef = useRef(null);
그런 다음 ref를 비디오 요소와 연결시킵니다.
<video
src={SpinningCarVideo}
ref={videoRef}
className="w-full scale-[2.0] lg:scale-125 z-0 pointer-events-none select-none"
/>
이제 저희는 videoRef를 통해 원하는 비디오 요소를 직접 참조할 수 있습니다. 앞에서 정의한 상태 videoTimeline
과 videoRef
를 연결시켜줍시다. 상태가 변화할 때마다 비디오 요소가 리렌더링 되야 하므로, useEffect
를 사용합니다.
useEffect(() => {
videoRef.current.currentTime = videoTimeline;
}, [videoTimeline]);
이제 상태 videoTimeline
이 변화할 때마다 실시간으로 비디오의 타임라인이 변화합니다.
저희가 조절하길 원하는 비디오의 길이은 2초입니다. 사용자가 변화하는 비디오를 온전히 볼 수 있으려면 videoTimeline
이 0~2 사이의 값을 가져야 합니다. (다만 저희가 관찰한 결과 videoTimeline
이 0 미만이나 2 초과의 값을 가져도 심각한 에러는 불러들이지는 않고, 단순히 비디오가 변화하지 않을 뿐입니다. )
따라서 언제 비디오 재생이 시작되고(videoTimeline
이 0) 비디오 재생이 끝나는 지(videoTimeline
이 2)를 알고 그 사이의 선형적인 값을 계산하여 이를 videoTimeline
에 반영해야 할 것입니다.
그러면 사용자에게 비디오를 보여주기 위해 필요한 스크롤 y좌표값인 위 사진의 a, b가 필요하겠죠? 비디오의 시작과 끝에 해당되는 스크롤 위치인 a, b에 대한 정의는 기획서에 따라 저희는 이렇게 판단했습니다.
비디오 재생 시작 위치인 a는 (비디오 정중앙의 y좌표)-(사용자의 뷰포트의 높이), b는 (비디오 정중앙의 y좌표)로 설정했습니다. 그림에선 비슷하게 묘사되었지만, 실제 대부분의 상황에선 사용자 뷰포트의 크기가 비디오보다 큽니다.
언제 비디오 재생을 시작하고 끝마쳤는지도 정의했습니다. 이제 타임라인을 계산해야 합니다. 하지만 타임라인을 계산하는 함수인 calculateTimeline
을 작성하기 앞서, 이 함수를 '언제' 실행할 지 결정해야 합니다. window.scroll과 window.resize 이벤트가 발생할 때마다 해당 함수를 실행시켜 주도록 합시다.
이때 왜 resize 이벤트에도 핸들링하는 이유는, 사용자가 페이지를 띄운 채 브라우저 크기를 임의로 조절하거나(PC), 스마트폰을 가로/세로로 전환시킬 수 있습니다. 이때 resize 이벤트를 핸들링하지 않으면 페이지를 새로고침하지 않는 이상 비디오가 저희의 의도대로 재생되지 않을 수 있습니다.
useEffect(() => {
window.addEventListener("scroll", calculateTimeline);
window.addEventListener("resize", calculateTimeline);
return () => {
window.removeEventListener("scroll", calculateTimeline);
window.removeEventListener("resize", calculateTimeline);
};
}, []);
이제 진짜 calculateTimeline
함수 로직을 작성해 봅시다.
function calculateTimeline() {
const frameDOM = frameRef.current;
if (frameDOM) {
const videoHeight = frameDOM.offsetHeight;
const videoTop = frameDOM.getBoundingClientRect().top + window.scrollY;
const startScroll = videoTop + videoHeight / 2 - window.innerHeight;
const endScroll = startScroll + window.innerHeight;
const timeline = ((window.scrollY - startScroll) / (endScroll - startScroll)) * VIDEO_LENGTH;
setVideoTimeline(timeline);
}
}
videoHeight
은 비디오 파일의 높이, videoTop
은 비디오의 상단 모서리가 페이지에서 위치한 절대 y좌표입니다. 고정된 videoTop
을 계산하는 데 뜬금없이 현재 스크롤 위치인 window.scrollY
가 필요한 이유는, 아직 자바스크립트에는 부모요소 기준이 아닌 전체 페이지에서 특정 요소의 절대위치를 직접 계산할 수 있는 변수나 메소드가 없기 때문입니다. 따라서 현재 뷰포트 기준 특정 요소의 상대위치를 나타내는 getBoundingRect()
에다 현재 스크롤 위치를 더해줘야 했습니다.
위의
calculateTimeline
함수를 보시면 비디오 요소를 참조하는videoRef
가 아닌frameRef
를 쓰고 있습니다. 이는 저희 기획상의 문제로, 저희가 디자이너로부터 받았던 비디오 파일은 자동차가 재생되는 부분 밖의 여백이 아주 넓습니다. 따라서 이를 그대로 출력하면 페이지에서 자동차가 꽤 작아 보이기에, 비디오를transform: scale
로 더 크게 보이게 하고, 비디오를 감싸는 프레임 요소를 따로 작성해overflow: hidden
처리하여 불필요한 여백이 보이지 않게끔 조절했습니다.따라서 비디오 파일이 정상이라면
videoRef
를 그대로 쓰셔도 됩니다.
처음 코드를 작성했을 때, 비디오가 스크롤 실시간 반영이 되지 않고 재생이 매우 끊기는 문제점이 있었습니다. 용량 문제인가 싶어서 비디오 화질을 열화시켜 용량도 줄여보았지만, 정말 미세한 개선이 있을 뿐이었습니다. 다른 mp4 파일을 가져왔더니 정상적으로 재생이 되길래 저희는 디자이너가 제공한 동영상 파일 자체에 문제가 있다고 판단했지만, 근본적인 해결 방법은 찾지 못했습니다.
이는 간단하게 해결되었습니다. mp4 형식이었던 비디오 파일을 webm 형식으로 변환시켰더니 매끄럽게 재생이 되었습니다.
-
🎯 기술적 선택 이유
-
✨ UX 및 접근성
-
#️⃣ 코드 퀄리티
-
🛠️ 구현