[TS 과제 챌린지] Custom Video Player - 구현

조민호·2023년 5월 18일
0


TS스터디 팀원들과 함께 매주 주차별 과제를 진행합니다

https://github.com/bradtraversy/vanillawebprojects

  • 위의 링크에는 바닐라JS로 간단히 구현된 단일 페이지의 예시들이 있습니다
  • HTML/CSS는 기존에 작성된 예시를 그대로 사용하지만, JS로직은 전부 삭제하고 스스로의 로직대로 완전히 새로 작성합니다
    - 이때 , 모든 기능 구현을 JS대신 TS로 직접 변환해서 작성합니다



2주차 - Custom Video Player

기능 동작 예시 링크 : https://vanillawebprojects.com/projects/custom-video-player/


구현 코드

const video = document.getElementById('video') as HTMLVideoElement;
const playButton = document.getElementById('play') as HTMLButtonElement;
const stopButton = document.getElementById('stop') as HTMLButtonElement;
const progressBar = document.getElementById('progress') as HTMLInputElement;
const timestamp = document.getElementById('timestamp') as HTMLSpanElement;

// 상태 수정 함수 , 상태 리턴 함수
type setStateFunc<T> = (state: T) => void;
type stateFunc<T> = () => T;

// 상태가 가질 수 있는 값
type TF=true|false

// 상태 인터페이스
interface playStateObj {
  play: TF; // false : 일시정지 or 정지 ,  true : 재생
}

type playOrStopFunc = () => void;
type swapIconFunc = () => void;
type changeProgressBarFunc = () => void;
type setVideoByProgressbarFunc = () => void;
type changeTimeFunc = () => void;
type stopVideoFunc = () => void;

// [상태,상태 변경 함수]를 리턴하는 changeState함수
const changeState = <T extends playStateObj>(status: T): [stateFunc<T>, setStateFunc<T>] => {

  let initialState = status;

  const state: stateFunc<T> = () => initialState as T;

  const setState: setStateFunc<T> = (newState: T) => {
    initialState = newState;
  };

  return [state, setState];
};

// 초기 상태 선언
const initialState: playStateObj = { play: false };
const [playState, setPlayState] = changeState(initialState);

// 일시 정지 , 재생을 다루는 함수
const playOrStop: playOrStopFunc = () => {

  // 재생중일 때
  if (playState().play) {
    video.pause();
    setPlayState({ play: false });
    playButton.innerHTML = '<i class="fa fa-play fa-2x"></i>';
    return;
  }

  //일시정지 or 정지일 때
  if (!playState().play) {
    video.play();
    setPlayState({ play: true });
    playButton.innerHTML = '<i class="fa fa-pause fa-2x"></i>';
    return;
  }
};

// 동영상 진행에 따른 프로그래스바 이동 함수
const changeProgressBar: changeProgressBarFunc = () => {
  const current: string = String((video.currentTime / video.duration) * 100);

  progressBar.value = current;
};

// 동영상 진행에 따른 하단 시간 업데이트 함수
const changeTime: changeTimeFunc = () => {
  let min: number = Math.floor(video.currentTime / 60);
  let second: number = Math.floor(video.currentTime % 60);

  timestamp.innerHTML = `${min}:${second}`;
};

// 프로그래스바 변경시 , 동영상 위치 조정 함수
const setVideoByProgressbar: setVideoByProgressbarFunc = () => {
  video.currentTime = (Number(progressBar.value) * video.duration) / 100;
};

// 동영상 정지 함수
const stopVideo: stopVideoFunc = () => {
  setPlayState({ play: true }); 
  video.currentTime = 0;
  playOrStop();

  // video.pause();
  // setPlayState({ play: false });
  // playButton.innerHTML = '<i class="fa fa-play fa-2x"></i>';
};

// 이벤트 할당
video.addEventListener('click', playOrStop);
video.addEventListener('timeupdate', changeTime);
video.addEventListener('timeupdate', changeProgressBar);
playButton.addEventListener('click', playOrStop);
progressBar.addEventListener('change', setVideoByProgressbar);
stopButton.addEventListener('click', stopVideo);



이번 구현에서 중점적으로 다룬 것들입니다

  1. 최대한 하나의 함수는 하나의 기능만 수행하도록 합니다
  2. 중복되는 로직이 있으면 최대한 기존 로직을 재사용 합니다
  3. 상태를 만들고 이걸 기반으로 로직을 진행합니다



동영상 플레이어 기능들을 구현하는 로직입니다. 대표적인 기능들은 아래와 같습니다

  1. 일시정지&재생 기능 (화면 , 버튼 클릭시)

  2. 재생시간에 따른 하단 프로그래스바 변경 및 프로그래스바 조정시 비디오 재생시점 변경

  3. 재생시간 표시

  4. 비디오 정지 기능


이번에 작성한 코드는 ‘상태(playState)’ 를 가집니다

playState라는 변수의 상태는 동영상이 일시정지인지 , 재생중인지에 따른 상태를 의미합니다


그렇지만 이번에는 단순히 함수 안에서 객체의 값만 바꾼다기보다,

뭔가 리액트에서의 useState()와 비슷하게 구현해보고 싶었습니다

완벽하진 않지만, JS의 클로저 개념을 이용해서 구현했습니다

해당 로직의 클로저와 관련된 기본적인 설명은 아래 링크에서 확인하면 됩니다

