본문 바로가기

프로젝트/Module Federation

리액트 네이티브 앱을 재시작하지 않고 일부분만 업데이트해 보자 (1. 프로젝트 소개)

목차

  1. 요약
  2. 최종 결과물
  3. 소스코드 및 재현 방법
  4. 테스트 환경
  5. 문제 인식
    5-1. 업데이트=재시작
    5-2. 재시작이 필요한 업데이트의 문제점
      5-2-1. 근본적인 문제
      5-2-2. 기존 상태 소실
  6. 해결 방법
    6-1. Module Federation 도입
    6-2. Module Federation 변조
  7. 프로젝트 구조
  8. 사용예
  9. 마치며

1. 요약

리액트 네이티브 앱을 재실행없이, 앱 사용중(스크린 스택 등 기존 상태 유지)에 부분 업데이트할 수 있는 방법을 소개합니다.

 

마치 헐크 버스터가 "싸움 도중에" 신체의 일부를 교체하는 것 처럼요. 

2. 최종 결과물

  1. host 앱과 number 앱을 독립적으로 실행
  2. number 앱을 로컬에서 변경하고 확인
  3. number 앱을 번들링하여 배포 서버에 업로드
  4. host 앱에서 업데이트 버튼을 클릭하면, 재시작 없이 number 앱을 제외한 나머지 상태(네비게이션 스택 등)는 유지하면서 number 앱 컴포넌트만 교체

 

3. 소스코드 및 재현 방법

 

GitHub - JoonDong2/dynamic-module-federation-example

Contribute to JoonDong2/dynamic-module-federation-example development by creating an account on GitHub.

github.com

 

스크립트를 만들어 놓았기 때문에, 루트 경로에서 실행하면 됩니다. 

  1. npm i (모든 라이브러리 및 앱의 의존성 설치)
  2. npm run deploy:android:all 모든 앱 배포 (배포 서버 localhost:4000에서 자동 실행)
  3. npm run android:host:staging host (host 앱 실행, 실패하면 에뮬레이터가 완전히 켜질 때까지 기다렸다 다시 실행)
  4. npm run android:host:staging number (number 앱 실행)
  5. apps/number/src/constants.ts 파일에서 앱 고유색 변경
  6. npm run deploy:android number (number 앱 배포, 버전 자동 지정)
  7. host 앱에서 "앱 업데이트" 버튼 클릭

4. 테스트 환경

  • CPU: Intel
  • OS: MacOS 14.4.1 (맥OS 전용 스크립트 명령어를 사용해서 윈도우, 리눅스에서 사용 불가)
  • Node: 20.12.1
  • JDK: OpenJDK 17.0.11 (Microsoft)
  • ruby: 2.7.6
  • 모바일 OS: 안드로이드 API 34 (iOS는 의존성 설치 후 개발 전 한 번만 실행하고 테스트 안했습니다.)
  • 장치: 에뮬레이터 (localhost:4000번을 서버로 이용했기 때문에, 실제 장치는 localhost 연결하는 작업 필요)

5. 문제 인식

5-1. 업데이트=재시작

스토어에서 앱을 업데이트하면 앱이 종료됩니다. 

 

codepush, Module Federation같이 스토어를 통하지 않는 업데이트 방법도 있습니다.

 

codepush는 단일 스크립트 파일을 교체하는 방식이기 때문에, 앱이 업데이트되면 무조건 재시작이 필요합니다.

 

Module Federation도 앱을 분리/병합하는 방법만 제공해 줄 뿐이고, 한 번 로드한 미니 앱은 앱이 종료할 때까지 계속 유지됩니다.

 

결국 대부분의 경우 변경사항을 적용하기 위해선 재시작이 필요했습니다.

5-2. 재시작이 필요한 업데이트의 문제점

5-2-1. 근본적인 문제

대부분의 앱이 업데이트 후 재시작이 필요한 이유는 런타임에 코드 일부를 바꾸는 것은 너무 위험하기 때문입니다.

 

그리고 이것은 변경사항을 바로 반영할 수 없다는 것을 의미합니다.

전 회사에서 푸시 알람을 통하여 사용자들을 버그가 수정된 페이지로 이동시키고 싶은데, 업데이트가 완료되지 않은 사용자들 앱에서는 작동하지 않기 때문에, 업데이트 비율이 일정 수치가 될 때까지 한 달 정도 기다린 적이 있습니다.

 

codepush로 강제 업데이트를 설정해도, 앱이 실행되기 전까지는 업데이트가 있는지도 모르기 때문에, 이런 문제는 여전히 발생합니다.

 

5-2-2. 기존 상태 소실

가장 대표적인 예는 사용자의 히스토리(스크린 스택)입니다.

결국 사용자에게 불편함을 야기합니다.

6. 해결 방법

6-1. Module Federation 도입

callstack 팀에서 기본 번들러인 metro대신 webpack을 사용할 수 있는 repack을 출시해서 리액트 네이티브에서도 Module Federation을 사용할 수 있게 되었습니다.

 

