네이티브 앱 → 리액트 네이티브 앱 전환 그리고 1년 후

velopert·2021년 1월 7일
17
post-thumbnail

2020년 2월, 애니메이션 스트리밍 서비스 라프텔에서는 기존에 Android 와 iOS 각각 따로 구현되어 있던 애플리케이션을 리액트 네이티브를 통해 개발을 통합하여 할 수 있도록 리빌드를 했습니다. 이제 릴리즈 후 거의 1년이 되어가는 시점에서 그동안 저희가 겪었던 것, 배웠던 것 그리고 느꼈던 것들을 여러분께 이 포스트를 통하여 공유해보고자 합니다.

이 포스트는 리디의 기술 블로그에도 게재되었습니다.

왜 리액트 네이티브를 선택했을까?

서비스의 개선

라프텔은 구독형 서비스 플랫폼입니다. 사용자가 멤버십을 구독하면 라프텔에서 제공하는 애니메이션 중 멤버십 포함 작품들을 무제한으로 감상할 수 있지요. 저희 팀에게 주어진 가장 중요한 미션 중 하나는 사용자가 서비스에 유입된 이후 멤버십 구독을 할때까지의 흐름을 개선하는 것입니다. 멤버십 구독 이후 재결제율 또한 매우 중요한 지표지요.

이 지표들을 개선하기 위해서 저희가 할 수 있는 것은 무엇이 있을까요? 사용자가 좋아할 만한 새로운 기능들을 추가하는 것도 중요하고요, 단순히 상품의 설명이나 UI의 재배치도 이 지표에 영향을 끼치기도 한답니다.

최적의 성과에 도달하기 위해서는 다양한 테스트를 거쳐야 하는데요, 저희는 리액트 네이티브를 사용했을 때 이러한 테스트를 진행할 때 매우 효율적일 것이라 생각했습니다.

예를 들어서 A/B 테스트를 진행한다면, 만약 Android 와 iOS 개발이 따로따로 이뤄져 있는 경우 테스트를 위한 코드를 두 번씩 작성해야 하고, 관련 QA도 두 번씩 해야 하고, 테스트를 마친 후 관련 코드를 제거하는 작업도 두 번씩 해야 합니다. 반면 리액트 네이티브를 사용하는 경우엔 해당 작업들을 한 번씩만 하면 되지요.

추가적으로, 라프텔 사업부에서는 서비스에 앞으로 도입될 기능이 무수히 많을 것이라 예상하고 있습니다. 만약 라프텔이 현재 서비스 중인 앱이 완성이 됐다고 판단하고 단순히 유지 보수만을 계획하고 있다면 리액트 네이티브로의 전환은 불필요했을 것이고, 부적합했을 것입니다. 하지만, 앞으로 서비스를 더욱 고도화 시켜야 하고 새로 구현할 기능들이 많기 때문에 더 늦기 전에 빨리 리액트 네이티브로의 전환을 해야겠다고 생각했습니다.

저희는 리액트 네이티브를 사용하면 Android와 iOS 앱을 동시에 개발할 수 있기 때문에 더욱 높은 생산성을 보일 것이라 믿었습니다. 따라서 전환을 하는 작업은 리소스를 많이 소모하겠지만 중장기적으로 봤을 때 서비스 개선을 함에 있어서 훨씬 유리하기에 이는 시간 및 개발 인력을 투자하기에 충분한 가치가 있다고 판단하였습니다.

개발 조직 구조

기존에는 클라이언트 진영에서 각 플랫폼을 담당하는 개발자가 1명씩 있었고, 서버 개발자는 2명이 있었습니다.

2019년 초까지 개발 조직이 위와 같은 체제로 운영되어 왔었는데요, 이러한 구조는 서비스를 개발해나가는데 큰 문제는 없었지만 아무래도 서비스의 규모가 커져 가면서 한계가 보이기 시작했었습니다. 각 플랫폼 당 1명씩 밖에 없으니 불편한 점들도 있었죠. 예를 들자면, 중요한 기간에 휴가를 내기 힘들 수도 있고, 서비스에 갑자기 문제가 발생하면 담당자 외엔 처리를 하기 힘든 상황이 간혹 발생하기도 했습니다.

그리고, 업무 처리에 있어서도 한계가 있었습니다. 예를 들어서 특정 기능을 개발한다고 하면, 동시에 다른 기능 개발을 진행하기 힘들고 그저 할 일이 Queue처럼 쌓일 뿐이었습니다.

현 상황에선 클라이언트 개발의 생산성을 높이기 위해서는 각 플랫폼당 개발 인력을 늘릴 필요가 있었습니다. 즉 Android, iOS, 웹 프런트엔드 총 3명씩 충원해야 했죠.

하지만, 채용 과정이 순탄치 않았습니다. Android의 경우 지원자 중에서 저희가 원하는 인재를 찾기가 힘들었고 iOS의 경우엔 지원자 수 자체가 매우 적었습니다 (아마 저희가 Kotlin과 Swift를 사용 중인 것도 한몫했던 것 같습니다).

모바일 앱 개발자 채용이 어려웠던 것이 리액트 네이티브로의 전환에 어느 정도 영향이 있었습니다.

만약 리액트 네이티브로 전환하는 경우엔 채용 관련 문제는 쉽게 해결될 수 있었습니다. 사내에 Android와 iOS를 잘 다루는 동료들이 있기 때문에, 저희는 그저 JavaScript와 리액트를 잘 다루는 개발자를 찾기만 하면 됐었습니다. 그러한 개발자들은 국내에서 더욱 찾기 쉽기 때문에 유리할 것이라 생각했고, 실제로도 채용이 잘 진행됐었답니다.

리액트 네이티브로 전환을 하면서 Android 개발자, iOS 개발자, 웹 프런트엔드 개발자들의 직무를 하나의 이름으로 통일 시키게 됐습니다. "프런트엔드 개발자"라는 이름으로 말이죠. 이렇게 라프텔의 프런트엔드 개발자는 리액트 네이티브 앱과 웹 앱을 모두 담당하게 됐습니다.

