12월 프리온보딩 프론트엔드 챌린지 - 모노레포 시스템
· 약 12분
12월 프리온보딩 프론트엔드 챌린지 수강 도중 애매하게 알고 있었거나 새로이 알게된 정보 정리합니다.
모노레포 도입 케이스
1. B2C, B2B, admin를 하나로
- 멀티레포를 모노레포로 가져가며 같은 설정을 반복하고 싶지 않을 때 도입
- 단단히 하나로 되어있는 git 저장소를 볼 수 있다.
2. monolith -> Microservice로 전환
- 기존에는 한 패키지 안에 여러 개의 서 비스가 있었음
- 이를 한 패키지에 하나의 서비스로 갖고 가겠다
예를 들어...
- 한 페이지 안에 menu가 몇 개 안 돼보이지만, 그 내부를 보면 수많은 서비스가 같이 있다.
- 하나의 거대한 빌딩을 보는 듯한 page folder 때문에 배포 시간도, 서비스 속도도, 트래픽도 점차 감당이 되지 않음.
- 라이브러리 하나가 몇 메가짜리인데, 한 페이지를 위해 거대한 라이브러리를 설치해버리면 다른 서비스에도 영향이 가고 맘.
- 그리하여 마이크로서비스를 택하게 된다.
즉, 모노레포란?
"여러 개의 개별 프로젝트를, 잘 정의된 관계를 통해서 하나의 Repo에 담은 것" "Monorepo ≠ Monolith"
yarn
yarn을 사용하여 모노레포를 구축하는 방법은 두 가지가 있다. yarn1, yarn berry가 그것이다.
yarn1을 사용하여 모노레포를 구축하는 경우는 별로 없고, yarn berry로 가는 경우가 많은 듯 하다.
yarn1
1. root package.json
두 줄의 코드가 yarn1의 모노레포 핵심이다.
package.json
{
"private": true,
"workspaces": ["packages/*"]
}
2. packages 폴더 하위에 workspace 생성하기
mkdir packages
mkdir ./packages/common
mkdir ./packages/server
1) common
이제 공통 모듈을 만들어 볼 것이다.
- packages/common/package.json 생성
./packages/common/package.json
{
"name": "@study/common",
"version": "1.0.0", // 버전 명시 해주자
"main": "index.js", // main을 꼭 적어주자
"license": "MIT"
}
질문
package name에 @를 같이 넣어주는 이유가 궁금해요 컨벤션일까요?
이는 관습입니다. 패키지 네임이 겹치는 일이 있을 수도 있기 때문에 @를 씁니다. 보통 회사이름을 앞에 쓰거나 본인의 id를 씁니다.
- packages/common/index.js 생성
module.exports = () => {
console.log('hello from common');
};
2) server
서버 프로젝트에서 common 모듈을 쓰기 위해 기초 세팅을 해준다.
- packages/server/package.json 생성
./packages/server/package.json
{
"name": "@study/server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "node index.js"
},
"license": "MIT"
}
- packages/server/index.js 생성
packages/server/index.js
// server 프로젝트 안에서 common 모듈을 가져다 쓸 수 있도록 한다. 아직 의존하기 전이므로 모듈을 찾을 수 없을 것이다.
const foo = require('@study/common');
console.log('hello from server');
foo();
common 폴더는 공통으로 가져갈 요소이고, 이제 server가 common 프로젝트에 의존하게 만들 것이다.
3. server 가 common 에 의존하게 하기
yarn1은 버 전을 뒤에 붙여주어야 한다.
yarn workspace @study/server add @study/common@1.0.0
- packages/server/package.json
./packages/server/package.json
{
"name": "@study/server",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@study/common": "1.0.0"
},
"scripts": {
"dev": "node index.js"
},
"license": "MIT"
}
root에서 실행해보기
yarn workspace @study/server dev
yarn berry
1. yarn을 yarn berry로 전환하기
// yarn 버전 확인
yarn -v // 1.22.17
// yarn 버전 변경
yarn set version berry
// 또는
yarn set version stable
// yarn 버전 확인
yarn -v // 3.3.0
// project 폴더 생성
mkdir yarn-berry-workspace
cd yarn-berry-workspace
2. yarn workspace 패키기 만들기
// packages 디렉토리 만들기 / 루트 초기화
yarn init -w
3. root package.json파일 수정
package.json
{
"name": "yarn-berry-workspace-test",
"packageManager": "yarn@3.3.0",
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
- apps : service 모음
- packages: 참조용. 공통 ui, 공통 함수 등… 모듈을 모아두는 곳.
4. apps 폴더에 create next-app 프로젝트 추가
- next project 설치
// 1. create-next-app 프로젝트 생성
cd apps
yarn create next-app
- nextjs 프로젝트에서 package.json name 수정하기
apps/wanted/package.json
{
"name": "@wanted/web",
"version": "1.0.0",
"main": "index.js"
// ...
}
- pnp.cjs에서 정보를 갱신해줘야 하므로 root에서 상태를 갱신해준다.
cd ..
yarn
yarn workspace @wanted/web run dev
팁
최대한 루트 경로에서 터미널을 입력해라.
- 의존성들을 루트에서 통합으로 관리하기 위함
- 프로젝트 내부에서 yarn 실행 시, 의도치 않은 동작이 발생할 수 있음.
- 공통으로 가져갈 libs 모듈 생성
- packages/libs 생성
cd packages/libs
yarn init
packages/libs/package.json
{
"name": "@wanted/libs",
// version과 main을 명시
"version": "1.0.0",
"main": "./src/index.ts",
"packageManager": "yarn@3.3.0"
}
packages/libs/src/index.ts
export const sayHello = () => {
return 'hello from lib';
};
이제 libs 에 타입스크립트를 추가하고 nextjs에서 libs(TS)를 의존하게 할 것이다.
root에서
// lib에 ts 추가
yarn workspace @wanted/libs add typescript -D
// next web project에서 libs 의존(추가)
yarn workspace @wanted/web add @wanted/libs
packages/libs/package.json
{
"name": "@wanted/lib",
"version": "1.0.0",
"private": true,
"main": "./src/index.ts",
"dependencies": {
"typescript": "^4.9.3"
}
}
packages/libs/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"target": "ESNext",
"lib": ["ESNext"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "./src",
"noEmit": false,
"incremental": true,
"resolveJsonModule": true,
"paths": {}
},
"exclude": ["**/node_modules", "**/.*/", "./dist", "./coverage"],
"include": ["**/*.ts", "**/*.js", "**/.cjs", "**/*.mjs", "**/*.json"]
}
apps/wanted/package.json
{
"name": "@wanted/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@wanted/libs": "workspace:^",
"eslint": "8.28.0",
"eslint-config-next": "13.0.4",
"next": "13.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.3"
}
}
팁
project는 모듈이 아니므로 main이 없다.
5. ts error 바로 잡기
./apps/wanted/pages/index.tsx에서 ts error가 나기 시작한다.
yarn berry는 npm과 모듈을 불러오는 방식이 다르기 때문.
yarn add -D typescript
yarn dlx @yarnpkg/sdks vscode
6. yarn PnP 사용을 위한 vscode extension 설치: arcanis.vscode-zipfs
arcanis.vscode-zipfs 설치 후, 다음 커맨드 추가
.vscode/extensions.json
{
"recommendations": ["arcanis.vscode-zipfs"]
}
- 이후 vscode 우측 하단에 allow 대화상자가 뜨면 허용
안 뜬다면 아래처럼 해본다.
- yarn dlx @yarnpkg/sdks vscode
- TypeScript file 아무거나 하나 열어본다.
- ctrl+shift+p
- "Select TypeScript Version" 선택
- "Use Workspace Version"
7. apps/wanted/pages/index.tsx 파일에서 @wanted/libs 모듈 사용해보기
apps/wanted/pages/index.tsx
import {sayHello} from '@wanted/libs';
export default function Home() {
return (
{sayHello()}
)
}
yarn workspace @wanted/web run dev
react ui 공통 모듈로 가져가기
- packages에 ui 추가
cd packages/ui
yarn init
- package.json 파일을 열어서 name을
@wanted/ui로 변경.
- react dependency 설치
cd ../../
yarn
yarn workspace @wanted/ui add typescript react react-dom @types/node @types/react @types/react-dom -D
- tsconfig 설정
packages/ui/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./src",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"jsx": "react-jsx",
"noEmit": false,
"incremental": true
},
"exclude": ["**/node_modules", "**/.*/", "dist", "build"]
}
packages/ui/src/index.ts,packages/ui/src/Button.tsx파일 생성
packages/ui/src/Button.tsx
import { ButtonHTMLAttributes, MouseEventHandler, ReactNode } from 'react';
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
children: ReactNode,
onClick?: MouseEventHandler<HTMLButtonElement>,
};
const Button = (props: ButtonProps) => {
const { children, onClick, ...other } = props;
return (
<button type='button' onClick={onClick} {...other}>
{children}
</button>
);
};
export default Button;
packages/ui/src/index.ts
export { default as Button } from './Button';
packages/ui/package.json에 main 추가
packages/ui/package.json
{
"name": "@wanted/ui",
"packageManager": "yarn@3.3.0",
"main": "src/index.ts",
"devDependencies": {
"@types/node": "^18.11.11",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^4.9.3"
}
}
apps/wanted에서packages/ui사용
root 에서 @wanted/ui 의존성 설치
yarn workspace @wanted/web add @wanted/ui
apps/wanted/pages/index.tsx파일 수정
토글 접기/펼치기
import { sayHello } from '@wanted/lib';
import { Button } from '@wanted/ui'; //추가
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';
export default function Home() {
return (
<div className={styles.container}>
// ...
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<h2>{sayHello()}</h2>
<Button>Hello From @wanted/ui</Button> // 추가
// ...
</div>
);
}
@wanted/web에서 javascript로 변환(transpile)
next-transpile-modules 설치
yarn workspace @wanted/web add next-transpile-modules
apps/wanted/next.config.js파일 수정
apps/wanted/next.config.js
// @wanted/ui 패키지를 tranpile 시킨다.
const withTM = require('next-transpile-modules')(['@wanted/ui']);
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
};
module.exports = withTM(nextConfig);
- 실행
yarn workspace @wanted/web dev