React-Native + CRA MonoRepo 환경 구축하기

황연욱·2020년 12월 6일
10

react-native

목록 보기
1/1

React-Native(RN)에서 MonoRepo 환경 구축하기

현재 진행하고 있는 프로젝트에서 CRA + ReactNative Monorepo 초기세팅 작업을 진행했던 경험을 바탕으로 개발환경을 구축하는 방법을 공유하고자 작성하는 글입니다.
관련된 자료가 많이 없어서 방법을 찾고 에러를 해결하는 과정에 많은 소요가 들었기에 이 과정을 공유해서 많은 분들이 참고해서 더 빠르고 편하게 환경설정을 돕고자 작성하는 글입니다.

버전 및 목적별로 조금씩 설정하는 방법이 다르기에 제가 사용한 방법이 최적의 효율을 내는 방법이 아닐수도 있고, 사용하는 패키지들의 버전이 바뀌면 조금씩 상이한 부분이 존재해서 수정해야 하는 부분이 존재할 수 있습니다.

시작전에

  • 이 포스트는 React-Native(이하 RN) 0.63, CRA(typescript-template 1.1.0) 기준으로 작성되었습니다.

왜 사용하는가?

  • 일반적으로 각 프로젝트들이 각각의 레파지토리를 가지면서 관리하는 전통적인 방법을 MultiRepo라고 지칭합니다.
  • Monorepo란 여러개의 프로젝트를 한 레파지토리에 저장하는 형태의 레파지토리 관리 전략입니다.
  • 이러한 형태로 모노레포를 구축하게 되면 장점은, 모든 프로젝트에서 사용되는 공용모듈들을 각 프로젝트마다 작성할 필요 없이 하나의 패키지로 작성한 뒤, 모노레포 안의 모든 프로젝트에서 그를 공유해서 사용할 수 있다는 장점이 있습니다.
  • 또한 node_modules중 중복되는 패키지들은 각기 다른 프로젝트마다 설치되는 것이 아니라, 루트 node_modules에서 관리하게 됨으로 node_modules의 용량을 줄일 수 있다는 장점도 있습니다.(제가 구축한 환경설정 방법에서는 패키지들의 경로문제로 인해, RN프로젝트에 설치되는 패키지들은 node_modules로 hoisting하지 않고, RN프로젝트쪽 node_modules에 설치되게 했기에 이 장점은 취할 수 없습니다.)

도입을 결정하게 된 계기

  • 제가 진행하는 프로젝트에서는 실질적으로 유저가 사용하게 되는 형태는 Mobile App의 형태로 개발되고, 그를 관리하는 관리자페이지의 경우에는 Web의 형태로 제공하게 될 계획이었습니다.
  • 따라서 React-Native와 CRA를 이용해서 각기 프로젝트를 진행하고자 방향을 잡았는데, 두 프로젝트 모두 같은 유틸함수와 theme을 사용해야 하는데 이를 각기 다른 프로젝트마다 작성할 경우 중복 코드가 늘어나고
  • 공용모듈에서 수정해야 할 부분이 생기면, 두가지 프로젝트에서 각기 수정해야 할 소요가 생겨서, 유지보수 측면에서 비효율적이라는 생각이 들어서 고민하던 중
  • Monorepo라는 개념을 접하게 되어서, 이를 정리해서 건의한 후에 CTO님이 승인해주셔서 Monorepo형태로 프로젝트를 진행하게 되었습니다.
  • 유사한 상황에 계신분들은 도입을 고려해봐도 좋을 것 같습니다.

구성요소 설치

  • 이 환경설정에서 사용하는 구성요소들은 아래와 같습니다.
  • yarn workspace
    • yarn에서 제공하는 multiple packages setup tool입니다.
  • lerna
    • monorepo 구성 tool, 현재 yarn workspace로 패키지들로 관리를 하며, lerna에서 제공하는 여러가지 script등을 사용 위해서 추가 함
  • CRA with typescript template
  • React-Native-CLI
    • 기본적인 리엑트 네이티브 프로젝트를 실행하기 위해서 필수 구성요소를 설치해야 합니다.
    • 이 부분은 React-Native 공식문서 QuickStart 파트에 문서화 되어있기에 링크첨부로 대체합니다.

