Frontend/UI&UX

snap-scroll & scroll된 동영상 재생 (youtube shorts)

덕구공 2022. 7. 7. 16:19

What to do?

  • snap-scroll을 이용해서 부드러운 스크롤을 구현해보자.
    • 스크롤을 조금만 내려도 바로 다음 아이템의 가운데까지 스크롤된 후 멈추게하자!
    • 스크롤할 아이템을 감싸는 컨테이너는 scroll-snap-type: y mandatory를 사용!
    • 스크롤될 아이템은 scroll-snap-align:center를 사용!
  • 유튜브 shorts 처럼 스크롤된 아이템의 가운데 화면이 멈추고 동영상이라면 동영상을 재생시키고 그렇지 않은 동영상들은 중지시키자
    • 스크롤되어 화면 가운데에 보이는 동영상만 재생시킴! 사진은 재생 x!

 

소스코드

 

App.js

  • 스크롤할 아이템을 감싸는 컨테이너의 뷰포트에서 맨 위 Y좌표(snapScrollTopY)와 맨 아래 Y좌표(snapScrollBottomY) 사이에 스크롤될 아이템의 뷰포트에서 중간 좌표(snapScrollItemCenter)가 들어오면 비디오를 재생시키고 그 사이에 있지 않은 아이템은 비디오를 중지시킨다!
    • 위 그림 보면 잘 이해될듯?
import "./App.scss";
import {useRef, useState} from "react";

const App = () => {
  // snapScrollItem을 담는 wrapper의 ref
  const snapScrollWrapperRef = useRef();
  const [images, setImages] = useState([
    {previewURL: "img/image1.jpg", type: "image"},
    {previewURL: "video/video1.mp4", type: "video"},
    {previewURL: "img/image2.jpg", type: "image"},
    {previewURL: "video/video2.mp4", type: "video"},
    {previewURL: "img/image3.png", type: "image"},
  ]);

  const playVideo = (e) => {
    // snap-scroll-wrapper의 뷰포트에서 맨 위 Y좌표와 맨 아래 Y좌표를 구함
    const snapScrollWrapperRect = e.target.getBoundingClientRect();
    const snapScrollWrapperTopY = snapScrollWrapperRect.top;
    const snapScrollWrapperBottomY = snapScrollWrapperRect.bottom;
    // 스크롤되는 아이템들은 snap-scoll-wrapper의 자식들(snap-scroll-item)이다.
    const snapScrollItems = e.target.childNodes;
    snapScrollItems.forEach((item) => {
      // 이미지나 비디오는 snap-scroll-item의 0번째 자식
      const snapScrollItem = item.childNodes[0];
      // 비디오일 때만 부모의 뷰포트 맨위와 맨 아래에 중심이 들어왔을 때 실행
      if (snapScrollItem.tagName === "VIDEO"){
        const snapScrollItemRect = item.childNodes[0].getBoundingClientRect();
        // snapScrollItem의 뷰포트에서 중앙 Y 좌표
        const snapScrollItemCenter = (snapScrollItemRect.top + snapScrollItemRect.bottom) / 2;
        if (snapScrollItemCenter > snapScrollWrapperTopY &&
          snapScrollItemCenter < snapScrollWrapperBottomY
        ) {
          snapScrollItem.play();
        }
        else{
          snapScrollItem.pause();
        }
      }
    })
  }
  return (
    <>
      <div className="header">
        THIS IS HEADER!!
      </div>
      <div className="snap-scroll-wrapper" ref={snapScrollWrapperRef} onScroll={playVideo}>
        {images.map((item, index) => (
          <div className="snap-scroll-item" key={index}>
            {item.type === "image" ? (
              <img src={item.previewURL}/>
            ) : (
              <video src={item.previewURL} controls={true} muted={true}/>
            )}
          </div>
        ))}
      </div>
    </>
  );
}
export default App;

App.scss

  • 한 화면에 하나의 비디오나 이미지가 들어오게 하기 위해서 헤더를 제외한 뷰포트의 전체 높이를 스크롤될 아이템들을 감싸는 컨테이너와 스크롤될 아이템을 위한 공간으로 사용한다!
  • (전체 높이 - 헤더의 높이)를 계산해서 높이를 부여하는 부분을 잘 염두해두자!
// snap-scroll-wrapper에서 %높이를 사용하려면 여기서 먼저 높이를 지정해야함
// body는 기본적으로 높이값이 없기 때문에 높이를 줘야함
html, body {
  height: 100%;
}

//body의 자식인 root도 height를 먹여줌
#root {
  height: 100%;
}

body {
  margin: 0;
  padding: 0;
}

// header 높이 계산
:root {
  --header--height: 100px
}

.header {
  position: fixed;
  top: 0;
  left: 0;
  background: gray;
  height: var(--header--height);
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  font-size: 2rem;
  color: #ffffff;
}

.snap-scroll-wrapper {
  position: relative;
  // header가 fixed이므로 relative 속성을 주고 header 높이만큼 top 값을 부여
  top: var(--header--height);
  // header 높이를 제외한 나머지는 모두 높이
  height: calc(100% - var(--header--height));
  // y축 snap-scroll
  scroll-snap-type: y mandatory;
  overflow: auto;

  .snap-scroll-item {
    height: 80%;
    width: 350px;
    // 이미지 border 밖으로 나가는거 자르기
    overflow: hidden;
    background: black;
    border-radius: 20px;
    // item의 가운데 스크롤이 멈추게
    scroll-snap-align: center;
    // 가운데 정렬 및 위아래 margin
    margin: 2rem auto;
    img, video{
      width: 100%;
      height: 100%;
      // 비율 유지하면서 최대한 포함 시키기 확대x
      object-fit: contain;
    }
  }
}