본문 바로가기
프론트엔드(FrontEnd)

성격 검사 페이지 구현 회고 2 - 리액트에서 동적 비디오 로딩화면 구현하기 (복병 IOS혼내주기)

by codeyaki 2023. 6. 16.
반응형

로딩화면?

성격 검사에 모두 응답을 하고 결과를 계산하기 위해 서버와 통신하는 사이 귀여운 캐릭터가 빙글빙글 도는 영상을 제공하려고 하였다.

기존에는 컴포넌트를 한 개씩 순서대로 보여주며 결과를 서버로부터 내려받을 때까지 로딩 애니메이션을 보여주다가 서버와 통신을 마치면 결과에 해당하는 캐릭터로 변신하는 애니메이션을 보여주고 끝나면 폭죽을 터트리며 결과화면으로 이어지는 하나의 애니메이션처럼 보이도록 단순하게 만들었다.

 

근데 단순히 컴포넌트를 변경하는 방법으로 구현했을 경우 네트워크 속도에 따라서 아직 영상 메타데이터들 조차 다운로드 되지 않아 빈화면으로 바뀌었다가 다시 변신 영상이 로드되는 모습이 보이게 되어 매끄럽지 않고 중간에 한번 끊기는 모습을 보여주었다. 또한 로딩 애니메이션이 끝난 뒤에 변신 애니메이션을 재생해야 하는데 그냥 로딩이 다됐다고 바로 바꿔버리면 로딩 영상 중간에서 바뀌어서 매우 어색했다.

 

나는 이걸 어떻게 하면 해결할 수 있을까 고민을 해본 결과 로딩화면을 띄우고  z-index를 사용하여 그 뒤에 변신 애니메이션을 준비시키고 그 뒤에 결과화면을 준비시킨 뒤에 로딩화면 -> 변신화면을 차례대로 제거하는 방법을 생각했고 바로 구현을 해보았다. 로딩화면이 중간에 끝나는 문제는 로딩 애니메이션이 끝날때마다 준비가 완료됐는지 확인하는 방식으로 변경하여 해결할 수 있었다.

 

이를 위해서는 여러 state가 필요하다. 이는 결과 페이지에서만 필요한 정보이므로 redux를 사용하지 않고 그냥 useState를 통해서 관리하였다.

  • 로딩 애니메이션을 끄기 위한 state => isLoading
  • 변신 애니메이션을 끄기 위한 state => isAni
  • 변신 애니메이션이 준비됐는지 확인할 state => isDownloadVideo
  • 결과 이미지가 준비됐는지 확인할 state => isDownloadImg
  • 서버로 부터 API통신이 완료됐는지 확인할 state => isDataFetch
  const [isLoading, setIsLoading] = useState(true);
  const [isEndAni, setIsEndAni] = useState(false);
  const [isDownloadVideo, setIsDownloadVideo] = useState(false);
  const [isDownloadImg, setIsDownloadImg] = useState(false);
  const [isDataFetch, setIsDataFetch] = useState(false);

 

서버 API 통신

이는 기존에 loading을 구현할 때 많이 사용하는 로직인 처음에 isDataFetch를 false를 걸고 api 통신이 완료되면 true로 바꾸는 방식으로 구현하였다.

 

로딩 애니메이션

{isLoading && (
        <ResultLoading
          isDataFetch={isDataFetch}
          isDownloadImg={isDownloadImg}
          isDownloadVideo={isDownloadVideo}
          setIsLoading={setIsLoading}
        />
)}

먼저 로딩 애니메이션에는 isDataFetch, isDownloadImg, isDownloadVideo와 setIsLoading을 필요로 한다.

ResultLoading 컴포넌트 내부에는 비디오 태그를 사용해서 영상을 재생시켜 주었다.

gif는 시작과 정지를 컨트롤 할 수 없었기에 동영상 파일로 진행하였다.

<video
  autoPlay
  playsInline={true}
  muted
  width='100%'
  loop={!(isDataFetch && isDownloadImg && isDownloadVideo)}
  onEnded={handleOnEnded}
>
 <source src='비디오 경로' type='비디오 타입(video/mp4)'/>
