웹뷰 앱에서 네이티브 같은 전환 경험 만들기


우리는 모바일 앱을 웹뷰 기반으로 운영하고 있다.
웹뷰 기반 앱은 웹이랑 통합돼서 개발하기 편리하지만, 네이티브 앱 만큼의 경험을 제공하기는 어렵다.
특히 페이지 전환에서 네이티브 앱과 비교했을 때 체감이 많이 됐다.

우리 앱의 모바일 화면 하단에는 Footer Navigation이 있다.
Home, Chat 등 유저가 가장 자주 이동하는 핵심 페이지로 바로 갈 수 있는 버튼들이다.

문제는 이 Footer에 있는 페이지는 실제로 사용자가 가장 많이 클릭하는 버튼이었기 때문에, 단순한 UI가 아니라 앱의 속도를 체감하게 만드는 가장 중요한 요소라는 점이었다.

기존 구현은 Footer 버튼을 누르면 router.push로 해당 페이지로 이동하는 방식이다.
하지만 웹뷰 환경에서 웹 특성 상 라우팅이 일어나서 약간의 대기 시간이 있었다.

버튼을 누르면 살짝 멈춘 뒤 페이지가 전환되는 사용자 경험은 네이티브 앱과 비교해서 거슬리는 동작이었고, 이 부분만큼은 반드시 네이티브처럼 부드럽게 동작하게 만들고 싶었다.


이 문제는 단순히 성능이 느리다기보다는, 라우팅이라는 개념 자체가 네트워크와 렌더링을 동반하면서 지연을 유발하는 어쩔 수 없는 문제였다.

그 사이에 Loading UI를 넣어서 즉시 뭔가 반응이 일어나면 앱이 멈춘 느낌을 덜 줄 수도 있을텐데, AWS Amplify Next.js에서는 Streaming이 지원되지 않아 페이지 이동 중 로딩 상태를 자연스럽게 보여주기도 어려웠다.

Ref : https://docs.aws.amazon.com/amplify/latest/userguide/ssr-amplify-support.html


1. 첫 번째 시도 (실패) – router.prefetch

“미리 페이지를 불러와두면, 버튼을 눌렀을 때 즉시 이동할 수 있지 않을까?”
라고 생각하고, Global하게 Footer에 있는 페이지를 대상으로 Prefetch를 넣어서 미리 로드하고자 하였다.

하지만 Footer에 연결된 페이지 중에는 SSR 페이지도 섞여 있었고, 어떤 경우에는 이전 상태를 다시 가져오는 문제가 발생했다.
그래서 페이지 구조가 복잡해질수록 예측하기 어려운 케이스가 늘어날 것을 예상하여 이 방법은 폐기하게 되었다.


2. 두 번째 시도 (실패) : SSG + Client Data Loading

다음으로 시도한 방법은 페이지 구조를 바꾸는 것이었다.
Footer로 이동하는 페이지들은 SSG로 페이지 껍데기만 먼저 생성해두고 페이지 진입은 최대한 빠르게하는 것이었다.

SSR 방식을 포기하고 페이지 진입 후에는 Skeleton으로 실제 데이터는 클라이언트에서 로드하는 방식을 적용했다.
그리고 여기에 prefetch까지 함께 적용했다.

이 방식 적용 후 페이지 전환 속도는 눈에 띄게 빨라졌고 체감 성능도 꽤 좋아졌다.
하지만 처음 한 번 눌렀을 때는 여전히 아주 미세한 멈춤이 있었고, Streaming이 안 되는 환경에서는 로딩 UI도 없이 앱이 잠깐 멈춘 것처럼 느껴지는 순간이 남아 있었다.

나쁘지 않은 결과였지만 “웹뷰 앱을 네이티브처럼”이라는 목표에는 아직 부족하다고 느꼈다.


3. 세 번째 시도 (성공) : Footer 페이지를 SPA처럼 관리하기

“Footer에 있는 페이지는 라우팅하지 말자.” 라는 방향으로 바꾸게 되었다.

Footer에 연결된 페이지들은 하나의 페이지 안에서 관리되어 최초 진입 시 기본 페이지를 먼저 보여주고 나머지 페이지 컴포넌트는 백그라운드에서 로드하는 방식으로 바꾸었다.

이후 전환은 router.push가 아닌 state 기반으로 보여주는 페이지만 변경하였다.
즉, 이미 로드된 컴포넌트를 상태에 따라 교체해서 보여주는 완전한 SPA 방식으로 동작하게 바꾸었다.

이렇게 하면 라우팅이 발생하지 않고 네트워크 요청도 없으며 웹뷰 환경에서도 즉각적인 전환이 가능하다.

다만 URL은 반드시 바뀌어야 하기 때문에 화면 전환 시 실제 라우팅은 하지 않지만 window.history.pushState를 사용해 주소창의 URL은 함께 변경되도록 처리했다.

아래와 같이 Footer에 존재하는 모든 페이지를 한 컴포넌트에 몰아놓고 로드하도록 하였다.
중요한 점은 Conditional 하게 보이는 페이지만 렌더링 하게 되면 페이지가 바뀌었을 때 컴포넌트가 로드되게 되어서 페이지가 스위칭 되었을 때 데이터가 로드되게 된다.

따라서 안 보이는 페이지는 display: none으로 감추기만 하는 방식으로 컴포넌트 데이터는 백그라운드에서 로드되도록 하였다.

TSX
export default function FooterPages() {
  return (
    <div>
      <SlideWrapper index={0}>
        <MainPage />
      </SlideWrapper>
      <SlideWrapper index={1}>
        <ChatListPage />
      </SlideWrapper>
      <SlideWrapper index={2}>
        <NotificationPage />
      </SlideWrapper>
      <SlideWrapper index={3}>
        <UserProfilePage />
      </SlideWrapper>
    </div>
  )
}

이 방식을 적용하니 Footer 버튼을 누르는 순간 멈춤 없이 즉시 화면이 전환되었다.
또한 애니메이션까지 자연스럽게 이어지면서 네이티브 앱처럼 부드럽게 스위칭을 할 수 있었다.

[PC 테스트 영상]

[모바일 앱 테스트 영상]


옛날에 스스로 “웹뷰 기반으로 앱을 개발하면 웹이랑 앱 둘 다 커버할 수 있는데 왜 큰 서비스에서는 네이티브 앱 개발을 따로 하는 거지?” 라는 질문을 한 적이 있었다.

그때는 단순히 기술 스택의 차이 정도로 생각했지만, 웹뷰 기반 앱을 직접 개발하고 운영하면서 그 이유를 직접 체감하게 되었다.
네이티브 앱에는 웹뷰로는 한계가 있는 네이티브만이 제공할 수 있는 경험이 있다.

그래서 웹뷰로 네이티브 같은 경험을 만든다는 것은 한계의 원인을 파악하고 그 간극을 줄여나가는 최적화 문제라고 생각한다.

가장 많이 눌리는 영역을 기준으로 구조를 다시 생각하고, 기술적인 정합성보다 체감 경험을 우선한 선택을 했을 때, 웹뷰 앱도 충분히 앱 같다는 느낌에 가까워질 수 있다는 걸 확실히 느꼈다.

  • 하지만 웹뷰 앱 전체를 네이티브처럼 만들기란 너무나 어려운 일이고, 아직도 너무 개선해야 할 부분이 많다고 생각한다.

그래도 실제로도 상당히 체감이 많이 되는 개선이었기 때문에 의미 있는 프로젝트라고 생각한다.

답글 남기기

Dalmeng's Footprints에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기