본문 바로가기

개발/React

모바일 웹뷰환경에서 영상을 제공하는 여정

반응형

부제 : 웹뷰개발 시 iOS 저전력모드에 대응하는 방법

 

 

목차

  • 글을 시작하며 : 글또 커뮤니티 소개 및 주제 결정
  • 서비스 영상 삽입 과정에서의 고민과 선택
  • Video 태그를 활용한 서비스 영상 삽입 및 Layout Shift 개선하기
  • iOS에서의 자동재생 이슈
  • Lottie를 활용한 영상 제공 및 Dynamic Import를 활용한 성능 최적화
  • 운영하던 도중 발견한 추가 이슈사항 (2024.4.17 추가내용)
  • Re-Cap

 

 

글을 시작하며  : 글또 커뮤니티 소개 및 주제 결정

 

이전 글에서 이야기했던 바와 같이 혼자서 파편화된 정보를 계속해서 축적해 나가기보다는 보다 정제된 글로 발전시키면서 가지고 있던 기억을 더 장기화하기 위해 많이 고민해 왔고, 이를 더 잘 수행하기 위해 글또라는 커뮤니티에 속하게 되어 앞으로 5개월간은 주기적으로 글을 작성할 예정입니다 :)

 

글또는 비영리 커뮤니티로 5개월의 운영기간동안 2주에 한번씩 글을 작성할 수 있도록 시스템을 잡아두어 글쓰기 습관을 들일 수 있도록 해주는 커뮤니티입니다.

 

이번 활동을 토대로 5개월 뒤에도 꾸준하게 글을 남길 수 있도록 노력해 보고자 합니다.

 

이번에는 글또 활동을 시작하며 어떤 글로 시작하면 좋을지 많이 고민했고, 고민 끝에 처음 작성하는 글로 사내 프로젝트를 진행하며 마주한 이슈를 트래킹하는 글을 작성하면 좋겠다고 생각했습니다. 

 

마침 팀이 담당하던 피처 서비스 지면을 리뉴얼하게 되었고 이 과정에서 서비스의 간단한 소개를 위해 짧은 영상을 넣게 되었는데 생각보다 간단할 줄 알았던 이 작업을 진행하는 도중 겪었던 문제들이 있어 이에 관해 이야기해 보고자 합니다.
 

 

 

서비스 영상 삽입 과정에서의 고민과 선택

 

위에서 이야기한 바와 같이 현재 팀에서 담당하는 피처 서비스의 지면이 리브랜딩 과정을 거치며 전반적으로 리뉴얼 작업을 거치게 되었고, 이 과정에서 새로운 브랜딩 콘텐츠에 대한 영상을 지면 최상단에 제공해야 하는 니즈가 생겼습니다.


영상을 추가해야 하는 상황이 오자 어떤 방식으로 영상을 보여줄지 고민하게 되었습니다. 떠올랐던 방법은 두 가지였는데요, MP4를 활용하는 방법과 Lottie를 활용하는 방식이었습니다.

먼저 Lottie가 생소하신 분들이 계실 수 있어 간단히 설명하자면, Airbnb에서 개발한 오픈소스 라이브러리로 JSON 형태의 벡터 기반 애니메이션 파일을 실행시키는 방식입니다. 특정 아이콘을 활용할 때 IMG보다는 SVG를 활용하는 것과 동일하게 벡터 기반의 장점은 비교적 용량이 작고 확대 및 축소에도 해상도의 저하가 없다는 점입니다.

해당 글은 Lottie에 대해 집중적으로 다루는 글은 아니기 때문에 Lottie에 대해 궁금하신 분이 계시다면 아래의 글을 따로 읽어보시는걸 추천드립니다 :)
https://lottiefiles.com/kr/blog/about-lottie/kr-lottie-vs-gif


처음에는 전달받은 두 파일의 용량을 비교해보았습니다. mp4가 400kB, Lottie가 5MB였습니다. 
일반적으로는 Lottie파일의 용량이 더 적으나해당 영상 파일은 낮은 프레임 레이트로 구성되어있었고 이를 그대로 Lottie로 변환하기에는 무리가 있어 프레임 레이트를 약 20배 정도 높이는 과정에서 이러한 현상이 발생했습니다.