리액트 네이티브로 전환을 하면서 저희는 업무 방식도 변경을 하고 싶었습니다. 기존에는 새로운 기능 개발을 하게 된다면 일단 모든 개발자들이 회의에 소집됐었습니다. 특정 기능을 iOS, Android, 웹에 모두 도입을 해야 하니까 관련 개발자들이 모두 참여를 해야 했는데 이게 꽤나 비효율적이라고 느꼈습니다.

저희는 개발 조직 내부에서 기능별로 또 다른 세부 조직을 만들고 싶었었고 리액트 네이티브를 선택하게 되면서 이러한 시스템의 도입을 더욱 앞당길 수 있었습니다.

2019~2020년에는 개발자들이 더 충원되어 개발 조직이 다음과 같이 구성됐는데요:

새로운 기능을 개발하게 될 때는 디자이너 1명, 프런트엔드 개발자 1명, 백엔드 개발자 1명씩 있는 스쿼드를 결성합니다. 프런트엔드 개발자는 해당 기능에 대하여 모바일 앱과, 웹에 구현을 하게 되는데요, 규모가 큰 경우에는 프런트엔드 개발자를 한 명 더 참여시키기도 합니다. 그리고 회의를 진행할 때에는 해당 스쿼드원 끼리만 회의를 진행합니다.

이러한 시스템으로 개발을 하니까 서로 다른 피쳐들이 동시에 개발될 수 있어서 서비스의 개선 속도가 과거보다 훨씬 빨라졌음을 체감할 수 있었습니다. 무엇보다 현재 자신이 개발하고 있는 것과 유관한 회의에만 참여를 하다 보니 업무에 집중할 수 있는 더 많이 확보할 수 있어서 너무 좋았습니다. 만약 리액트 네이티브로 전환을 하지 않았었더라면, 훨씬 많은 개발자를 충원해야 지금과 같은 구조로 일을 할 수 있었을 것입니다.

개발 과정

사전 조사

리액트 네이티브 도입에 관련된 이야기가 나왔을 때, 개발 조직 내에 리액트 네이티브를 깊게 사용한 유경험자는 따로 없었습니다. 라프텔의 조직장님의 경우 2016년경에 iOS 로 라프텔 앱을 개발한 적이 있긴 했으나 그때는 너무 옛날이기도 하고, 서비스에 비즈니스 모델도 붙기 전이여서 기능도 지금보다 적었던 상황이었습니다.

이 기술을 도입해보기 전에 현재의 라프텔 앱에 있는 모든 기능을 리액트 네이티브로 구현이 가능한지 알아볼 필요가 있었습니다.

리액트 네이티브의 UI 표현에 대해서는 전혀 걱정이 없었지만, 우려했던 부분은 비디오 스트리밍 플레이어였습니다. 단순히 영상을 재생만 하는 게 아니라, DRM(저작권 보호 장치)도 적용해야 됐기에, 이게 리액트 네이티브 환경에서도 구현이 가능한지 알아볼 필요가 있었습니다.

리액트 네이티브를 전혀 다뤄보지 않은 시점에서 2주 정도 사용해보면서 앞으로 라프텔 서비스를 개발할 때 문제가 될 만한 게 없을지 알아보는 시간을 가졌었습니다. 리액트를 잘 숙지하고 있는 상태에서 진행을 하다 보니 큰 어려움 없이 적응할 수 있었습니다.

영상 재생에 문제가 없는 것을 검토한 이후, 리액트 네이티브로의 전환에 대한 결정을 확정하고 2019년 7월부터 본격적인 개발이 시작됐습니다.

프로젝트를 제로부터 리빌드

프로젝트를 리액트 네이티브로 전환하는 것은 여러 가지 방법이 있습니다.

  1. 처음부터 새로 만들기
  2. 네이티브 프로젝트에 리액트 네이티브 화면을 추가
  3. 새로 만든 리액트 네이티브 프로젝트에 기존의 네이티브 뷰 호환

저희는 이 중 첫번째 방법 **"처음부터 새로 만들기"**를 택하였습니다. 이러한 결정을 내린 이유는 여러가지가 있었습니다.

만약 기존 앱의 모든 기능을 유지한 상태로 리액트 네이티브로 새로운 기능들을 구현한다면 2번의 방식이 적합했을 수도 있었습니다. 하지만, 저희의 경우 기존 iOS 앱의 버전이 Android 앱과 대비하여 뒤쳐지고 있었습니다. 즉, Android에는 있지만 iOS에는 없던 기능들이 있었죠. 또는, Android엔 새로운 디자인이 적용되어 있고, iOS에는 옛날 디자인이 적용되어 있는 화면들이 다수 있었습니다. iOS 앱의 개선을 위하여 어차피 새로 만들어야 하는 UI들이 많았기 때문에 그냥 다 리액트 네이티브로 짜는 게 좋겠다고 판단을 했습니다.

추가적으로, 꼭 네이티브로만 구현되어야 하는 화면은 따로 없었습니다. 굳이 따지자면 영상 플레이어 쪽에서 네이티브 코드를 많이 사용하긴 하는데, 저희는 플레이어의 UI를 리액트 네이티브를 사용하여 Android 와 iOS끼리 통합을 시키고 싶었기 때문에 기존 플레이어의 네이티브 코드들을 다시 가져올 필요는 없었습니다.

그리고, 기존의 네이티브 코드들을 많이 유지한 상태로 전환을 진행했더라면 릴리즈 날짜를 앞당길 수 있었겠지만 그 대신에 추후 유지 보수 측면에서 어려워질 것이 크게 우려됐습니다. 그렇게 되면 결국엔 유지 보수를 위해서 Swift 코드도 수정하고, Kotlin 코드도 수정하고 또 JavaScript 코드도 수정하는 일이 발생하게 될 텐데, 프런트엔드 개발팀 내에 네이티브 코드를 수준 높게 다룰 수 있는 인원이 소수이기에 이 방식은 저희 팀에 적합하지 않았습니다.

