웹뷰 서버사이드에서 앱 정보 받아오기

웹뷰 서버사이드에서 앱 정보 받아오기

웹뷰는 네이티브 앱에 내재되어 있는 웹 브라우저로 쉽게 말해, 앱(Android, iOS 등) 내에서 웹을 띄운 것이다. 앱에 비해 배포, 롤백 등이 자유로워서 현대 프로덕트 개발에 웹뷰는 굉장히 많이 사용된다. (웹뷰라고 기존 웹이랑 크게 다를 건 없다. 주소창, 새로고침, 즐겨찾기와 같은 기능은 없고 단순히 웹페이지만 보여주는 페이지다.)

As-is

내가 일하는 회사에서도 마찬가지로 대부분의 프론트개발을 웹뷰(Next.js)로 한다. 그리고 앱에서 웹뷰로 필요한 정보를 넘겨줘야 할 때가 있다. 여러 가지 방법이 있겠지만, 우리는 현재 앱에서 웹뷰를 띄울 때 아래와 같이 브라우저의 window 객체에 필요한 정보를 넣고 있다.
웹뷰에서는 window 객체를 참조해서 필요한 데이터를 가져다 사용하고 있고 별다른 이상은 없다. 하지만 할당된 데이터를 useEffect를 통해 컴포넌트가 마운트 될 때 불러오니 간혹 타이밍 문제로 마운트가 완료되었을 때, window 객체에 데이터가 없는 경우가 있다. 또한 앱 쪽 문제로 window 객체에 데이터 자체가 할당되지 않는 경우도 있다. (많지는 않고 하루에 30여 명 정도고 그마저도 페이지를 새로고침하면 다시 정상 작동된다.)
에러 수가 많지 않지만, 어떤 프로덕트의 UX 퍼널이 긴 경우 프로덕트의 프로세스가 끝날 때 즈음 위와 같은 문제로 페이지를 새로고침하게 된다면 사용자의 경험은 매우 좋지 않을 것이다.
우선 _app.tsx, 특정페이지.tsx 에서 마운트 될 때 console.logwindow 객체에 있는 정보를 찍어보았다.
appVersion, userData, isApp, platform 은 각각의 커스텀 훅 내에서 상태로 관리되고 있다.
_app.tsx 결과
특정페이지.tsx 결과
useUserInfouseEffect에서 데이터를 가져오고 있고, useIsApp, usePlatform 은 jotai에서 데이터를 가져오고 있다. 리렌더 전에는 해당 상태들이 window 객체에 있는 값을 할당받지 않았으니 당연히 초기 값인 null, undefined 가 할당되어 있는 것을 알 수 있고 해당 훅들로 인해 리렌더가 되며 window 객체에 있는 데이터가 상태에 잘 할당된 것을 확인할 수 있다.
하지만 위 상황은 아래와 같은 문제가 있다.
  • window 객체에 있는 값을 가져오는 과정에서 불필요한 리렌더가 한번 더 발생
  • userData, isApp, platform 등에 데이터가 존재할 때까지 대기하는 페이지의 경우에 사용자는 로딩이나 빈화면을 더 오래 봐야 함.
  • 타이밍 문제로, 마운트가 완료되었지만 window 객체에 데이터가 없는 경우, 또는 앱 쪽 문제로 window 객체에 데이터 자체가 할당되지 않는 경우에 페이지는 작동하지 않음
부가적으로, platform 값은 아래처럼 jotai atom을 초기화할 때 window 객체를 참조하는 함수를 호출해 반환 값을 할당하고 있다. Andriod 라는 값이 리렌더 전에도 찍히는 것을 보니 첫 마운트 때 이미 정상적으로 할당된 것으로 보인다. (하지만 특정 상황(타이밍 이슈)에서는 platform 값도 문제 있을 수 있다.)

To-be