</video>
  • autoplay: 페이지가 로드되면 자동으로 영상을 실행하기 위해서 넣어주었다.
  • playInline: ios같은 경우 비디오를 재생하면 전체화면으로 재생이 되는데 이를 방지하기 위해서 넣어주었다.
  • muted: 자동재생을 하기 위해서는 음소거 설정을 필수로 해주어야 하기 때문에 넣어주었다.
  • loop: 영상이 끝나면 자동으로 처음부터 다시 시작할지 설정이다. api통신과 변신애니메이션, 결과 이미지가 모두 준비 되었을 때 loop 속성을 false로 변경하여 자연스럽게 영상이 모두 재생된 뒤에 종료될 수 있도록 하였다.
  • onEnded: loop가 도는 동안은 호출이 되지 않고 loop가 false로 바뀌고 영상이 종료되었을때 호출된다. handleOnEnded에는 아래 코드를 넣어주어 로딩이 종료될 수 있도록 하였다.
const handleOnEnded = () => {
    setIsLoading(false);
  };

 

변신 애니메이션

변신 애니메이션이 페이지에 렌더링되기 시작하는 시간은 바로 api통신을 끝내고 결과를 받아온 뒤다. 왜냐하면 결과를 알아야 그에 맞는 변신 애니메이션을 넣어줄 수 있기 때문이다. 또한 isEndAni가 true가 되면 없애어야 한다.

{isDataFetch && !isEndAni && (
    <ResultAni
      SetIsDownloadVideo={setIsDownloadVideo}
      isLoading={isLoading}
      setIsEndAni={setIsEndAni}
      type={test.resultedType}
    />
)}

ResultAni 내부에는

먼저 변신 애니메이션은 바로 렌더링 시키자마자 재생시키면 안되고 로딩이 끝나면 그 후에 로딩화면을 제거하면서 동시에 변신 애니메이션을 재생시켜야 한다.

useEffect를 사용해서 로딩이 끝나면 변신 애니메이션이 재생될 수 있도록 하였다.

이를 위해서 useRef의 개념을 사용하였다.

const ref = useRef(null); // video 태그에 ref={ref}로 ref를 걸어주어야 함.
useEffect(() => {
	if (!isLoading) ref.current.play();
}, [isLoading]);

이후에 video 태그를 사용할때 로딩화면과는 조금 다른 속성들을 넣어주었다

<video
    playsInline={true}
    preload='auto'
    muted
    width='100%'
    onCanPlayThrough={() => {
      SetIsDownloadVideo(true);
    }}
    onEnded={() => setIsEndAni(true)}
    ref={ref}
  >
    <source
      src='변신 애니메이션 경로'
      type='비디오 타입(video/mp4)'
    />
</video>
  • preload: 페이지에 로딩되면 자동으로 모든 데이터를 다운로드하는 동작이다. ('metadata'로 설정하면 비디오의 메타데이터들만 다운로드하여놓은 뒤 사용자가 영상을 재생하면 모두 다운로드하는 방식이다.)
  • onCanPlayThrough: 영상이 자연스럽게 재생할 수 있는 상태가 되면 발생하는 이벤트 함수로 변신 애니메이션이 준비되었는지 확인하기 위해서 사용하였다.
  • ref: ref를 걸어주어 useEffect에서 video객체를 다루기 위해서 사용하였다.
  • onEnded: 로딩과 마찬가지로 영상이 종료됐는지 확인하기 위해서 사용하였다.

 

결과 페이지

로딩 애니메이션 -> 변신 애니메이션을 모두 하고나면 마지막에는 결과페이지로 바꿔주어야 한다.

나는 모두 겹쳐두었기 때문에 로딩 애니메이션과 변신애니메이션을 차례대로 제거만 해주면 자연스럽게 결과 페이지가 나오도록 만들었다.

다만 로딩 애니메이션을 종료시키기 위해서는 이미지가 로드가 다 되었는지 알려주어야 한다.

<img
  src='캐릭터 이미지 경로'
  alt={`character images`}
  onLoad={() => {
    setIsDownloadImg(true);
  }}