저희는 기존의 네이티브 코드를 일부 사용하긴 했었지만 주로 UI와 관련 없는 코드들이었습니다.

프로젝트 초기 환경

새 리액트 네이티브 프로젝트는 React Native CLI 를 통하여 생성했습니다. 어차피 네이티브 코드 연동을 많이 해야 했기에 Expo 사용은 처음부터 고려하지도 않았습니다.

그리고, 리액트 네이티브 프로젝트에 정적 타입 시스템 Flow가 기본적으로 적용되어 있긴 하지만, 워낙 Flow 쪽 커뮤니티는 너무 작아서 이를 사용하지 않고 TypeScript를 초기부터 도입했습니다.

화면 관리 라이브러리는 React Navigation 을 사용했습니다. React Native Navigation도 후보중 하나였는데요, 해당 라이브러리는 기존 네이티브 앱에 리액트 네이티브를 적용하는 경우 더 적합해 보였고, 이를 사용 할 경우 화면 하나 하나가 완전히 다른 별개의 리액트 앱으로 취급되는 부분이 마음에 들지 않아서 사용하지 않았습니다.

전역 상태 관리는 리덕스와 Context API를 함께 사용했습니다.

릴리즈까지

리액트 네이티브 앱 개발이 시작되고 나서, 기존의 앱은 새 피쳐 개발이 중단되었습니다. 작은 Bug fix 및 배너 디자인 변경처럼 최소 공수로 구현 가능한 것들만 처리가 됐습니다.

저희는 iOS를 먼저 릴리즈하기로 결정했는데요, 그 이유는 이 글에서도 언급했었던 것처럼 iOS가 버전이 Android대비 뒤처져있는 상황이였기에 배포가 조금 더 급했습니다.

리액트 네이티브 앱의 첫 릴리즈까지 걸리는 기간은 저희가 예상했던 것보다 길었습니다. 저희의 예상은 11월 중에 모두 마무리가 되는 것이었는데요, 개발 자체는 12월쯤에 마무리가 됐었지만 QA 가 길어지면서 2월에 초에 드디어 릴리즈를 하게 됐습니다.

iOS를 릴리즈 했다고 해서 Android를 금방 릴리즈 할 수 있는 것은 아니었습니다. Android에서만 존재하는 기능들도 있어서 추가적으로 개발해야 하는 것 들도 있었고, 스타일이 의도한 대로 나오지 않거나, Android에서만 존재하는 성능 이슈 및 버그들이 은근히 존재해서 그런 것들을 고치느라 시간이 꽤나 걸렸습니다. iOS 릴리즈 후 Android 릴리즈까지는 2개월 정도의 시간이 걸렸습니다.

릴리즈 후

리액트 네이티브 앱을 릴리즈 후, QA 과정에서 잡아내지 못했던 버그들이 간혹 발견됐었습니다. 리액트 네이티브에서는 CodePush라는 기술을 사용하여 앱스토어 검수 없이 바로 배포할 수 있는데요, 이 기능 덕분에 서비스를 빠르게 안정화 시킬 수 있었습니다.

버저닝 규칙

위에서 언급한 것처럼 리액트 네이티브 앱은 CodePush로 업데이트할 수도 있고 스토어를 통해서 업데이트할 수 도 있습니다. CodePush의 경우엔 네이티브 의존성이 바뀌지 않았을 때에만 업데이트를 할 수 있으며, 만약 네이티브 쪽 코드가 업데이트된다면 스토어를 꼭 거쳐야 합니다.

이 때문에 저희는 버저닝 규칙을 새로 정립했습니다. major.minor.patch 에서, 이전에는 버그 패치나 간단한 기능 수정은 patch , 새로운 피쳐의 도입은 minor 그리고 프로젝트의 전체적인 리뉴얼의 경우 major 를 올렸었습니다. 이제는 더 이상 그렇게 하지 않고, JavaScript 코드만 업데이트됐을 땐 patch , 그리고 네이티브 코드가 변경될 때 minor  값을 올리기로 했습니다. major 의 경우엔 기존 규칙대로 유지했습니다.

리액트 네이티브의 장점

리액트 네이티브 사용의 가장 큰 장점은 역시나 JavaScript를 사용하여 Android 와 iOS의 네이티브 UI를 작성할 수 있다는 것이죠.

이 외에도 저희가 리액트 네이티브를 사용하면서 느꼈던 장점이 많았는데요, 그중에서 중요한 것들을 여러분께 공유해드리겠습니다.

프런트엔드 그룹 통합

이 글에서 이미 언급이 됐었는데요, 리액트 네이티브를 통하여 Android, iOS, Web, 세 플랫폼에 흩어진 개발자들을 하나의 그룹으로 통합시킬 수 있었던 것이 참 좋았습니다.

다른 플랫폼과 코드 공유

리액트 네이티브를 사용하면서 단순히 Android 와 iOS에서 코드를 공유할 수 있었을 뿐만 아니라, 저희 서비스에서 지원하는 웹, 그리고 스마트 TV 앱과도 코드를 공유할 수 있었습니다.

물론 웹 / 스마트 TV 앱과 공유할 수 있는 코드들은 한정적이긴 한데요, 그래도 충분히 유용하고 효율적이며, 유지 보수 측면에서도 매우 좋았습니다.

웹과 리액트 네이티브 프로젝트 모두 리덕스를 사용하기 때문에, 초반에는 리덕스 관련 코드를 재사용 할 수 있을 것이라 생각했습니다. 하지만 실제로 개발을 진행하다 보니 생각보다 어렵더군요, 왜냐하면 웹과 모바일에서 리덕스를 사용하는 개발 환경도 조금씩 달랐고, 웹과 모바일에서 다뤄야 하는 상태가 달랐기 때문입니다. 그래서 실질적으로 통합을 하기엔 어려웠습니다.