리액트 네이티용 Module Federation

원래 리액트 네이티브는 metro라는 자체 번들러를 사용했기 때문에, 개발자가 개입할 수 있는 방법이 제한적이었습니다. 그런데 callstack 팀에서 리액트 네이티브를 위한 webpack인 repack과 리액트

joondong.tistory.com

 

Module Federation은 여러 개의 미니 앱이 하나의 앱을 구성합니다.

 

미니 앱은 필요할 때 필요한 만큼만 로드할 수 있습니다.

codepush는 한 개의 번들링 파일로 작동하기 때문에, 고려할 여지가 없었습니다.

 

하지만 한 번 로드한 미니 앱은 앱이 종료할 때까지 계속 유지됩니다.

6-2. Module Federation 변조

하지만 Module Federation이 만들어낸 파일에 약간의 코드만 추가하면, 앱 단위로 런타임에 삭제하고 다시 로드할 수 있을 것 같았습니다.

 

이를 위해 다음 편에 설명할 react-native-dynamic-module-federation 라이브러리를 직접 만들었습니다.

 

해당 라이브러리에서 제공하는 webpack 플러그인은 로드된 앱을 삭제하는 코드를 Module Federation이 만들어낸 파일에 추가합니다. 

 

이 코드를 사용하여 앱 버전이 변경이 감지되면, 기존 앱을 삭제하고, 새로운 앱을 로드합니다.

7. 프로젝트 구조

dynamic-module-federation-example

 

entry, alphabet, number 앱에서 노출하는 모듈들이 번들링되어 containers-server에 배포(deploy)됩니다.

 

host 앱뿐만 아니라, 모든 앱은 containers-server에서 노출하는 모듈을 실시간으로에 다운받아 업데이트(realtime update)할 수 있습니다.

 

host, entry 앱은 팀에서 공통으로 관리(manage)하고, alphabet 앱 개발자, number 앱 개발자가 각자 자신이 담당한 앱을 배포(deploy)까지 담당하는 상황을 가정했습니다.

  • host
    스토어 배포를 가정한 앱입니다.
    entry 앱을 로드하는 것 이외에 아무런 로직이 없습니다.

  • entry
    다른 앱을 로드합니다.

    이것을 host에서 하지 않은 이유는 이전 포스트인 "리액트 네이티브용 Module Federation"에서 언급한 Host 앱으로서의 접근 제한과 Host 앱은 변경하기 위해 스토어 배포가 필요하기 때문입니다.

  • alphabet, number, emoji
    특정 도메인을 담당하는 미니앱들입니다.

  • shared
    편의를 위해 라이브러리처럼 만들긴 했는데, dynamic-module-federation-example 프로젝트에 100% 의존하는 설정값, 설정값을 반환하는 함수들의 모음입니다.

    그래서 shared 모듈은 root에 있지만, react-native-dynamic-modules 라이브러리는 로컬 모듈 저장 공간인 modules 폴더에 있습니다.

  • containers-server
    앱을 배포하고, 배포 정보를 제공합니다. 

    배포된 파일은 containers-server/deployments 경로에 저장되고, / 경로에서 호스팅됩니다. 

    프로덕션이었다면 S3 등에서 호스팅했을 것입니다.

8. 사용예

DynamicImportManager 객체를 사용하여 리액트 내외부 어디에서든지 refreshContainers 또는 refreshContainer 메서드를 사용하여 미니 앱(container)을 업데이트할 수 있습니다. 

 

예를 들어 host 앱에서는 앱이 백그라운드에서 포그라운드로 이동했을 때, refreshContainers 메서드를 호출하여 업데이트가 필요한 미니 앱들을 최신 버전으로 업데이트합니다.

const manager = new DynamicImportManager({
  fetchContainers,
  fetchContainer,
  errorManager: ErrorManager,
});

let appState = AppState.currentState;

// inactive 무시
AppState.addEventListener('change', nextAppState => {
  if (appState === 'background' && nextAppState === 'active') {
    manager.refreshContainers();
  }

  appState = nextAppState;
});

https://github.com/JoonDong2/dynamic-module-federation-example/blob/main/apps/host/App.tsx#L23-L38

9. 마치며 

경력의 대부분을 리액트 네이티브로 개발해 오면서, 최근 앱이 커질 수록 관리성능에 대한 한계를 느꼈습니다.

 

이 중에서 관리에 대한 이슈는 리액트 네이티브만의 문제는 아닐 것입니다. 

 

Module Federation은 자바스크립트라는 특별한 생태계를 통해서 관리에 대한 문제를 해결할 수 있는 멋진 방법이라고 생각합니다.

 

그리고 이번 프로젝트는 Module Federation 기능을 업그레이드하여 사용할 수 있는 프로젝트 정도로 봐주셔도 될 것 같습니다. 

 

다음 포스트에선 이 프로젝트의 주요 코드를 설명해 보겠습니다.