현재 모노레포 PoC(Proof of Concept) 중이기 때문에 기록용으로 남겨둡니다.
도입 배경
배워야 할 게 산더미인데, 이런 새로운 기술은 왜 자꾸 생기는 거야
라는 한탄 아닌 한탄을 하지만 어느 순간, 이래서 이런 도구가, 기술이 생기는 거구나. 하는 지점이 매번 오는 것 같다. 모노레포도 그렇다.
모노레포를 도입하게 되는 배경은 어느 곳이나 비슷한 것 같다.
우리팀의 경우는 바퀴를 다시 발명하지 마라
라는 격언이 있듯 같은 일을 반복하고 싶지 않다는데에 출발했다. 새로운 프로젝트가 생길 때마다 레포지토리를 만들고 세팅하는데만 하루 이상을 쓴다. 템플릿을 쓰면 되지 않느냐는 말을 할 수도 있지만, 프레임워크가 달랐기 때문에 그럴 수 없었다.
같은 프레임워크를 쓴다 하더라도 빠른 발전을 하고 있는 프론트엔드 특성 상, 그 사이에 버전이 상이해져 구 프로젝트에선 잘 돌아가던 코드가 새로운 프로젝트에서는 안 될 수도 있었다. (Next13과 Next12가 얼마나 많이 달라졌는지 생각해보면...)
시작은 이렇게도 단순했으나...
어떤 도구를 쓸까?
가장 고려해야 할 점은 하나였다. 우리 서비스는 React-native로 되어있었고 Web(Next, React)을 위한 프로젝트를 만들어야 했다. 그러니까 RN과 Web이 모노레포로 공유되어야 했는데 관련 레퍼런스를 찾기가 어려웠다. 대부분 Web으로만 이루어진 모노레포를 사용하고 있었고 RN + Web에 대한 자료가 거의 없었다. 있다고 하더라도 RN을 Web으로 보는 듯한 느낌의 자료만 있어서 Mobile 따로, Web 따로 모노레포를 가져가야 하는 게 아닌가, 모노레포에 대한 이해를 잘못하고 있는 건가라는 생각이 들었다.
일단 칼을 빼들었으니 시도는 해보기로 했다.
조사해보니 모노레포 툴이 생각보다 많았다. 툴 설명이 있는 웹사이트까지 있었다. 링크. 팀 내에서 어떤 것으로 할 지 이야기 중에 있는데 세 가지로 좁혀졌다.
- yarn1
- 큰 기능 없이 그저 공통 요소만 공유하고 싶었기 때문에 yarn workspace 만 쓰는 방향
- Nx
- 향후 고도화 할 때 yarn1의 지원미흡이 발목을 잡을 수 있지 않을까하여 모노레포를 위한 빌드 시스템을 선택
- RN 가이드 지원
- Turborepo
- vercel이 밀고 있고 빠르다는 장점
팀 내에서 각자 빌드해보고 도입해보기로 했는데, 처음에 yarn1이 생각만큼 잘 되지 않아서 (모듈을 못 찾는 에러가 가장 많았다.) Nx로 다시 시작해보았는데 희망이 보일 듯 말 듯 했다.
Nx?
why Nx?
- yarn1의 무차별적인 에러로 Nx로 재구축
- 한때 Lerna가 유명했었던 것 같은데 Lerna를 nrwl가 인수했다고 한다. Nx가 지원하는 기능은 굉장히 많은데 한때 유명했던 Lerna까지 인수했다니 그러면 모노레포 도구로써 뭔가 더 낫지 않을까?라는 생각으로 Nx를 긍정적으로 살펴보았다.
- 무엇보다 RN에 대한 문서가 있어서 Nx를 채택했다. 찾아본 사람은 알겠지만 RN + Web 모노레포 예시 대부분이 RN Expo를 기본으로 가져가고 있다.
터보레포
의 경우도 RN Expo를 예제 템플릿으로 내놓았다.사내에서는 RN Cli를 쓰고 있었기 때문에 도움이 전혀 되지 않았다. Nx는 RN Cli 문서까지 내놓고 있어서 긍정적으로 보았다. (그 외 다른 App도 많다!)
Nx의 컨셉
Nx에서는 통합 저장소와 패키지 기반 저장소 컨셉으로 나뉘는데, 패키지 기반 저장소
는 유연성과 채택 용이성에 중점을, 통합 저장소
는 효율성과 유지 관리 용이성에 중점을 둔다고 한다. 일반적인 모노레포툴은 패키지 기반 저장소로부터 시작하는 것 같다.
패키지 기반 저장소는 package.json와 node_modules가 중첩되어있고 일반적으로 각 프로젝트에 대해 서로 다른 종속성 세트가 있지만 통합 저장소는 루트에 정의된 모든 속성이 있다.
어떤 것을 선택해야 할 지부터가 가장 혼란스러웠는데, 확장성을 생각한다면 통합 저장소를 추천하고 있어서 통합 저장소로 선택해보기로 했다.
통합 저장소를 사용하며 가장 헷깔렸던 것 중에 하나가 두 프로젝트 간 공통 요소가 없으면 어떻게 처리하는가?
였다. 왜냐하면 프로젝트 내부에는 package.json
이 없기 때문이다.
Nx는 libs
라는 폴더 아래에 공통 요소를 묶어둘 수 있는데, 단일 요소를 처리하는 방법에 대해서는 설명되어 있지 않아서 혼란스러웠다. 이에 대해 알아보다가 깃허브 이슈를 마주했다.
school-ui는 다른 앱에서 공유하지 않는데 왜 libs 디렉토리에 넣어야 할까요?
그것은 우리의 의도가 아닙니다.
libs
에서 공통/공유를 가질 수 있지만특정 앱 사용 또는 특정 기능을 위한 라이브러리
를 가질 수도 있습니다. 대규모 팀과 함께 작업하는 경우 이 조직은 사람들이 독립적으로 작업할 수 있도록 도와줍니다. 또한 기능/앱 특정 라이브러리에서 모듈을 지연 로드할 수 있습니다. 영향을 받는 또 다른 이점은 테스트 및 영향을 받는 빌드입니다. 각 기능에 대해 더 작은 라이브러리가 있는 경우 하나의 라이브러리에서 변경이 발생할 때마다 테스트하거나 다시 빌드할 필요가 없습니다. 내가 당신의 질문에 대답하기를 바랍니다.
즉, 꼭 공통요소만 libs
에 넣는 것이 아니라, 단일요소도 libs
에 넣고 사용하라는 것 같다.
이에 대한 libs
에 대한 폴더 구조 설명이 있다.
Code Organization & Naming Conventions
그래서 Nx는 app보다, libs 폴더 구조를 더 잘 짜야하고 구축을 잘 해야한다고 한다. (또 그만큼 복잡하다.)
두 프로젝트를 조화롭게 사용하기 with Nx
1. Nx React-native
공식문서에서 친절하게 잘 알려주고 있다. React-native with Nx
2. Nx React-native Migrating
1번의 경우, RN이나 React 등 라이브러리들이 최신 버전으로 빌드되기 때문에 기존 RN을 모노레포로 가져와 마이그레이션 할 때 버전을 유의깊게 보고 가져와야 에러가 나지 않는다. 0.6x버전과 0.7x버전 react-native의 의존성도 상이하기 때문에 라이브러리 정리는 필수적이다.
- 기존 RN을 nx apps에 복사, 붙여넣어준다.
이 예에선 기존 프로젝트의 이름이
my-native-app
인 것으로 한다.
- root/apps/my-native-app
node module까지 복사해서 가져오면 시간이 오래 걸리니 삭제해서 가져오는 편이 더 빠름.
- 기존 RN에 있던 package.json의 의존성들을 root의 package.json에 옮겨적는다. root/apps/my-native-app/package.json의 의존성들은 '*' 처리를 해주자.
없는 라이브러리는 빌드할 때 자동으로 의존성을 추가해주는 것 같은데, 모든 것을 추가해주진 않아서 마이그레이션 할 때는 이처럼 하는 것이 좋다.
"dependencies": {
// ...
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-masked-view/masked-view": "0.2.6",
"@react-navigation/bottom-tabs": "^6.2.0",
}
"dependencies": {
// ...
"@react-native-async-storage/async-storage": "*",
"@react-native-masked-view/masked-view": "*",
"@react-navigation/bottom-tabs": "*",
}
다른 웹 프레임워크와는 달리 react-native는 Nx가 통합 저장소
구조로 가져감에도 불구하고 프로젝트 내에 package.json을 두고 있다. 이는 react-native의 자동연결 기능 때문인 것 같다.
- apps/my-native-app/metro.config.js에 다음처럼 nrwl 모듈을 연결해준다.
const { withNxMetro } = require('@nrwl/react-native');
const { getDefaultConfig } = require('metro-config');
module.exports = (async () => {
const {
resolver: { sourceExts, assetExts },
} = await getDefaultConfig();
return withNxMetro({
transformer: {},
resolver: {},
// ...
});
})();
- ios 코드를 업데이트
AppDelegate.m
파일을 열기jsBundleURLForBundleRoot:@"index"
를jsBundleURLForBundleRoot:@"apps/my-native-app/index"
으로 바꿔써주기Xcode workspace를 열기
Build Phases
클릭Bundle React Native code and images
아래에 ENTRY_FILE 를 다음처럼 써준다.
export NODE_BINARY=node
export ENTRY_FILE=./apps/my-native-app/index.js
../node_modules/react-native/scripts/react-native-xcode.sh
- android 코드를 업데이트
- MainApplication.java 파일 열기
- getJSMainModuleName() 에서
"index"
를apps/my-native-app/index
로 변경
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
// ...
@Override
protected String getJSMainModuleName() {
return "apps/my-native-app/index";
}
};
// ...
}
- 경로 설정 마무리하기
{
"extends": "../../tsconfig.base.json"
}
{
"projects": {
"my-native-app": {
"tags": []
}
}
}
{
"version": 1,
"projects": {
"my-native-app": {
"root": "apps/my-native-app",
"sourceRoot": "apps/my-native-app/src",
"projectType": "application",
"schematics": {},
"architect": {
"start": {
"builder": "@nrwl/react-native:start",
"options": {
"port": 8081
}
},
"run-ios": {
"builder": "@nrwl/react-native:run-ios",
"options": {}
},
"bundle-ios": {
"builder": "@nrwl/react-native:bundle",
"outputs": ["apps/my-native-app/build"],
"options": {
"entryFile": "apps/my-native-app/index.js",
"platform": "ios",
"bundleOutput": "dist/apps/my-native-app/ios/index.bundle"
}
},
"run-android": {
"builder": "@nrwl/react-native:run-android",
"options": {}
},
"build-android": {
"builder": "@nrwl/react-native:build-android",
"outputs": [
"apps/my-native-app/android/app/build/outputs/bundle",
"apps/my-native-app/android/app/build/outputs/apk"
],
"options": {}
},
"bundle-android": {
"builder": "@nrwl/react-native:bundle",
"options": {
"entryFile": "apps/my-native-app/index.js",
"platform": "android",
"bundleOutput": "dist/apps/my-native-app/android/index.bundle"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["apps/my-native-app/**/*.{js,ts,tsx}"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "apps/my-native-app/jest.config.js",
"passWithNoTests": true
}
}
}
}
}
}
- 빌드
npx nx run-ios my-native-app
npx nx run-android my-native-app
3. Nx NextJs
공식문서에서 친절하게 잘 알려주고 있다. Next with Nx
4. React-native 기반 컴포넌트를 NextJs에 붙이기
설정해주지 않으면 다음과 같은 터미널 에러를 마주하게 된다.
> import typeof AccessibilityInfo from './Libraries/Components/AccessibilityInfo/AccessibilityInfo';
- 이렇게 하려면
react-native-web
을 써야된다. 컴포넌트를 Web에 붙이기 위함이다. 앞서 말했듯이, 이러한 라이브러리들은 root에 설치하면 된다.
npm install --save react-native-web
npm install --save-dev babel-plugin-react-native-web
yarn add react-native-web
yarn add --dev babel-plugin-react-native-web
- NextJs 프로젝트 내의
.babelrc
에 다음 코드를 붙인다. 파일이 없다면 만들어주자.
{
"presets": [
[
"@nrwl/next/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": [["react-native-web", { "commonjs": true }]]
}
- _document.tsx에 다음 코드를 붙여준다.
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps,
} from 'next/document';
import { AppRegistry } from 'react-native';
import { ServerStyleSheet } from 'styled-components';
export default class CustomDocument extends Document {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
AppRegistry.registerComponent('main', () => Main);
const originalRenderPage = ctx.renderPage;
const sheet = new ServerStyleSheet();
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
enhanceComponent: (Component) => Component,
});
const intialProps = await Document.getInitialProps(ctx);
const styles = sheet.getStyleElement();
return { ...intialProps, styles };
}
render() {
return (
<Html>
<Head>{this.props.styles}</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Nx로 개발하는데 도움되는 커맨드들
1. 글로벌로 Nx 설치하기
sudo npm install -g nx
2. 의존성 그래프로 확인하기
nx graph
3. Nx Console 설치하기
Reference
동영상
- 일백개 패키지 모노레포 우아하게 운영하기
- yarn berry + lerna 조합
- Library Only Monorepo + Service Monorepo 조합
- 모노레포_마이크로 아키텍처를 지향하며
- Turborepo
- How to use React Native with Nx in monorepo
- Nx