MonoRepo

디렉토리 생성

  mkdir <project-name>
  cd <project-name>

프로젝트 및 lerna init

  yarn init -y -p
  npx lerna init
  • 위 스크립트를 실행 시 lerna.json, package.json, packges 디렉토리가 루트에 추가됩니다.

lerna.josn 및 package.json 수정

  • yarn workspace 및 lerna를 사용하기 위해 설정을 추가해줍니다.
  • 추후 RN프로젝트를 생성하고 난 뒤에 react-native 모듈의 경로가 꼬이는 문제를 방지하기 위해서, package.json에 nohoist설정을 추가해줘야 합니다.
    (이 부분은 React-Native-CLI init후에 설정하도록 하겠습니다.)

[lerna.json]

{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "useWorkspaces": "true",
  "version": "0.0.0"
}

[package.json]

{
  "name": "<Project-name>",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "<Repository-url>",
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  },
  "workspaces": {
    "packages": ["packages/*"]
  }
}

.gitignore 파일 생성

touch .gitignore

[.gitignore]

.DS_Store
node_modules/
yarn-error.log

RN프로젝트 생성

RN 프로젝트 init 및 불필요 파일 삭제

cd packages
npx react-native init <rn-project-name> --template react-native-template-typescript
cd <rn-project-name>
rm yarn.lock && rm -rf node_modules && rm -rf .git

nohoist 설정 추가

  • root directory에 있는 package.json파일에서 rn-project의 node_modules이 루트 node_modules로 hoist되지 않도록 no-hoist설정을 추가해줍니다.
{
  "name": <project-name>,
  "version": "1.0.0",
  "main": "index.js",
  "repository": <your-repository-url>,
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  },
  "workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": [
      "**/<rn-project-name>",
      "**/<rn-project-name>/**"
    ]
}
  • 제가 찾은 리서치결과의 참고자료들에는 nohoist설정에 react-native 모듈만 추가해도 구동이 되었지만, 제가 프로젝트를 진행하면서 겪은 상황에서는 rn프로젝트에 새로운 dependency를 추가할때 마다 서로 의존성이 있는 경우에는 서로의 경로를 제대로 못찾아서 에러가 발생하는 경우가 많았기에,
    그때마다 일일히 해당 모듈을 nohoist설정을 하다보니 비효율적으로 판단되어서,
    rn프로젝트 전체의 node_modules를 nohoist설정을 하는 식으로 해결했습니다.

metro.config.js 수정

  • 웹에서의 webpack과 같은 역할을 하는 metro서버 의 설정을 수정해주기 위해서 metro.config.js파일을 수정해줍니다.
  • 일반적인 프로젝트에서 루트디렉토리는 보통 RN을 init 한 위치이지만, monorepo에서는 전체 프로젝트의 root directory가 있고 각 프로젝트들은 packages 디렉토리 하위에 각각 생성되게 설정되어 있으므로 루트디렉토리의 위치를 수정해줘야합니다.
  • 파일을 열어서 projectRoot의 경로를 수정해줍니다.
const path = require("path");

module.exports = {
  projectRoot: path.resolve(__dirname, "../../"),
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

IOS 설정

1.xcode 실행

(in Root directory) open packages/<rn-project-name>/ios/<rn-project-name>.xcodeproj/

2.AppDelegate.m 파일 수정

  • AppDelegate.m 파일의 jsBundleURLForBundleRoot 부분의 "index" 를 "packages/<rn-project-name>/index"로 수정
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"packages/<rn-project-name>/index" fallbackResource:nil];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

3. Bundle React Native code and Images 수정

  • Xcode 왼쪽 디렉토리부분의 최상단 <rn-project-name> 클릭
  • Build Phases ⇒ Bundle React Native code and Images
  • shell 부분에 EXTRA_PACKAGER_ARGS 추가