바닐라JS로 useState() 구현 도전해보기

실제 리액트의 useState()와 동일하게 구현하는 것은 난이도가 매우 높기 때문에 , 시간을 가지고 비슷하게라도 만들어보기 위해 추후 더 개선하도록 노력하겠습니다



[상태,상태 변경 함수]를 리턴하는 changeState함수 입니다

    // 상태 수정 함수 , 상태 리턴 함수
    type setStateFunc<T> = (state: T) => void;
    type stateFunc<T> = () => T;
    
    // 상태가 가질 수 있는 값
    type TF=true|false
    
    // 상태 인터페이스
    interface playStateObj {
      play: TF; // false : 일시정지 or 정지 ,  true : 재생
    }
    
    // [상태,상태 변경 함수]를 리턴하는 changeState함수
    const changeState = <T extends playStateObj>(status: T): [stateFunc<T>, setStateFunc<T>] => {
    
      let initialState = status;
    
      const state: stateFunc<T> = () => initialState as T;
    
      const setState: setStateFunc<T> = (newState: T) => {
        initialState = newState;
      };
    
      return [state, setState];
    };

실제 리액트에서 useState()의 명세서를 보면 제네릭을 이용하고 튜플타입을 리턴하고 있습니다

그러므로 이 역시 비슷하게 타입을 지정합니다

  1. 제네릭으로 전달받는 인자는 이 로직에서 상태의 타입으로 사용되고 있는 playStateObj의 종류이도록 제약조건을 걸어줍니다

  2. 리턴하는 상태 함수와 상태 업데이트 함수 역시 T타입을 받아서 로직을 진행합니다



changeState함수를 바탕으로 초기 상태를 선언하는 부분입니다

const initialState: playStateObj = { play: false };
const [playState, setPlayState] = changeState(initialState);

상태 인터페이스를 보면 아래와 같습니다

상태값이 가질 수 있는 값은 true와 false이며

true 일경우엔 비디오가 재생중인 경우,

false인 경우엔 비디오가 일시정지or정지인 경우입니다

// 상태가 가질 수 있는 값
type TF=true|false
    
// 상태 인터페이스
interface playStateObj {
	play: TF; // false : 일시정지 or 정지 ,  true : 재생
}

💡 play: boolean으로 해도 무방합니다



일시 정지 , 재생을 다루는 함수 입니다

    const playOrStop: playOrStopFunc = () => {
    
      // 재생중일 때
      if (playState().play) {
        video.pause();
        setPlayState({ play: false });
        playButton.innerHTML = '<i class="fa fa-play fa-2x"></i>';
        return;
      }
    
      //일시정지 or 정지일 때
      if (!playState().play) {
        video.play();
        setPlayState({ play: true });
        playButton.innerHTML = '<i class="fa fa-pause fa-2x"></i>';
        return;
      }
    };

이 함수에서 진행하는 기능은 다음과 같습니다

  1. 상태를 변경하는 부분

  2. 비디오를 재생or일시정지 하는 부분

  3. 버튼모양을 변경하는 부분

사실 , 여기서도 위 과정들이 전부 기능별 함수단위로 분리가 되어야 하지만 가독성 측면도 고려해야 하기 때문에 생략했습니다



동영상 진행에 따른 프로그래스바 이동 함수 입니다

    const changeProgressBar: changeProgressBarFunc = () => {
      const current: string = String((video.currentTime / video.duration) * 100);
    
      progressBar.value = current;
    };



동영상 진행에 따른 하단부의 시간을 업데이트 하는 함수 입니다

    const changeTime: changeTimeFunc = () => {
      let min: number = Math.floor(video.currentTime / 60);
      let second: number = Math.floor(video.currentTime % 60);
    
      timestamp.innerHTML = `${min}:${second}`;
    };



프로그래스바 변경시 , 동영상 위치 조정을 하는 함수 입니다

    const setVideoByProgressbar: setVideoByProgressbarFunc = () => {
      video.currentTime = (Number(progressBar.value) * video.duration) / 100;
    };



동영상 정지 함수 입니다

    const stopVideo: stopVideoFunc = () => {
      if(playState().play===false){
        setPlayState({ play: true }); 
      }
      video.currentTime = 0;
      playOrStop();
    
      // video.pause();
    	// video.currentTime = 0;
      // setPlayState({ play: false });
      // playButton.innerHTML = '<i class="fa fa-play fa-2x"></i>';
    };

아래의 주석대로 진행해도 되지만, 앞서 언급했듯이 최대한 중복되는 로직을 반복하기 위해서

동영상의 시간만 수정해주고 playOrStop() 함수를 호출합니다

여기서 상태값을 true로 강제로 지정한 이유는,

로직상 일시정지일때(false) playOrStop()을 호출하면

정지를 하자마자 다시 동영상이 재생되는 로직이 실행됩니다

    if (!playState().play) {
        video.play();
        setPlayState({ play: true });
        playButton.innerHTML = '<i class="fa fa-pause fa-2x"></i>';
        return;
      }

그러므로 정지할때만 이렇게 임의로 상태값을 강제로 true로 변경해서

일시정지 상태에서 정지버튼을 눌러도 제대로 정지가 되게 하는 것입니다

( play : false인 경우에서 이렇게 강제로 true로 변경을 해도,

playOrStop()로직에 의해 다시 false로 되돌아오므로 안전합니다 )

profile
웰시코기발바닥

0개의 댓글