react-native 에 모듈 페더레이션 더하기 (with. RePack)

Jerrynim
직방 기술 블로그
11 min readOct 26, 2023

--

모듈 페더레이션(Module Federation) 은 애플리케이션을 여러 개의 작은 애플리케이션으로 분할하는 것을 말합니다. 마이크로 서비스와 유사하여 Micro-frontends 라고도 불립니다. 직방 프론트엔드 팀에서는 react-native를 사용하여 앱을 개발하면서 다음과 같은 미션을 수행하기 위해 모듈 페더레이션을 고려하고 있습니다.

  • 업데이트 과정 최적화
    OTA(Over-the-air programming)를 활용하여 자바스크립트 코드를 업데이트하여, 사용자는 앱을 다시 다운로드하거나 재설치하지 않고도 업데이트된 서비스를 사용할 수 있습니다.
  • 유연한 배포 관리
    각 모듈의 코드를 서버에서 제어하므로 A/B 테스팅, 사용자 그룹 또는 각 모듈별로 업데이트를 제어할 수 있습니다.
  • 심사과정 없는 앱 업데이트
    remote 모듈을 업데이트하는 것으로 앱을 업데이트를 수행할 수 있습니다. 다만 Store의 정책을 위반하지 않도록 코드에 대한 수정 사항이나 조정을 제공하는 데 사용하여야 하고, 새 기능을 추가해서는 안 됩니다.
  • 서비스 별 개발 및 배포
    전체 서비스를 빌드하는 것에 비해 작은 앱으로 빌드하게 되어 디버깅 및 테스트를 빠르게 할 수 있습니다.

모듈 페더레이션의 기능

모듈 페더레이션을 통해 다음과 같은 것을 이룰 수 있습니다.

  • 애플리케이션을 여러 개의 격리된 컨테이너로 분할할 수 있습니다.
  • 각 컨테이너에 대한 빌드 및 프로세스를 구성할 수 있습니다.
  • 필요에 따라 컨테이너를 동적으로 로드할 수 있습니다.
  • 각기 다른 버전의 컨테이너를 로드할 수 있습니다.
  • 외부 Micro-frontend를 사용가능하게 됩니다.

모듈 페더레이션의 용어

모듈 페더레이션을 이해하기 위해서는 몇가지 용어를 익혀두어야 합니다.

[사진-1] 모듈 페더레이션 예시
  • Host Application(host): 처음으로 실행되는 컨테이너, 스토어에 배포된 앱
  • Local module(local): App bundle(.ipa, .apk)에 포함되는 모듈
  • Remote module(remote): App bundle 로 포함되지 않으며 요청시 원격 위치에서 다운로드됩니다.
  • Expose : 컨테이너가 외부에 노출하려는 모듈의 목록을 나타냅니다.
  • Shared: 별도의 청크로 분리하여 앱의 런타임에 로드해 사용하는 의존성 모듈입니다.
  • Shell: 껍데기라는 의미로 여러 마이크로 프론트엔드 프로젝트를 모아서 보여주는 하나의 프론트엔드 프로젝트입니다. 마이크로 앱 간의 라우팅, 공유 데이터 저장소 초기화 및 각 앱의 lazy loading 을 처리합니다.

react-native 에 모듈 페더레이션을 적용하기 위해서는 Re.Pack 라이브러리를 사용해야 합니다.

Re.Pack

Re.Pack은 callstack 에서 만든 React Native 어플리케이션을 위한 bundler 입니다. react-native cli 를 대체하고, 개발 서버로 작동하거나 React Native 앱을 번들링 하는 역할을 합니다. 또한, Re.Pack 은 Webpack 으로 제작된 번들을 react-native 애플리케이션에서 사용할 수 있게 만들어, 코드 스플리팅(Code splitting) 을 가능하게할 수 있습니다.

Re.Pack을 코드레벨에서 살펴보면서 간단하게 모듈 페더레이션을 살펴보도록 하겠습니다. 앱의 구조는 다음 그림과 같이 host앱이 app1, app2의 remote 앱을 가지는 구성을 해보도록 하겠습니다.

프로젝트의 구조는 다음과 같습니다.(monorepo 구조를 사용합니다)
각각의 모듈에서 독립적으로 구성 및 실행이 가능하도록 구성합니다.

/packages
ㄴ host
ㄴ android
ㄴ ios
ㄴ webpack.config.js
ㄴ app1
ㄴ android
ㄴ ios
ㄴ webpack.config.js
ㄴ app2
ㄴ android
ㄴ ios
ㄴ webpack.config.js

host 설정

host 앱은 app1, app2 컨테이너로부터 “./App” 모듈을 import 합니다.
원격으로 모듈을 불러오기때문에 로딩이 발생하게됩니다. 로딩을 처리하기 위하여 Suspense 와 ErrorBoundary를 사용하도록 합니다.

//App.tsx
import { Federated } from "@callstack/repack/client"

const App1 = React.lazy(() => Federated.importModule("app1", "./App"))
const App2 = React.lazy(() => Federated.importModule("app2", "./App"))