export NODE_BINARY=node
export EXTRA_PACKAGER_ARGS="--entry-file packages/<rn-project-name>/index.js --reset-cache"
../node_modules/react-native/scripts/react-native-xcode.sh

4.Do Test!

(in project root directory)yarn workspace <rn-project-name> ios

  • 앱이 빌드되고, metro server 터미널이 켜지면서 정상적으로 시뮬레이터에 앱이 보이면 성공
  • metro server 터미널이 자동으로 안 켜질 경우 yarn workspace start를 통해서 metroserver만 따로 실행시킨 후 yarn workspace mobile ios 구동 시도

Android 설정

1.packages/<rn-project-name>/android/app/src/main/java/com/mobile/MainApplication.java수정

  • packages//android/app/src/main/java/com/mobile/MainApplication.java 파일의 getJSMainModuleName() 함수 내 경로 수정
@Override
protected String getJSMainModuleName() {
  return "packages/<rn-project-name>/index";
}

2.packages/<rn-project-name>/android/app/build.gradle 수정

  • ___build.gradle 파일이 packages/<rn-project-name>/android/디렉토리에도 존재하고, packages/<rn-project-name>/android/app/디렉토리에도 존재하기에 두 파일을 헷갈리지 않도록 주의하시길 바랍니다.
  • cliPath, entryFile 추가
project.ext.react = [
  (enableHermes: false), // clean and rebuild if changing
  (cliPath: "../../node_modules/react-native/local-cli/cli.js"),
  (entryFile: "packages/<rn-project-name>/index.js"),
];

Do Test!

(in project root directory)yarn workspace <rn-project-name> android

  • 혹시 빌드와 실행과정에서 에러가 발생한다면, 기존의 실행되어 있는 metro-server가 있는지 확인하고 실행되고 있다면 종료 후 다시 위 스크립트를 실행해보기 바랍니다.
  • 원래 기존 실행중인 metro-server가 존재한다면, 그 서버에 연결되어서 안드로이드 에뮬레이터가 가동되지만 간혹 자동으로 연결이 되지 않는 경우도 있으니 시도해보기 바랍니다.
  • Execution failed for task ':app:installDebug' Error 발생 시
    Android의 경우 adb(android debug bridge)와 연결이 잘 안됬을 경우 해당 에러가 발생합니다, 이 경우에는 기존의 metroserver를 종료하고,
adb kill-server
adb start-server

스크립트를 실행 후 다시 yarn workspace <rn-project-name> android스크립트를 실행해보기 바랍니다.

CRA 프로젝트 생성

CRA init

cd packages
npx create-react-app web --template typescript
cd web
rm yarn.lock && rm -rf .git

node_modules삭제 및 재설치

  • 기본적으로 패키지에 설치된 node_modules 삭제 및 재설치 과정 실행(monorepo개념으로 node_modules관리하기 위함)
  • cra init을 하면 pakcages//node_modules에 모듈들이 설치될텐데,
    이걸 루트 디렉토리에서 관리하기 위해서 삭제 후 install 하는 과정이 필요합니다.

(in Root Packages) yarn lerna clean && yarn install

Do Test!

yarn workspace <cra-project-name> start를 했을때 정상적으로 로컬호스트에 cra프로젝트가 실행되면 성공입니다.

CRA 필요 dependency 개별 추가

  • 일부 dependency들의 경우 모노레포에서 rn과 cra프로젝트를 모두 관리하다 보니 서로 같은 패키지들이지만 다르 버전을 사용하는 경우 에러가 발생할 수 있습니다.
  • yarn workspace <cra-project-name> start 실행한 후 dependecy 버전 오류메세지가 출력된다면 해당하는 버전의 패키지를 개별적으로 cra project에 추가해주면 됩니다.
  • 프로젝트에 패키지들을 추가하려면은 yarn workspace <project-name> add <package-name> 스크립트를 실행하면 됩니다.
  • 예를들어 방금 만든 cra프로젝트에 bable-jest와 eslint를 devDendency에 추가하고싶다면
    yarn workspace <cra-project-name> add bable-jest eslint -D 스크립트를 통해서 추가할 수 있습니다.