따라서 처음에는 용량의 차이를 보고 자연스럽게 mp4를 사용하는 방식을 선택하게 되었고, 실제로 비디오파일을 실행하는 코드도 복잡하지 않다는 점에서 바로 작업을 진행해보았습니다.

 

 

 

Video 태그를 활용한 서비스 영상 삽입 및 Layout Shift 개선하기



먼저 필요한 기본 요구사항을 정리해보니 다음과 같았습니다.

 

 

  1. 지면 진입 시 자동으로 실행
  2. 반복재생 

위 요구사항을 기반으로 video 태그에서 지원해주는 속성들을 현재 상황에 맞게 적용해둔 코드는 같습니다.

 

  • controls : video의 조작 여부
  • autoPlay : 자동 실행 여부
  • muted : 사운드 출력 여부
  • loop : 반복 재생 여부
  • playsInline : 전체화면 없이 바로 재생할지에 대한 여부
<video
  width="100%"
  controls={false}
  autoPlay
  muted
  loop
  playsInline
>
  <source src={CARE_VIDEO} type="video/webm" />
  <source src={CARE_VIDEO} type="video/mp4" />
</video>



위와 같이 작성하고 나니 한가지 문제가 생겼습니다.

 

Video를 실제로 load하고 실행하기 전까지는 넓은 영역이 잡히다가 Video가 나타나면서 해당 영역이 좁혀졌고, 그 덕분에 기존의 UI들이 밀리는 현상이 발생했습니다.

 