/>
  • onLoad 속성을 통해서 이미지가 로드되면 스테이트를 변경하여 loading 컴포넌트에게 알려주었다.

이러한 과정을 모두 마치면 아래의 영상같이 자연스러운 하나의 애니메이션이 탄생한다!

 

결과 화면1 - ESFJ

 

결과 화면 2 - INFJ

 

 

 

아이폰 IOS에서의 문제..

모두 구현을 하고 테스트를 해보는 중 아이폰에서 정상적으로 재생이 되지 않는다는 얘기를 듣고 처음에는 대체 어떤 문제가 있는지 알 수가 없어서 헤매고 있었다.

열심히 문제를 찾던중 <video> 태그의 onCanPlayThrough 속성이 ios에서 동작하지 않는다는 것을 찾을 수 있었다.

이유는 아직도 모르겠다. 아이폰의 폐쇄적인 정책으로 인하여 발생한 문제로 보인다.

그래서 대체할만한 방법을 찾던 중 video태그의 이벤트 함수에 대해 알아보았고 상태에 따른 여러 이벤트 함수가 있다는 것을 알게 되었다.

그중 유일하게 ios에서는 onLoadedMetadata 이벤트만 발생한다는 것을 확인하였다.

그리하여 onCanPlayThrough에서 onLoadedMetadata로 교체해 주었고, 정상적으로 재생되는 것을 확인할 수 있었다.

잠깐 설명하자면 리액트의 video 태그에는 동영상의 현재 상태에 따른 다양한 이벤트 함수가 있는데
1. onLoadedMetadata 영상의 메타데이터(미디어의 길이, 미디어 섬네일, 가로 세로 크기 등 영상을 재생하기 전에 알아야 하는 데이터들)를 로드했을 때
2. onLoadedData 단순히 영상을 재생할 수 있는 상태가 되었을 때
3. onCanPlay 현재 상태에서 원활하게 재생할 수 있는 상태일때 (버퍼링 X)
4. onCanPlayThrough 영상을 끝까지 원활하게 재생할 수 있는 상태일 때 
이러한 다양한 이벤트 함수가 있다. 그 외에도 다양한 이벤트 함수가 있으니 자기한테 맞게 사용하면 좋을 것 같다!

 

두 번째 문제로는 투명 배경 영상 사용에 제한적이라는 것이다.

처음에는 webm 영상을 이용해서 투명배경 영상을 제작하여 만들었는데 아이폰에서는 vp9코덱이 지원하지 않아 webm을 사용하면 영상이 나오지 않는 문제가 발생했다. (맥북에서는 투명배경이 적용되지는 않았지만 그래도 재생이 되기에 생각하지 못했다)

왜 많은 사람들이 mp4를 사용하는지 느끼고 모든 영상을 투명배경에서 하얀색배경을 넣고 mp4로 바꿔서 업로드를 해서 해결할 수 있었다.

말로 하니 금방 해결한 것처럼 느껴지지만 많은 시간 동안 헤매고 해결한 문제이다 ㅠㅠ...

안드로이드에서 문제

마지막으로 발생한 문제는 카카오 인앱에서 우리 검사를 진행하면 애니메이션 사이에 아래 화면이 등장하여 애니메이션이 끊겨 보이는 증상을 발견했다...

왜 이런가 알아본 결과 비디오의 포스터가 설정되어 있지 않아서 발생한 문제로 보여져서 아래 속성을 넣어주면 된다.

<video
    playsInline={true}
    preload='auto'
    muted
    width='100%'
    onLoadedMetadata={() => {
      SetIsDownloadVideo(true);
    }}
    onEnded={() => setIsEndAni(true)}
    ref={ref}
    poster={process.env.PUBLIC_URL + `/characters-mp4/umi_thumb.png`} // 추가된 속성
    >
        <source />
</video>
  • poster를 추가해주면 영상이 시작되기 전에 설정한 사진을 보여준다.
  • 카카오 인앱 브라우저는 해당 설정이 없으면 기본 이미지를 보여주기 때문인것 같다.

 

그래도 결과를 보니 조금 만족스러운 결과가 나와서 즐거웠다.

반응형