웹과 모바일에서 사용하는 리덕스 미들웨어도 달랐었습니다. 웹의 경우엔 redux-saga 그리고 모바일에서는 redux-observable을 사용했습니다. 모바일에서 redux-observable을 쓴 이유는 redux-saga의 경우 saga를 작성하는 과정에서 타입 유추가 안돼서 코드 내에서 직접 타입을 설정을 해줘야 하는 반면 redux-observable은 타입 유추가 아주 잘 되기 때문이었습니다. 웹에서 redux-observable로 넘어가는 것에 대해서도 고민 했었는데, redux-observable의 경우 서버 사이드 렌더링을 구현할 때 어려움이 있을 것 같아 전환할 수 없었습니다.

그래서 리덕스 관련 코드의 공유는 미루고, 정말 간단한 로직부터 코드 공유를 해보았습니다. 가장 처음 공유한 로직은 에피소드를 대여한 후 남은 대여 시간을 표기하는 함수였습니다.

라프텔의 에피소드 대여 잔여 시간을 표기하는 규칙은 다음과 같습니다.

72시간 이상 → 00일 남음

99일 초과시 99+일 남음

3시간 ~ 72시간 사이 → 00시간 남음

1시간 ~ 3시간 사이 → 00시간 00분 남음

1시간 미만 → 00분 남음

이 규칙이 과거에는 조금 달랐었고, TV 화면에서는 이 정보를 보여줄 수 있는 공간이 한정되어 있기 때문에, 이를 변경해달라는 기획팀의 요구 사항이 있었습니다. 그런데 이러한 로직을 웹에서도 작성하고, 모바일에서도 작성하고, TV에서도 작성을 하는 게 매우 비효율적이라고 느꼈습니다. 아무리 한번 구현을 하고 복사/붙여넣기를 한다고 해도, 나중에 또 이 로직이 변경되면 또 세 프로젝트를 열어서 수정을 해줘야 하니까요.

그래서, @laftel/common 이라는 라이브러리를 만들고 이 프로젝트에 해당 함수를 넣어서 npm에 배포했습니다. 그동안 플랫폼 간 코드 공유를 미루기만 해왔었는데요, 이렇게 간단한 함수 재사용으로 첫 번째 발자국을 만들었고 그렇게 2020년 3분기부터 본격적인 코드 공유 작업이 시작될 수 있었습니다.

저희는 스마트 TV 앱 개발을 하면서 API 요청 상태 관리를 위하여 React Query 라는 라이브러리를 사용했었습니다. 사용 후 만족도가 굉장히 높았고, 라이브러리의 완성도/인기도도 갈수록 높아져서 다른 프로젝트들에서도 사용하기로 결정했습니다. 그뿐만 아니라, 서버사이드 렌더링의 구현도 간단해서 웹 서비스에서 사용하기에도 적합했습니다.

이 라이브러리를 사용하면 API 요청 상태 관리를 다음과 같이 Hook으로 할 수 있는데요,

const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)

(여기서 fetchTodoList는 Promise를 반환하는 함수입니다)

이 Hook을 사용하면 리덕스를 사용했을 때 보다 코드를 재사용하기 훨씬 쉬웠습니다. 그래서 저희는 API를 요청하는 함수, 그리고 해당 함수에 대한 타입을 @laftel/common 프로젝트에 넣었고, 그리고 useQuery 를 감싼 함수를 새로 만들어서 내보내주었습니다. 다음과 같이 말이죠.

// 홈의 추천 작품 목록
export function useHomeRecommends() {
  return useQuery('homeRecommends', getHomeRecommends);
}

// 작품 상세 정보
export function useItemDetail(itemId: number) {
  return useQuery(
    ['item', itemId], 
    (key: string, itemId: number) => getItemDetail(todoId)
  );
}

이렇게 하면 웹, 모바일, TV의 컴포넌트에서 API 요청 상태를 관리하기 위해 만든 커스텀 Hook을 쉽게 재사용 할 수 있었죠.

이렇게 API 요청과 관련된 코드를 통일시키고 나니, 재사용 할 수 있는 것들이 더 많이 보이기 시작했습니다. 예를 들어서 저희가 조만간 업데이트할 피쳐 중 다음과 같은 기능이 있습니다.

여러 에피소드를 선택하여 구매 할 수 있는 기능인데요, UI의 생김새는 매우 비슷하지만, 왼쪽은 React Native Component 를 사용하고 우측은 HTML Element 들을 사용하기 때문에 UI를 재사용 할 수는 없었습니다. 하지만 여기서 사용되는 로직은 모두 재사용 할 수 있었습니다.

위 UI에선 다음과 같은 기능을 가지고 있습니다:

  • 작품의 모든 에피소드 리스팅
  • 우측 상단의 첫화부터/최신화부터를 눌러 정렬
  • 단일 항목을 눌러 해당 항목의 활성화/비활성화 상태 토글
  • 좌측 상단의 전체 선택을 눌러 전체 항목의 비활성화/비활성화 상태 토글
  • 대여 또는 소장 버튼 누를 시 결제 해당 에피소드들의 상품 ID를 추출하여 결제 시작

서로 다른 플랫폼이지만 완전히 똑같은 기능이다 보니, 위 로직들은 하나의 Hook으로 만들어서 재사용을 할 수 있었습니다. 다만, 결제 시작의 경우 모바일과 웹은 방식이 조금 다릅니다. 모바일의 경우 새로운 화면을 띄워서 결제를 하고, 웹의 경우 모달을 띄워서 결제를 합니다. 결제를 시작하는 시점에 호출되어야 하는 함수는 서로 다르기에, 결제에 관련된 함수는 Hook의 파라미터로 콜백 형태로 설정하여 호출하도록 구현했습니다.

// 결제에 필요한 정보
type PurchaseInfo = {
  price: number;
  title: string;
  isRent: boolean;
  productIds: number[];
}

// Hook의 파라미터
type UseEpisodeSelectorParams = { 
    itemId: number;
    purchaseCallback: (purchaseInfo: PurchaseInfo) => void;
}