이러한 현상은 Layout Shift라고 합니다. ( https://web.dev/articles/cls?hl=ko )

 

이 문제에 대해서는 이미 알고 있었고, 이를 해결하기 위해 추가적인 작업을 진행했습니다.

 

Layout Shift 개선하기

 

일반적으로 이러한 Layout Shift 현상은 Image나 Video를 활용할 때 자주 마주할 수 있는데, 이는 반응형 웹을 구현하는 과정에서 여러 Viewport에 대응하기 위해 Image와 Video의 width height를 직접 명시하지 않고, 자동으로 영역을 계산하도록 width: 100%, height: auto와 같은 방식을 사용하는 경우에 자주 볼 수 있는데, 이는 source 파일이 로드되기 전까지는 정확한 높이를 알 수 없어 로드 전과 로드 후의 점유 영역 차이로 인해 이외의 UI요소들이 이동하는 상황이 나타나게 됩니다.

 

Layout Shift를 개선하는 방법은 여러 가지 방법들이 있지만 자주 사용하는 두 가지 방법을 고려해보았습니다.

 

  1. aspect-ratio 사용
  2. padding-top 사용


첫번째 방법인 aspect-ratio란 해당 상황에 정확하게 들어맞는 CSS 속성으로, width와 height의 비율을 지정하여 종횡비를 맞춰주는 속성입니다. 이 방법을 사용하면 편하게 해결할 수 있었지만, 한가지 문제가 있었습니다.
저희 회사에서 제공하는 서비스는 iOS Safari 14버전도 지원하도록 개발하고 있기 때문에 항상 크로스 브라우징 이슈가 있을지 확인해야 하는데, aspect-ratio는 아쉽게도 iOS Safari 15버전부터 사용할 수 있습니다. 따라서 해당 옵션은 선택할 수 없었습니다.

( https://caniuse.com/?search=aspect-ratio )

두번째 방법은 padding-top을 사용하는 방식입니다. 해당 방식은 height값으로 지정되어있는 값 대비 height를 어느정도 보장해야하는지에 대한 비율을 작성하여 실제 Image 및 Video가 로드지 않은 상황에서도 영역을 보장할 수 있도록 해줍니다.


이 방식은 Wrapper의 Height를 padding-top을 통해 비율을 지정해주고 position을 relative로 설정, 하단에 들어올 Asset영역의 position을 absolute, top, left를 0으로 부여한 뒤 width, height를 모두 100%로 지정하여 Wrapper가 리소스 로드 전에 미리 차지해둔 padding-top만큼의 영역을 Asset의 리소스 로드 이후 그대로 덮어버리는 원리입니다.

또한 위 방식은 리소스 로드 이전의 영역만 잡아준 것이기 때문에 실제 영상이 실행되는 시점부터 영역을 덮어주어야 합니다.

해당 시점을 잡아내기 위해 Video 태그의 메서드 중 하나인 onLoadData 를 사용할 수 있으나 해당 이벤트가 발생하는 시점이 Video 태그의 경우 전체 데이터가 로드된 것이 아닐 수 있기때문에 완벽하게 Layout Shift 를 방지하기 위해서는 실제로 영상이 실행되는 시점에 Layout Shift 를 방지하기 위해 만들어둔 영역을 덮을 수 있도록 해야했습니다.

이를 위한 Video 태그의 메서드가 하나 더 있는데, 이는 onPlay 메서드입니다. 위의 이유 뿐 아니라 특히 autoPlay 의 경우 Android 에서는 실행되기 전에 Play 버튼이 가운데 나타났다가 실행되면서 사라지기때문에 자동실행되는 영상을 바로 제공하고자 하는 의도와 다르게 어색한 경험을 제공할 수 있습니다.

또한 autoPlay 옵션을 사용중이고 onPlay 메서드를 통해 실행 시점을 캐치하더라도 여러가지 요인으로 인해 autoPlay 가 데이터가 로드된 이후에 실행을 시켜주는 것을 보장할 수 없습니다. 따라서 두가지 메서드를 모두 활용하여 Layout Shift 를 방지하고자 했습니다.

 

const [isLoaded, setIsLoaded] = useState(false)
const [isStarted, setIsStarted] = useState(false)

const canShowVideo = isLoaded && isStarted

return (
  <Wrapper>
    {!canShowVideo && (
      <div>
        <Skeleton />
      </div>
    )}

    <video
      width="100%"
      controls={false}
      autoPlay
      muted
      loop
      playsInline
      style={{ opacity: canShowVideo ? 1 : 0 }}
      onLoadedData={() => setIsLoaded(true)}
      onPlay={() => setIsStarted(true)}
    >
	  <source src={CARE_VIDEO} type="video/webm" />
	  <source src={CARE_VIDEO} type="video/mp4" />
    </video>
  </Wrapper>
)

const Wrapper = styled.div`
  position: relative;
  width: 100%;
  height: 100%;
  padding-top: calc(100% * 0.3894);

  & > div {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  & video {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
`




위와 같이 적용해주면 비디오에 대한 렌더링이 완료되기 전에도 Layout Shift를 방지하면서 영상이 시작되기 전까지도 Skeleton UI를 보여주어 시작 전까지 사용자에게 더 나은 경험을 제공해줄 수 있게 되었습니다.


여러가지 고려사항을 전부 해결한 후 빠르게 배포한 뒤에 안심하고 있던 도중, 당일 오후 사내에서 일부 직원분들에게는 영상이 자동실행되지 않는다는 제보를 받았습니다. (분명 테스트할때에는 전혀 없던 경우인데 말이죠 ..)

 

iOS 에서의 자동재생 이슈



일단 운영환경에서 발생하는 문제였기 때문에 원인을 빠르게 찾아보기로 했습니다. 문제의 원인을 모를 뿐 현상은 명확하여 바로 구글링에 들어갔습니다.

'iOS video autoplay dont work' 로 검색하니 가장 최상단 stack overflow에 유사한 글이 있어 확인해보았는데요, 전혀 생각도 못 한 댓글을 발견했습니다.

해당 댓글은 아래와 같았습니다.

 

iOS 10+ allow video autoplay inline. but you have to turn off "Low power mode" on your iPhone.

 


해당 내용은 과거 글이기때문에 앞부분은 크게 의미가 있지 않았지만 뒷 부분의 내용을 보면 iOS의 저전력모드를 사용중이라면 해당 옵션을 off 해야한다는 내용이었습니다.

기존에는 생각조차 해본적 없는 저전력모드의 케이스에 대해 실마리를 얻었고, iOS 디바이스에서 저전력모드가 켜져있다면 아래와 같은 제약이 있음을 발견했습니다.

 

  • CPU Throttling (60% 제한)
  • Background Application Refresh 제한
  • WebView Video play제한
  • auto-download 제한
  • GPU performance 저하 (requestAnimationframe등의 효과 제한)
  • 화면 밝기 저하

이후 실제로 제보해주신 사내 인원들을 대상으로 저전력 모드의 사용 여부를 문의드렸고, 실제로 영상 재생이 안 된다고 제보해주신 인원 모두 찾아보니 제보해주신 모든 인원은 아이폰 사용자 및 저전력모드를 사용 중이셨습니다.

 


해당 이슈를 해결하기위해 여러 가지 방면으로 시도를 해보았지만, 아무래도 script를 통한 play는 막혀있는지 어떤 방법으로도 실행되지 않았습니다. (테스트 도중 화면 렌더 후 첫 터치 시점에 event를 걸어 실행하는 방법을 사용해보았을 때는 정상 동작했던 것을 보아 event의 isTrusted를 체크하는 것이 아닐까 생각합니다.)


따라서 어쩔 수 없이 용량은 크지만 두 번째 방법인 Lottie를 활용하는 방법을 사용해보기로 했습니다.

 

 

Lottie를 활용한 영상 제공 및 Dynamic Import를 활용한 성능 최적화

 


Lottie 역시 실행이 어려운 방식은 아닙니다.

이는 실행을 위한 라이브러리가 잘 구축되어있어, lottie-react와 같은 라이브러리를 설치한 후 컴포넌트를 선언 및 해당 Lottie를 주입해주면 무난하게 실행할 수 있는데요, 여기서 한가지 걸리는 점은 용량이 크다는 점이었습니다.
Lottie가 존재하지 않더라도 한 페이지에 서빙하는 데이터가 많은 지면이었기 때문에 이러한 요소로 성능저하를 일으킬까 우려되었습니다.

따라서 이러한 요소로 인해 초기 로딩에 영향을 미치지 않기 위해 사용한 방법은 React의 Dynamic Import를 사용하기로 했습니다.

Dynamic Import특정 컴포넌트를 별도의 파일로 분리하여 사용자가 해당 코드 조각이 필요할 때만 불러오도록 하는 방식입니다. 따라서 이 방식을 사용한다면 페이지 초기 로딩 시에는 5MB의 Lottie 파일이 로딩되지 않아 큰 용량의 Lottie파일은 초기 로딩속도에 영향을 주지 않게 됩니다.

다만 이렇게 Dynamic Import를 사용할 때는 해당 컴포넌트가 필요한 시점에 렌더링 되기 전까지는 Fallback UI를 보여주어야하기때문에 아래와 같이 Suspense를 함께 사용해야 합니다.

 

const LottieComponent = lazy(() => import('@components/LottieComponent'))

return (
  <Suspense fallback={<FallbackUI />}>
    <LottieComponent />
  </Suspense>
)


그렇다면 실제로 초기로딩 시간이 단축되었을까요?

맞습니다. 아래와 같이 실제로 Dynamic Import를 하기 전 Network를 Slow 3G로 바꾸어 테스트해보았을 때, LCP까지 걸리는 시간이 대략 20~21초가 걸렸다면 Dynamic Import를 적용한 후 LCP까지 걸리는 시간은 대략 16~17초까지 내려갔습니다.

이렇게까지 적용하고 나니 결국 맨 처음부터 적용하고자 했던 Video를 정상적으로 제공하면서도 다양한 요소로 인해 사용자의 경험을 해치는 일을 최소화할 수 있게 되었습니다.

 

Dynamic Import를 적용하기 전 LCP까지 약 20~21초가 걸리던 시점

 

 

Dynamic Import를 적용한 후 LCP까지 16~17초가 걸리던 시점



 

운영하던 도중 발견한 추가 이슈사항 (2024.4.17 추가)

 

위와 같은 방식으로 잘 운영하던 중 한가지 문제가 발견되었습니다. 해당 방식은 가장 사용자들이 공유하기 링크를 많이 주고받는 카카오톡을 기준으로 테스트를 진행했었는데요, iOS에서는 해당 링크를 클릭할 때, 카카오톡 내부 브라우저를 통해 링크를 열어주고, 앱으로의 이동이 동작하여 앱이 켜지면 기존에 링크를 실행했던 카카오톡 내부 브라우저를 닫아줍니다. 따라서 카카오톡 채팅창으로 돌아오면 대화를 나누던 화면으로 되어있는 것이지요. 하지만 AOS에서는 다르게 동작합니다. AOS 카카오톡에서는 대화창에 있는 링크를 클릭할 때, 동일하게 카카오톡 내부 브라우저를 통해 링크를 열어준 다음에도 카카오톡 내부 브라우저를 닫아주지 않습니다. 그러면서도 다른 앱으로 갔다가 돌아오면 내부 브라우저는 그대로 켜져있는 상태로 내용만 다시한번 새로고침을 해줍니다. 이렇게 될 경우 기존에 리다이렉트를 위해 최초 렌더 시 걸어준 롱링크 실행동작이 다시 실행되어 의도치 않은 동작을 불러옵니다.

 

이러한 이슈는 고민끝에 다음과 같이 해결했습니다.

 

내부 브라우저는 동일하게 켜져있더라도 브라우저 자체가 종료된 것은 아니기에 세션은 동일하다고 판단하였고, 최초 진입 시에 세션 스토리지에 특정 flag를 저장해주고, 이에 대한 값이 있다면 더이상 리다이렉트를 시켜주지 않는 로직을 넣어주었습니다.

 

const registeredSessionValue = getSessionStorageValue('bridge-session')

useEffect(() => {
  if (registeredSessionValue) return

  setSessionStorageValue('bridge-session', 'true')
  
  const redirectUrl = `${LONG_LINK}`

  window.location.href = redirectUrl
}, [])

 

 

실제로 위와 같이 로직을 넣어준 이후에는 AOS 카카오톡에서의 링크를 클릭할 때 내부 브라우저가 종료되지 않고 남아있더라도 다시 돌아왔을 때에는 동일한 세션으로 인지되고 세션 스토리지로부터 해당 flag값을 가지고올 수 있었기때문에 해당 이슈가 재발하지 않았습니다.

 

Re-Cap


생각보다 간단할 줄 알았던 영상제공에 있어서 문제가 되었고, 또한 고려했던 여러 가지 요소들에 대해 다시 한번 정리해보겠습니다.

 

  1. iOS 웹뷰환경 개발하는 상황에서 Video를 제공하려고 한다면, 저전력모드에서는 자동재생이 안 될 수 있다.
  2. 위 상황에서는 스크립트로 핸들링할 수 없기 때문에 반드시 사용해야 하는 상황이라면 다른 방법을 찾아보는 것이 좋다.
  3. 반응형으로 Image나 Video를 제공해야 하는 상황에서는 더 좋은 사용자 경험을 위해서는 Layout Shift를 방지하는 것이 좋다.
  4. React 프로젝트에서는 Dynamic Import 방식을 활용하여 초기 로딩을 최적화할 수 있다.



글에서 제공하고자 했던 정보가 많다 보니 각각의 포인트 개념을 깊게는 짚어볼 수 없었는데, 모바일 내 웹뷰를 개발하는 경우 여러 가지 고려할 점이 많고, 위와 같은 여러 가지 개념을 활용하면 더 좋은 사용자 경험을 제공하는 데에 도움이 될 수 있다는 점에서 정리하게 되었습니다. (특히 iOS 저전력 모드에서 위와 같은 문제를 겪은 것은 잊을 수 없을 것 같습니다.)

반응형