내가 원하는 것은 app.tsx에서 리렌더 전에 useUserInfo, useIsApp, usePlatform 의 반환 값을 출력했을 때 앱에서 전달한 정보들이 온전하게 들어있는 것이다.
마침 최근에 Andriod, iOS 앱이 강제 업데이트를 했고 해당 버전에서는 웹뷰를 띄울 때 request의 header에 아래와 같은 정보를 넣어주기로 협의했다.
브라우저의 window 객체에 넣어주던 정보와 같다. 무엇을 위해 같은 정보를 웹뷰의 header에 또 넣었을까? 바로 서버사이드에서 앱 정보들을 가져다 활용할 수 있는 것이다.
_app.tsx
위와 같이 page 라우터에서 getInitialProps 를 사용하면 header의 정보를 가져올 수 있다.
if 절 조건이 있는 데 간단하게 살펴보면 ctx.req.url?.startsWith('/_next') 는 클라이언트 사이드에서 라우팅 했을 때를 의미한다. 웹뷰를 띄운 후, 클릭 등으로 페이지를 이동할 때마다 header의 정보에 접근할 필요는 없다. 웹뷰를 처음 띄울 때 한 번만 request header에 접근해 정보를 가져오면 되어서 위 조건을 추가했다.
추가로 getInitialProps_app.tsx 에 추가하면 모든 페이지가 서버 사이드에서 위 로직을 한 번 수행한다. 내가 담당하는 프로젝트는 SSG(Static Site Generation)를 현재 사용하지 않고 95% 이상의 페이지가 웹뷰로 이루어져서 추가했다. 물론 혹시 모를 상황을 대비해 Devops분께 말해서 k8s pod를 늘려놓았다. (각자 프로젝트의 상황에 따라 선택하면 될 듯하다.)
위 코드를 통해 클라이언트 props 로 user 정보를 넘길 수 있다. 이 user 데이터를 전역 클라이언트 상태 (atom)에 넣고 컴포넌트에서 필요할 때 atom에 있는 user 데이터를 사용한다.
그리고 위에서 설명한 타이밍 이슈로 window 객체에 데이터가 없는 경우에 getInitialProps 에서 가져온 데이터를 할당하면 타이밍 이슈를 해결할 수 있다.
그림 설명
그림 설명
다음은 불필요한 리렌더가 한번 더 발생하는 현상을 해결해 보자. 리렌더 현상을 해결하면 사용자가 로딩이나 빈화면을 더 오래 보는 문제도 자연스레 해결된다. (_app.tsx 에서 리렌더가 일으킴으로서 필요한 isApp, userData 를 할당하기 때문)
jotai에는 서버사이드에서 가져온 데이터를 클라이언트 상태로 hydrate해주는 useHydrateAtoms 이 있는데 해당 훅을 사용해 문제를 해결해 보자!
useHydrateAtoms 는 jotai에 위와 같이 작성되어 있다. store를 가져와서, 없다면 새로 생성한다. 그리고 데이터(atom)가 없다면 해당 데이터를 넣는다. (옵션에 따라 데이터가 존재해도 넣을 수 있도록 설정할 수 있다.)
_app.tsx 최상단에서 useHydrateAtoms 를 사용해 클라이언트 전역 상태에 할당해 주었다. isAppAtom 은 user의 id가 존재하면 웹뷰 환경임을 보장할 수 있어서 true, 아닌 경우 false 를 할당하고 다른 atom의 로직도 유사하다. 그럼 값을 다시 출력해보자.
_app.tsx 결과
서버사이드에서 받은 데이터를 hydrate하고 웹뷰 _app.tsx 최상단에서 출력해 보니 불필요한 리렌더가 사라졌고 값도 온전하게 다 들어있다. 이로써 as-is에 존재하던 이슈들을 다 해결했다.
마지막으로 방어로직으로 jotai onMount를 추가했다.
jotai의 onMount는 해당 atom를 사용하는 컴포넌트가 Mount 될 때 실행되는 메소드라고 생각하면 된다. 컴포넌트가 Mount 될 때 platform 이나 웹뷰 여부가 바뀔리는 없지만 예기치 못한 상황이 있을 수 있으니 넣어놨고 어차피 같은 값이면 리렌더도 안되니 방어 로직으로 넣어도 무방하다고 생각한다.
notion image
위 사진처럼 window 객체에 필요한 데이터가 없다면 getInitialProps 에서 가져온 데이터를 할당하는 함수도 추가했다. (간단한 할당 함수라 생략한다.)

마지막으로 한 눈에 보는 결과

_app.tsx 결과 (변경 전)
_app.tsx 결과 (변경 후)
위처럼 불필요한 리렌더링이 사라졌고 필요한 데이터가 있을 때까지 대기하는 시간이 줄었다. 얼마나 줄었는지는 배포 후 모니터링을 통해 확인해 봐야겠다.
그리고 서버사이드에서 앱 데이터 처리가 가능해졌으니 앱 토큰 기반으로 API를 요청하는 것도 가능해졌다. 이것도 한번 적용해 봐야겠다!

Reference