function useEpisodeSelector({itemId, purchaseCallback }: UseEpisodeSelectorParams) {
  const { data, isLoading, refetch } = useEpisodes(itemId);

  /* 다양한 비지니스 로직 구현... */

  return {
    data, // 에피소드 배열
    isLoading, // 로딩 상태
    isTheater, // 극장판 여부
    rentDays, // 좌측 하단의 N일 대여에서 보여줄 N
    toggleId, // 단일 항목 토글
    toggleAll,  // 전체 항목 토글
    purchase,  // 결제 시작
    refetch, // 에피소드 목록 초기화
  }
}

Hook을 만들고 나서는 웹과 모바일에서 그대로 사용하기만 하면 됐습니다. 받아온 상태와 함수를 가지고 플랫폼에 맞춰 UI만 작성하면 되죠.

이렇게 했을 때 두 가지의 장점이 있습니다. 첫 번째로 다양한 로직을 한 번만 작성하고 재사용 할 수 있다는 것이고요, 두 번째는 추후 서버에서 API의 반환 결과에 변동이 있다거나, 비즈니스 로직에 변동이 생기면 이 @laftel/common 라이브러리에서 한 번만 업데이트 하면 모든 플랫폼에 쉽게 반영을 할 수 있다는 것이죠. 따라서, 생산성과 유지 보수성에 있어서 매우 유리해집니다.

지금은 일부의 기능만 코드가 플랫폼간에 재사용이 되고 있는데요, 한번 해보니까 어떤 걸 재사용 할 수 있는지 감이 조금씩 붙게 됐습니다. 앞으로 더 많은 기능들에 대하여 코드를 재사용해볼 예정이랍니다.

CodePush를 통한 배포

리액트 네이티브를 도입하면서 개발팀외에도 비즈니스팀, 운영팀에서도 좋아했던 부분인데요, 원하는 기능의 배포를 아무 때나 스토어를 거치지 않고 할 수 있다는 것이 매우 좋았습니다. 물론, 네이티브 코드에 변동이 있을 땐 CodePush를 사용하진 못하지만, 네이티브 코드는 매우 뜸하게 업데이트되기 때문에 큰 문제가 없었습니다.

CodePush가 있기 때문에 버그를 고쳤을 때 바로바로 반영할 수 있는 것도 좋았고, 최신 버전 사용자의 비중을 이전보다 더 빨리 높일 수 있는 것이 좋았습니다.

쉽고 빠른 UI 작성

JavaScript와 리액트만 잘 알고 있다면 모바일 UI를 쉽고 빠르게 작성할 수 있습니다 (적어도 저는 그렇게 생각합니다). 저의 경우엔 Android의 네이티브 레이아웃을 작성해본적이 있었는데요 Android에서 XML 그리고 Java (또는 Kotlin)로 UI 코드를 작성하는 것이 저는 굉장히 불편하고 실수하기도 쉬웠습니다. 반면 리액트 네이티브를 사용 할 땐 빠르게 완성도 높은 UI를 작성 할 수 있었습니다 (물론, 프런트엔드 개발자들이 리액트 사용에 익숙해서 그런 것도 한 몫 했습니다).

효율적인 QA

리액트 네이티브를 사용하면서 개발 프로세스만 효율적으로 됐을 뿐만 아니라, QA도 더욱 효율적으로 되었습니다. 비지니스 로직은 일부 상황을 제외하고는 Android와 iOS가 동일하기 때문에 QA가 더욱 효율적으로 진행되었습니다. 새로운 기능을 개발하였을 때 일단 원칙적으로는 두 기기에서 모두 검토를 하긴 하지만, 예를 들어서 어떠한 이슈가 발견되었을 경우, 한 번만 고치면 Android와 iOS 모두 반영이 될 수 있어서 편했습니다.

추가적으로, QA에 사용되는 스테이지 앱에도 CodePush가 가능합니다. 그래서 간단한 수정을 하고 나서 앱을 처음부터 빌드 할 필요 없이 CodePush를 통해서 2~30초 만에 반영해서 바로 검토를 할 수 있던 게 굉장히 좋았습니다.

리액트 네이티브의 단점

가끔씩 일관성 없는 UI

대부분의 경우엔 괜찮지만, 가끔씩은 Android에서 보이는 UI와 iOS에서 보이는 UI가 일관성이 없게 나타날 때가 있습니다. 예를 들어서 iOS에서는 괜찮게 보이는데 Android에서는 스타일이 문제가 있어서 이를 고치기 위한 스타일 코드를 더 추가해야 될 때가 있죠.

애니메이션의 경우도 마찬가지로 한 쪽에서 잘 작동한다고 해서 다른 쪽에서도 잘 작동할 것이라는 것을 보장할 수 없기 때문에 애니메이션 개발 이후에는 각 기기에서 잘 작동하는지 확인하는 작업은 필수적입니다.

리액트 네이티브로 만든 앱이 Android와 iOS에서 둘 다 잘 나타날 것이라고 기대를 한 상태에서 사용을 하면 실망을 조금은 할 수 있습니다. 하지만 그렇게 심하지는 않으니 애초에 감안하고 개발을 하면 그렇게까지 불편하지는 않습니다. 다만, 옆 동네 기술 Flutter의 경우 이 부분이 리액트 네이티브보다 훨씬 괜찮다고 하니 (참고 링크), 이 부분에 대해서 민감하시다면 Flutter를 고려해보는 것도 좋을 것 같습니다.

잘못짜면, 성능이슈 쉽게 나타남

이건 꼭 리액트 네이티브만의 문제는 아닌 것 같기는 합니다만, 별생각 없이 코드 작성하다 보면 성능 저하가 느껴질 때가 있습니다. 다행인 건 그러한 부분들은 대부분 최적화가 가능합니다. 네이티브 코드만을 사용하여 구현한다 하더라도 성능 이슈가 발생할 수 있기 때문에 이것이 비단 리액트 네이티브만의 문제라고 볼 수는 없습니다. 리액트 네이티브 개발을 할 때에는 최적화에 대해서 신경을 써가면서 낭비되는 렌더링을 줄여가면서 개발을 해야 앱에서 높은 성능을 보입니다.

