React ๐Ÿ“ฝ๏ธ์—์„œ ๋งž์ถคํ˜• ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๋งŒ๋“ค๊ธฐ

10384 ๋‹จ์–ด webdevreactjavascriptvideo
์—ฌ๋Ÿฌ๋ถ„, ์ด ํŠœํ† ๋ฆฌ์–ผ์—์„œ๋Š” React์—์„œ ์‚ฌ์šฉ์ž ์ง€์ • ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋นŒ๋“œํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ๋›ฐ์–ด๋“ค์ž!



์„ค์ •



์ƒˆ๋กœ์šด ๋ฐ˜์‘ ์•ฑ ๋งŒ๋“ค๊ธฐ




npx create-react-app custom-video-player


๋Œ€์ฒญ์†Œ


  • App.js์˜ ์•ฑ div์—์„œ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

  • import "./App.css";
    
    function App() {
      return <div className="app"></div>;
    }
    
    export default App;
    


  • App.css์˜ ๋ชจ๋“  ํ•ญ๋ชฉ ์‚ญ์ œ
  • in index.css ์ถ”๊ฐ€-

  • * {
      margin: 0;
    }
    


    ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด์šฉ UI ๋งŒ๋“ค๊ธฐ



    ๋น„๋””์˜ค ์ถ”๊ฐ€



    ์•ฑ div ๋‚ด์—์„œ ๋น„๋””์˜ค์˜ src์™€ ํ•จ๊ป˜ ๋น„๋””์˜ค ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์Šคํƒ€์ผ ์ง€์ •์„ ์œ„ํ•ด className๋„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

     <video
        className="video"
        src="https://res.cloudinary.com/dssvrf9oz/video/upload/v1635662987/pexels-pavel-danilyuk-5359634_1_gmixla.mp4"
     ></video>
    


    ๋น„๋””์˜ค ์ปจํŠธ๋กค ์ถ”๊ฐ€



    ๋น„๋””์˜ค ๊ตฌ์„ฑ ์š”์†Œ ์•„๋ž˜์— ์•„์ด์ฝ˜์œผ๋กœ ์ผ๋ถ€ Svgs๊ฐ€ ์žˆ๋Š” ์ด div๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์™€ ๊ฐ™์€ ์ง์ ‘ Svg๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์•„์ด์ฝ˜์— ๋Œ€ํ•œ ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค :).

      <div className="controlsContainer">
            <div className="controls">
              <img className="controlsIcon" alt="" src="/backward-5.svg" />
              <img className="controlsIcon--small" alt="" src="/play.svg" />
              <img className="controlsIcon" alt="" src="/forward-5.svg" />
            </div>
      </div>
    


    ์‹œ๊ฐ„์— ๋Œ€ํ•œ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์ค„ ์ถ”๊ฐ€



    ๋˜ํ•œ ํ˜„์žฌ ์‹œ๊ฐ„๊ณผ ๋น„๋””์˜ค์˜ ์ด ์‹œ๊ฐ„์„ ๋ณด์—ฌ์ฃผ๋Š” ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์ค„์„ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.

     <div className="timecontrols">
            <p className="controlsTime">1:02</p>
            <div className="time_progressbarContainer">
              <div style={{ width: "40%" }} className="time_progressBar"></div>
            </div>
            <p className="controlsTime">2:05</p>
       </div>
    


    Forgive me for not having very good naming conventions in classes. I have forgotten how to name my classes because of Tailwind :P



    UI ์Šคํƒ€์ผ๋ง



    ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด๋Š” ์ง€๊ธˆ ๋ณด๊ธฐ์— ๋งค์šฐ ๋ชป์ƒ๊ฒผ์œผ๋‹ˆ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. App.css์—์„œ ๋ช‡ ๊ฐ€์ง€ ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    /* Main Container */
    
    .app {
      display: flex;
      flex-direction: column;
      width: 100vw;
      height: 100vh;
      overflow: hidden;
    }
    
    /* Video */
    
    .video {
      width: 100vw;
      height: 100vh;
    }
    
    /* Controls */
    
    .controlsContainer {
      display: flex;
      flex-direction: column;
      align-items: center;
      width: 100vw;
      background-color: transparent;
      margin-top: -50vw;
      padding: 0 40px;
      z-index: 20;
    }
    
    .controls {
      display: flex;
      align-items: center;
      justify-content: space-evenly;
      padding-top: 18rem;
      margin: auto;
    }
    
    .controlsIcon {
      width: 40px;
      height: 40px;
      cursor: pointer;
      margin-left: 10rem;
      margin-right: 10rem;
    }
    
    .controlsIcon--small {
      width: 32px;
      height: 32px;
      cursor: pointer;
      margin-left: 10rem;
      margin-right: 10rem;
    }
    
    /* The time controls section */
    
    .timecontrols {
      display: flex;
      align-items: center;
      justify-content: space-evenly;
      position: absolute;
      bottom: 4rem;
      margin-left: 10vw;
    }
    
    .time_progressbarContainer {
      background-color: gray;
      border-radius: 15px;
      width: 75vw;
      height: 5px;
      z-index: 30;
      position: relative;
      margin: 0 20px;
    }
    
    .time_progressBar {
      border-radius: 15px;
      background-color: indigo;
      height: 100%;
    }
    
    .controlsTime {
      color: white;
    }
    


    ์ด์ œ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.



    ํ”Œ๋ ˆ์ด์–ด์— ๋…ผ๋ฆฌ ์ถ”๊ฐ€



    ๊ธฐ๋Šฅ์„ ์ž‘์—…ํ•˜๋ ค๋ฉด ๋จผ์ € useRef ํ›„ํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋””์˜ค์— ์ฐธ์กฐ๋ฅผ ์ฒจ๋ถ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์•„๋ž˜ ์ œ๊ณต๋œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค.
  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฐธ์กฐ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

  • const videoRef = useRef(null);
    


  • ๋ฐ˜์‘์—์„œ useRef ํ›„ํฌ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

  • import { useRef } from "react";
    


  • ๋™์˜์ƒ์— ์ฒจ๋ถ€

  •  <video
        ref={videoRef}
        className="video"
        src="https://res.cloudinary.com/dssvrf9oz/video/upload/v1635662987/pexels-pavel-danilyuk-5359634_1_gmixla.mp4"
     ></video>
    


    ์žฌ์ƒ ๋ฐ ์ผ์‹œ ์ •์ง€ ๊ธฐ๋Šฅ



    ์žฌ์ƒ ๋ฐ ์ผ์‹œ ์ •์ง€๋ฅผ ์œ„ํ•ด ์ œ์–ด ์ธ์ˆ˜๋ฅผ ์ทจํ•˜๊ณ  ์ œ์–ด์— ๋”ฐ๋ผ ๋น„๋””์˜ค๋ฅผ ์žฌ์ƒํ•˜๊ฑฐ๋‚˜ ์ผ์‹œ ์ค‘์ง€ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

     const videoHandler = (control) => {
        if (control === "play") {
          videoRef.current.play();
        } else if (control === "pause") {
          videoRef.current.pause();
        }
      };
    


    ์ด์ œ play.svg ์ด๋ฏธ์ง€์—์„œ onClick ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋น„๋””์˜ค๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

      <img
         onClick={() => videoHandler("play")}
         className="controlsIcon--small"
         alt=""
         src="/play.svg"
         />
    


    ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜์‹œ๋ฉด ์˜์ƒ์ด ์žฌ์ƒ๋ฉ๋‹ˆ๋‹ค!

    ์žฌ์ƒ/์ผ์‹œ์ •์ง€ ์ƒํƒœ์— ๋”ฐ๋ฅธ ์•„์ด์ฝ˜ ๋ณ€๊ฒฝ
    ์ด๋ฅผ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด useState ํ›„ํฌ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์žฌ์ƒ ์ƒํƒœ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    const [playing, setPlaying] = useState(false);
    


    const ๋น„๋””์˜ค ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜์—์„œ onClick ๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    const videoHandler = (control) => {
        if (control === "play") {
          videoRef.current.play();
          setPlaying(true);
        } else if (control === "pause") {
          videoRef.current.pause();
          setPlaying(false);
        }
      };
    


    ์•„์ด์ฝ˜ ๋ณ€๊ฒฝ
    ์žฌ์ƒ ์•„์ด์ฝ˜์ด ์žˆ๋Š” ๊ณณ์—์„œ ์ด์ œ ์‚ผํ•ญ ์—ฐ์‚ฐ์ž์˜ ๋„์›€์„ ๋ฐ›์•„ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

      {playing ? (
                <img
                  onClick={() => videoHandler("pause")}
                  className="controlsIcon--small"
                  alt=""
                  src="/pause.svg"
                />
              ) : (
                <img
                  onClick={() => videoHandler("play")}
                  className="controlsIcon--small"
                  alt=""
                  src="/play.svg"
                />
              )}
    


    ์ด์ œ ๋น„๋””์˜ค๋ฅผ ์žฌ์ƒํ•˜๊ณ  ์ผ์‹œ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค ๐Ÿฅณ

    ๋น„๋””์˜ค ์ „๋‹ฌ ๋ฐ ๋˜๋Œ๋ฆฌ๊ธฐ



    ๋‚˜๋Š” ์ด๊ฒƒ์„ ์œ„ํ•ด ๋งค์šฐ ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.

     const fastForward = () => {
        videoRef.current.currentTime += 5;
      };
    
      const revert = () => {
        videoRef.current.currentTime -= 5;
      };
    


    ์ด์ œ ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ ๋ฒ„ํŠผ์˜ onClick์œผ๋กœ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    ์•ž์œผ๋กœ

    <img
      onClick={fastForward}
      className="controlsIcon"
      alt=""
      src="/forward-5.svg"
         />
    


    ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ

    <img
      onClick={revert}
      className="controlsIcon"
      alt=""
      src="/backward-5.svg"
         />
    


    ์‹œ๊ฐ„ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์ค„



    ๋™์˜์ƒ ๊ธธ์ด ๊ฐ€์ ธ์˜ค๊ธฐ

    ๋™์˜์ƒ ๊ธธ์ด๋ฅผ ํ™•์ธํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”.
  • ๋™์˜์ƒ ๊ตฌ์„ฑ์š”์†Œ์— ID๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

  •  <video
        id="video1"
        ref={videoRef}
        className="video"
        src="https://res.cloudinary.com/dssvrf9oz/video/upload/v1635662987/pexels-pavel-danilyuk-5359634_1_gmixla.mp4"
     ></video>
    


  • ๋น„๋””์˜ค ๊ธธ์ด๋ฅผ ์ €์žฅํ•  ์ƒํƒœ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

  • const [videoTime, setVideoTime] = useState(0);
    


  • ์˜์ƒ ์žฌ์ƒ์‹œ ์˜์ƒ ์‹œ๊ฐ„์„ ์ด๋ ‡๊ฒŒ ์„ค์ •

  • if (control === "play") {
          videoRef.current.play();
          setPlaying(true);
          var vid = document.getElementById("video1");
          setVideoTime(vid.duration);
        }
    


    ์ด์ œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์‹œ๊ฐ„ ๋Œ€์‹  videoTime ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์ž์—ด ์กฐ์ž‘์€ ์‹œ๊ฐ„์„ 1:05์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

      <p className="controlsTime">
        {Math.floor(videoTime / 60) + ":" + ("0" + Math.floor(videoTime % 60)).slice(-2)}
     </p>
    


    ๋น„๋””์˜ค์˜ ํ˜„์žฌ ์‹œ๊ฐ„ ๊ฐ€์ ธ์˜ค๊ธฐ

    ๋น„๋””์˜ค์˜ ํ˜„์žฌ ์‹œ๊ฐ„์„ ์–ป์œผ๋ ค๋ฉด 1์ดˆ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฏ€๋กœ window.setInterval์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    window.setInterval(function () {
        setCurrentTime(videoRef.current?.currentTime);
      }, 1000);
    


    ์ด์ œ ํ•ญ์ƒ ๊ทธ๋ ‡๋“ฏ์ด ๊ฐ’์„ ์ €์žฅํ•  ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    const [currentTime, setCurrentTime] = useState(0);
    


    ํ•˜๋“œ ์ฝ”๋“œ ๊ฐ’ ๋Œ€์‹  ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    <p className="controlsTime">
        {Math.floor(currentTime / 60) + ":" + ("0" + Math.floor(currentTime % 60)).slice(-2)}
    </p>
    


    ์ง„ํ–‰๋ฅ  ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์ค„์— ์„ค์ •

    ์ง„ํ–‰์„ ์œ„ํ•œ ๋‹ค๋ฅธ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ-

    const [progress, setProgress] = useState(0);
    


    ์ด์ œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  window.setInterval ํ•จ์ˆ˜ ์•ˆ์— ๋‹ค๋ฅธ ์ค„์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

    setProgress((videoRef.current?.currentTime / videoTime) * 100);
    


    ๊ธฐ๋Šฅ์€ ์ด์ œ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

    window.setInterval(function () {
        setCurrentTime(videoRef.current?.currentTime);
        setProgress((videoRef.current?.currentTime / videoTime) * 100);
      }, 1000);
    


    ์ด์ œ ๋งž์ถค ๋™์˜์ƒ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค ๐ŸŽ‰๐ŸŽŠ

    ์œ ์šฉํ•œ ๋งํฌ-

    GitHub repository

    ReactJS docs

    All socials

    ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