return (
<ErrorBoundary>
<React.Suspense
fallback={
<Loading />
}>
<App1 />
</React.Suspense>
</ErrorBoundary>
<ErrorBoundary>
<React.Suspense
fallback={
<Loading />
}>
<App2 />
</React.Suspense>
</ErrorBoundary>

host는 각 remote 모듈을 요청할 주소를 설정해주어야합니다. Re.Pack의 ScriptManager.shared.addResolver 를 사용하여 주소를 설정하도록합니다.

// index.js
import { ScriptManager, Federated } from "@callstack/repack/client"

const resolveURL = Federated.createURLResolver({
containers: {
app1: __DEV__
? "http://localhost:9001/[name][ext]"
: `https://CF.zigbang.in/chunks/app1/${Platform.OS}/[name][ext]`,
app2: __DEV__
? "http://localhost:9002/[name][ext]"
: `https://CF.zigbang.in/chunks/app2/${Platform.OS}/[name][ext]`,
},
})

ScriptManager.shared.addResolver(async (scriptId, caller) => {
const url = resolveURL(scriptId, caller)
return {
url,
cache: false,
query: {
platform: Platform.OS,
},
}
})

개발시에는 localhost:9001, localhost:9002 주소를 사용하도록 하였기 때문에 host의 dev 서버 이외에도 각각의 모듈의 서버를 실행해주어야합니다. (host 앱은 기본적으로 localhost:8081 주소를 사용합니다.)

//package.json
"start": "react-native webpack-start --port 9001",
$app1 % yarn start
>Server listening at http://[::1]:9001
$app2 % yarn start
>Server listening at http://[::1]:9002

모듈 expose 설정

각 컨테이너에서는 host 에서 요청할 모듈을 expose 해주어야합니다.

//app1/webpack.config.js
plugins: [
new Repack.plugins.ModuleFederationPlugin({
name: 'app1',
exposes: {
'./App': './src/App.tsx',
},

//app2/webpack.config.js
plugins: [
new Repack.plugins.ModuleFederationPlugin({
name: 'app2',
exposes: {
'./App': './src/App.tsx',
},

보신 코드를 통해 코드 스플리팅과 모듈을 원격으로 불러오도록 구성이 가능하게 됩니다.

캐싱

remote 모듈은 요청 시 네트워크를 통해 다운로드를 하기 때문에 최적화를 위해 캐싱을 활용할 수 있습니다. 한번 다운로드된 모듈은 ScriptManager에 의해 캐싱되어 이후에 다시 요청을 할 때에는 다운로드를 하지 않습니다. 그렇다면 업데이트된 모듈을 다운로드하기 위해서는 어떻게 해야 할까요?
캐싱된 모듈을 사용하지 않고 모듈을 다시 다운로드하기 위해서 다음과 같은 방법을 사용할 수 있습니다.

  • url 변경 : 기본적으로 ScriptManager 는 method/url/query/header or body를 비교하여 다운로드가 필요한지 결정합니다.
  • 서버 캐싱 Invalidation: Cloudfront 등 을사용하여 서버에서 캐싱을 하고 있다면 Invalidation을 통해 캐시를 새로 다운로드하도록할 수 있습니다.
  • invalidateScripts 메서드 사용: ScriptManager.shared.invalidateScripts 메서드를 활용하여 캐시를 제거할 수 있습니다.

Re.Pack 앱 배포

Re.Pack 앱을 배포할때에는 host 앱과 remote 모듈을 배포하는 방식이 다릅니다. host 앱을 배포하기 위하여 번들링 된 .ipa, .apk 파일을 만들어야 합니다.
이때 ios에서는 main.jsbundle 파일이 필요로 하는데 webpack-bundle 커맨드를 통해 번들을 생성하여 main.jsbundle 로 지정해주어야 합니다.

//기존 react-native main.jsbundle 생성
"build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --reset-cache --dev=false --platform='ios'",
//repack에서 bundle 생성
"bundle:ios": "react-native webpack-bundle --platform ios --entry-file index.js --bundle-output ./ios/main.jsbundle --dev=false",

remote 모듈을 배포를 할 때에도 webpack-bundle 커맨드를 통해 청크를 생성하여야 합니다. webpack-bundle을 통해 생성된 build는 다음과 같은 구조를 가지게 됩니다.

/build
ㄴ ios
ㄴ assets
ㄴ app.container.bundle
ㄴ app.container.bundle.map
ㄴ index.bundle
ㄴ index.bundle.map
ㄴ src_App_tsx.chunk.bundle
ㄴ src_App_tsx.chunk.bundle.map

생성된 청크를 서버에 업로드하여 다운로드할 수 있도록 합니다. 저의 경우 S3에 업로드하였습니다.

Re.Pack 사용시 고려해야할 사항

Re.Pack을 적용하고 사용해 보면서 겪은 것들이 많았습니다.

  • 모듈 페더레이션에 대한 이해도가 필요하였고, 기존 앱을 어떻게 분할하고 구성할 것인지 많은 고민이 필요하였습니다.
  • 초기 설정을 하는 과정에서 react-native와 native 라이브러리와의 호환성으로 인해 버전을 수시로 변경하여야 했습니다.
  • 기존의 metro 번들러에 비하여 속도적으로 느렸지만 리소스를 분리하여 빌드하기에 더 빠르게 빌드할 수 있었습니다.
  • remote 모듈을 사용하면서 네트워크 비용이 발생하게 되었습니다.
  • 앱의 개수가 늘어나게 되면서 테스트 앱의 관리비용이 증가하였습니다.
  • 에러 발생 시 생태계의 도움을 받기 힘들어 많은 시간이 필요했습니다.

마치며

react-native에 모듈 페더레이션을 도입하기 위해 Re.Pack을 사용한 경험을 소개해 드렸습니다.
모듈 페더레이션은 react-native를 사용하고 계시는 많은 개발자들에게 성숙한 서비스의 개발환경 개선을 위한 좋은 방안이라고 생각합니다. 이 글에서 제시하는 Re.Pack을 통해 모듈 페더레이션을 사용해 보시면 좋을 것 같습니다.

출처:
사진-1: https://rangle.io/blog/module-federation-federated-application-architectures

--

--