추가적으로 상황에 따라 다르긴 하겠지만 저희 앱의 경우엔 리액트 네이티브를 사용하여 구현을 했을 때 네이티브 앱 대비하여 메모리를 더 많이 사용했었습니다. 문제가 되는 수준은 아니었지만, 구형 기기에서 메모리 관리를 위하여 가비지 컬렉터가 너무 빠른 주기로 작동해서 CPU 사용률도 함께 올라가는 현상이 발생하기도 했습니다. 그래서 이는 최적화를 통해서 따로 안정화 시키는 작업을 거쳐야 했었습니다.

가끔씩 진짜 어이없는 이슈 때문에 시간 낭비할 때가 있음

리액트 네이티브를 사용해서 앱 개발을 할 때, 대부분의 경우엔 정말 빠른 속도로 개발을 할 수가 있습니다. 그런데 문제는, 가끔씩 어떤 이슈가 발생하고 그 이슈가 무엇 때문에 발생하는지 파악을 하기가 어려워 시간 낭비를 엄청나게 하는 상황이 간혹 발생할 수 있다는 것입니다.

최근에는 iPhone X 이상 기종에서만 발열이 엄청 심해지는 이슈가 있었습니다. 너무 심해서 장시간 영상 시청 시 다음과 같은 경고가 뜰 정도였습니다.

처음에 사용자 문의를 받았을 때는 그냥 많이 따뜻한 정도인가 보다 싶었는데, 발열이 발생하는 기종에서 직접 재현을 해보니, 진짜 "으악!" 소리가 나올 정도로 너무 뜨거웠습니다.

이 현상이 플레이어를 사용할 때 발생하기 때문에 당연히 비디오 플레이어와 관련된 이슈일 것이라고 생각을 하고 열심히 디버깅을 했는데요, 플레이어에서 보이는 모든 컴포넌트를 지워도 발열 증상이 나타났습니다. 정말 어이가 없었죠. 그래서 이게 도대체 뭐지.. 하고 추적해본 결과 범인은 리액트 네이티브에서 화면을 관리하는 react-navigation이었습니다. 우측에서 나타나는 사이드바를 관리하는 DrawerNavigator에서 width가 정수가 아닌 소수라면 CPU 사용률이 높아지는 이슈였죠.

앱에서는 플레이어 화면에서만 가로로 전환이 되는데 이 때 화면 스택에 존재하는 다른 화면의 사이드바의 너비가 소수로 설정이 되면서 발열 문제가 발생한 것이였습니다.

대부분 써드 파티 라이브러리를 사용하게 될 때 이런 일이 간혹가다 발생하곤 하는데 그럴 때마다 버그의 원인을 추적해나가는 과정이 조금 힘든 것 같습니다.

뻑하면 실패하는 앱 구동, 너무 오래 걸리는 Build Time

리액트 네이티브 프로젝트 개발 후, 시뮬레이터 또는 개발용 기기로 개발 모드를 구동을 할 때 대부분의 경우엔 문제없이 잘 작동하지만 브랜치를 변경하여 네이티브 코드에 변동 사항이 발생하거나 하면, 간혹가다가 앱 구동이 제대로 안되는 현상이 발생합니다. 이럴 땐, Clean Project를 하고, 다시 실행을 해야 잘 작동을 하는데요, 앱의 규모가 커져가면서 모든 걸 초기화 하고 앱을 다시 구동하면 9~10분이 넘어가기도 합니다. iOS 경우엔 더 오래 걸릴 때도 있습니다. 가장 짜증 날 때는 막 3~40분 넘게 걸리고 나서 결국 맨 마지막에 실패로 뜨는 상황이죠..

일반 네이티브 프로젝트를 개발할 때는 이 정도는 아닌데 리액트 네이티브는 앱의 규모가 커질수록 유독 이 문제가 심각한 것 같습니다.

리액트의 철학과 맞지 않는 같은 써드파티 라이브러리들

리액트의 설계 원칙에는 다음과 같은 내용이 있습니다:

"React는 실용적입니다. Facebook에서 작성된 제품의 필요에 의해 발전되었습니다. 함수형 프로그래밍과 같은, 아직은 완전한 주류는 아닌 몇 가지 패러다임에 의해 영향을 받지만, 다양한 기술과 경험을 가진 광범위한 개발자에게 접근 가능하도록 유지하는 것은 프로젝트의 명백한 목표입니다.

우리가 원하지 않는 패턴을 비권장하는 경우는 그것을 비권장하기 전에 존재하는 모든 유스케이스를 고려하고 대체방법에 대해 커뮤니티에 교육하는 것은 우리의 책임입니다. 앱을 구축하는 데 유용한 어떤 패턴을 선언적 방식으로 표현하기가 아주 어려운 경우는 우리는 명령적 API를 제공할 것입니다. 많은 앱에서 필요한 어떤 것에 대한 최적의 API를 찾아내지 못 한 경우, 가능한 추후에 제거할 수 있고 장래의 개선의 여지가 있는 경우에 한해서 보통 수준 이하의 임시적인 작업용 API를 제공할 것입니다."

제가 정말 리액트를 사랑하는 이유 중에선, 라이브러리의 개선이 아주 잘 이뤄지면서, Deprecation은 순조롭고 느린 속도로 이뤄진다는 것입니다. 이를테면 클래스 컴포넌트의 componentWillMount  같은 것들이 있죠. 해당 LifeCycle API를 없앨 거라는 것은 3년 전부터 언급해오고 있는데, 아직까지도 해당 API가 사라지지 않고 유지되고 있습니다. 하지만 해당 API를 없애도록 권장해오고 있죠.

리액트의 클래스  컴포넌트 자체도 마찬가지입니다. 함수 컴포넌트에 Hook이 도입되면서 사실상 클래스 컴포넌트는 componentDidCatch를 사용 할 때를 제외하곤 모두 함수 컴포넌트를 사용하여 구현할 수 있기 때문에 기존의 클래스 컴포넌트를 deprecate 시킬만한데, 그렇게 하지 않고 유지를 하고 있죠.