공용모듈 패키지 dependency에 추가하는 법

  • 모노레포 안의 패키지를 다른 패키지의 dependency로 추가해서 사용하려면은 아래와 같이 실행하면 됩니다.
  • 기본적인 개념은 npm 패키지들을 설치하는 것과 동일합니다.
  • yarn workspace <project-name> add <package-name>
  • 여기서 좀 더 추가해야할 부분은 을 추가하고자 하는 패키지의 package.json에 적혀있는 "name"속성 값을 추가해야합니다.
  • 또한, 기입해서 add하면 npm에 등록되어있는 패키지들을 찾기 때문에 뒤에 버전정보도 기입해줘야 합니다.
  • 버전정보는 추가하고자 하는 패키지의 packages.json의 "version"속성 값을 기입해주면 됩니다.
  • 즉 자주 사용되는 유틸함수가 모여있는 하나의 패키지를 만들어서 모노레포안의 모든 패키지들에서 import해와서 사용하는 상황을 가정했을때
  • 유틸함수가 모여있는 패키지의 이름을 여기서는 "@mono/lib"로 가정하겠습니다.
{
  "name": "@mono/lib",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "<Repository-url>",
  "private": true,
}
  • yarn workspace <your-project-name> add @mono/lib@1.0.0
  • 위와 같은 식으로 스크립트를 입력하게 되면 우리가 작성한 @mono/lib 패키지가 대상이 되는 프로젝트에 dependency로 추가가되고, 일반 다른 npm패키지들을 사용하듯이 파일안에서 import해서 사용할 수 있게 됩니다.

기본적인 스크립트 사용법

  • 기본적으로 yarn workspace를 사용 시 yarn workspace

  • 예를 들어 web이란 프로젝트에 build라는 스크립트가 설정되어 있고, 그걸 root directory에서 실행하고 싶다면 yarn workspace web build를 터미널에서 실행시키면 됩니다.

  • 처음에 lerna를 설치했었습니다. lerna에서 제공하는 여러가지 모노레포를 관리하기 위한 기능들이 있는데 그 중에서 저는 주로 lerna clean 이라는 스크립트만 사용하고 있습니다.

  • yarn lerna clean 스크립트를 실행하면 packages/ 디렉토리 하위의 모든 프로젝트들의 node_modules를 일괄적으로 삭제할 수 있습니다.

  • root directory에서 yarn install 스크립트를 실행하면 하위의 모든 프로젝트들의 dependecy들이 설치됩니다.

  • dependecy들이 꼬였거나, 기타 이유로 인해 node_modules를 삭제 후 재설치해야 할 필요가 있다고 판단되면 yarn lerna clean && yarn install을 통해서 실행할 수 있습니다.

  • 또한 저의 경우에는 매번 yarn workspace를 치는 것이 번거로워서 alias적인 개념으로 root pakcage.json에 script를 추가해놨습니다.

{
  "name": <your-project-name>,
  "version": "1.0.0",
  "main": "index.js",
  "repository": <your-repostiory-url>
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  },
  "workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": [
      "**/<rn-project-name>",
      "**/<rn-project-name>/**"
    ]
  },
  "scripts": {
    "web": "yarn workspace web",
    "mobile": "yarn workspace mobile",
    "server": "yarn workspace server",
    "lib": "yarn workspace @mono/lib",
    "theme": "yarn workspace @mono/theme"
  }
}

참고문서

profile
효율적이고 아름다운 코드를 쓰고싶은 호기심 많은 개발자입니다.

4개의 댓글

comment-user-thumbnail
2020년 12월 8일

👍

답글 달기
comment-user-thumbnail
2021년 1월 28일

안녕하세요 글 보면서 많은 도움이 되었습니다
다만, 의존 패키지가 심볼릭링크로 연결되던데 metro에서 실행했을때 문제 없으셨나요?
module not found 에러가 계속 나네요...

1개의 답글
comment-user-thumbnail
2021년 8월 2일

언젠간 저도 이 글을 알아볼 수 있겠지요..?🥲
항상 건강하시기를 바랍니다 연욱님💪🙏

답글 달기