그래서 리액트를 사용할 땐 라이브러리의 버전을 업데이트를 해도 코드가 와장창 깨지는 일은 많지 않습니다. 하지만, 리액트 네이티브의 써드 파티 라이브러리들은 이런 철학을 따르지 않기 때문에 매우 불편한 상황이 발생하기도 합니다. 버전 업데이트를 하면 코드가 다 깨져버리고, 이를 호환시키기 위해선 큰 공수가 들어갈 때가 많습니다.

저희의 경우엔 react-navigation의 버전을 v4 서 v5으로 올리는 과정에서 큰 어려움이 있었습니다. 라이브러리의 버전 업데이트를 하면서 영향이 가는 코드가 너무나도 많았고, 전체적으로 고치고 나서 한번 QA를 진행해야 되는데, 그 사이에 새로운 화면들이 도입이 되면서 고쳐야 하는 코드들이 또 늘어나고 이런 식으로 계속 업데이트 작업이 연장됐습니다. 급기야 나중에는 새로운 피쳐가 조금 늦게 배포되는 한이 있더라도 새로운 화면은 무조건 리액트 네비게이션 v5를 사용하는 브랜치에서 작업을 하기로 결정하여 이 작업이 드디어 마무리가 되어가고 있습니다. 업데이트를 계획한 건 5월이었는데, 1월인 지금도 업데이트가 이뤄지지 않았습니다 - 아마 이번 달 안에 배포가 될 것이라 생각합니다.

그리고 이 외에도, 버전 업데이트를 하면서 API들이 완전히 바뀌어버리는 리액트 네이티브 라이브러리들이 꽤 많은데, 이 점이 저는 기존에 리액트를 사용하던 사람들이 느낄 수 있는 큰 단점이라고 생각합니다.

완성도가 낮은 써드 파티 라이브러리들

리액트 네이티브에 자체적으로 구현되지 않은 기능들의 경우엔 네이티브 코드를 직접 작성하여 구현하거나 관련 써드 파티 라이브러리의 힘을 빌려 구현을 해야 합니다. 다행히도 수많은 써드 파티 라이브러리들이 npm에 배포되어 있어서 대부분의 기능은 금방 구현을 할 수 있습니다. 이 점은 장점이기도 한데, 문제는 일부 라이브러리들은 완성도가 낮다는 것입니다.

우리가 원하는 방식대로 작동을 하지 않아서 코드를 까보면, 가끔씩은 왜 이렇게 짰지? 싶을 정도로 별로인 코드들이 있을 때가 있습니다. 추가적으로, 업데이트가 제때 이뤄지지 않는 경우도 있어서 라이브러리를 fork 해서 직접 수정을 해야 할 일도 있습니다.

저희는 그러한 상황에 초반에 수정이 필요한 라이브러리를 프로젝트에 내장시켜서 수정 후 사용하는 형태로 구현을 하다가, 나중에는 patch-package 라는 도구를 통해서 라이브러리를 패치하여 사용을 했습니다.

이 라이브러리를 사용하면 프로젝트를 fork 하여 따로 관리하지 않아도 node_modules에서 필요한 수정을 바로 하고 npx patch-package some-package 명령어를 입력하여 패치 내용을 프로젝트에 따로 저장하여 관리를 했습니다. 만약, JavaScript 쪽에 수정이 많이 필요할 때에는 프로젝트를 fork 후 직접 빌드를 해서 결과물을 node_modules에 넣고 이 명령어를 실행하면 됩니다.

이 명령어를 사용하면 프로젝트에 다음과 같은 패치 파일을 생성되며, 추후 patch-package 명령어를 사용하여 패치를 적용 할 수 있습니다.

추가적으로, Android 와 iOS 앱 개발의 트렌드는 Kotlin, Swift 사용인데 리액트 네이티브 라이브러리들은 대부분 Java와 Objective-C를 사용합니다. Kotlin, Swift를 사용 하는게 불가능한 것도 아닌데 이 부분은 많이 아쉽습니다. 저희 팀 내에는 Swift를 아주 잘 다루는 동료가 있는데 써드 파티 라이브러리를 수정할 땐 자신이 별로 좋아하지 않는 Objective-C를 사용해야 해서 불편해했던 게 기억에 남습니다.

조금 복잡한 에러 추적

기존에는 Firebase Crashlytics을 통하여 Android 와 iOS에서 발생하는 에러들을 쉽게 추적 할 수 있었는데, 리액트 네이티브 전환 이후에는 JavaScript 단에서 발생한 에러를 추적하기 어려워서 버그를 고칠 때 효율이 떨어지는 경우가 있었습니다. 이는 해결 방법이 있는 것 같기는 한데요, 약간의 공수가 들어갈 것 같고 조만간 필히 해결해야 할 숙제입니다.

리액트 네이티브 사용시 유의할 점

위 단점들을 읽으면서 아마 짐작하셨겠지만, JavaScript만 할 줄 안다고 무조건 리액트 네이티브로 앱을 완성할 수 있는 것은 아닙니다. 간단한 소규모 앱이라면 가능할 수도 있겠지만 분명히 써드 파티 라이브러리만 의존하다 보면 한계가 존재합니다.

그럴 땐, 써드 파티 라이브러리를 수정하거나 직접 만들어야 하기 때문에 네이티브 코드를 다룰 수 있는 실력이 있어야 합니다.

저희 팀의 경우엔 한 사람이 Android와 iOS 모두 다루는 일은 별로 없고, 보통 1/2의 인원이 Android 네이티브 코드를 담당하고, 나머지 1/2의 인원이 iOS 네이티브 코드를 담당하는 형태로 팀을 운영하고 있습니다. 개발을 시작하기 전부터 관련 기술을 꼭 숙지할 필요는 없겠지만, 프로젝트 개발이 진행되면서 천천히 공부하면 충분하다고 생각합니다.

회고

리액트 네이티브로의 전환은 사실상 리스크가 굉장히 큰 결정이었습니다. 예상보다 작업 기간이 더 많이 걸렸었고 작업 기간이 딜레이가 될 수록 계속 불안했었습니다.

약 6개월의 개발 기간, 꽤나 긴 기간이였지만 정말 다행히도 서비스에 타격을 주지 않았습니다. 이를 가능케 했던건 여러가지 방어막들이 라프텔을 지켜주고 있었기 때문이라고 생각하는데요, 그 중 생각나는 몇가지를 적어보자면 다음과 같습니다.

1. 리디와 함께 타는 배

2019년 8월에 라프텔이 리디에 인수 합병이 되면서 이런 공격적인 변화를 안정적으로 도전 할 수 있었습니다. 스타트업은 타이밍이 정말 중요한데 타이밍이 스타트업의 존폐여부를 결정하기도 하기 때문에 만약 라프텔이 이번 작업을 독자적으로 진행했었더라면 스트레스를 많이 받았을것입니다. 공교롭게도, 라프텔이 리디에 인수합병이 되어 현금 흐름이 더욱 안정화됐기 때문에 심적으로 덜 부담됐습니다.

2. 라프텔 컨텐츠 팀, 운영 팀의 활약

앱에 새로운 피쳐 업데이트가 없었지만 그 대신 컨텐츠 팀과 운영 팀에서 기존과 다른 새로운 시도들을 하면서 힘을 많이 써준 덕분에 정체되지 않고 성장 할 수 있었습니다. 개발적인 측면으론 앱에 큰 변화가 없었지만, 사용자 입장으로는 볼 수 있는 재밌는 컨텐츠들이 더 많아져서 정체되어있다고 느끼지 않았던 것 같습니다.

지난 해 저희는 컨텐츠 제작, 더빙 및 구작 수급 등을 통하여 저희 서비스에서만 볼 수 있는 다양한 라프텔 ONLY 애니메이션들을 제공했었답니다.

3. 스마트 TV앱 개발

리액트 네이티브 프로젝트가 개발되는 동안 기존 앱에 새로운 기능을 가져다주진 못했지만, 이 기간 동안 스마트 TV앱 이 개발 및 릴리즈가 됐었습니다. 이를 통하여 사용자들에게 큰 화면으로 더욱 편하게 볼 수 있는 멋진 사용 경험을 제공 할 수 있었습니다. 이 기간이 TV앱을 개발하기에 딱 적당한 시기가 아니였을까 생각합니다.

리액트 네이티브 앱의 개발과 릴리즈는 성공적으로 되었었지만, 릴리즈 직후에는 이렇게 전환을 한 것이 과연 가장 좋은 선택이였을까? 라는 의문을 가지고 있었습니다. 만약 리액트 네이티브로 통합하지 않고 개발자 채용을 잘 진행해서 Android 개발자, iOS 개발자, Web 개발자 각 플랫폼에 한명씩 더 인력 충원을 하고 서비스 기능 개발에 힘을 더 줬더라면 다른 좋은 결과가 있지 않았을까? 라는 생각을 하기도 했습니다.

2020년 7월까지는 그렇게 생각을 했었는데, 리액트 네이티브 프로젝트가 안정화 되면서 그러한 의문은 모두 사라졌습니다. 저희 개발 조직의 업무 처리가 이전보다 훨씬 가속화 된 것을 느꼈고, 또 코드를 웹, 모바일 앱, TV 앱 간에 공유 하는 것을 실행으로 옮기게 되면서 개발 효율이 높아져서 이 방향으로 틀길 확실히 잘했다는 것을 느꼈습니다.

가장 좋았던 것은 프런트엔드 개발자들의 협업과 소통의 발전이였습니다. 이전에 Android, iOS, Web 개발이 따로 따로 이뤄질 때에는 서로 소통하는 것이 꽤나 적은 편이였습니다. 왜냐하면 서로의 개발 환경도 달랐고 만약 소통을 한다면 특정 로직 구현에 관한 노하우 공유 정도가 전부였는데, 이제는 프런트엔드 개발팀의 그룹원들이 모두 웹 개발자이자, 앱 개발자이기 때문에 더욱 많은 것을 함께 설계해나갈 수 있게 됐습니다. 같은 고민을 함께 하는 사람들이 늘어나니까 확실히 이전보다 더 좋은 솔루션에 도달하는 것이 느껴집니다. 앞으로 팀이 더 커질 것을 생각하면 너무 기대가 됩니다.

리액트 네이티브를 원동력으로 2020년에 우리의 서비스, 기술 그리고 조직이 훨씬 성장했다고 봐도 과언이 아닙니다.

마치면서

리디에는 라프텔 앱 말고도 2개의 리액트 네이티브 프로젝트가 더 있습니다. 리디북스 모바일 앱의 경우 2020년에 리액트 네이티브로 마이그레이션이 진행되었습니다. 현재 iOS는 이미 리액트 네이티브 버전이 릴리즈 되었고, Android의 경우 릴리즈가 임박한 상태입니다. 그리고, 리디에서 2020년에 글로벌 웹툰 서비스 Manta 를 출시했는데요 이 프로젝트 또한 리액트 네이티브를 사용하여 아주 빠르게 완성되었답니다.

리디북스와 Manta의 개발팀에서도 리액트 네이티브를 사용하면서 겪은 재미있는 이야기들이 많을텐데요, 해당 내용 또한 조만간 공유해보도록 하겠습니다!

profile
Frontend Engineer@RIDI. 개발을 재미있게 이것 저것 하는 개발자입니다.

4개의 댓글

comment-user-thumbnail
7일 전

오 마이갓.. 벨로퍼트 님이다... 리액트 책 잘 읽고 있어요~ 고마워요.

1개의 답글
comment-user-thumbnail
3일 전

정말 좋은 글이네요, 벨로퍼트님을 통해서 많이 배우고 있습니다. 감사합니다.

답글 달기