| 패키지 매니저 | yarn |
|---|---|
| 프레임워크 | react+typescript |
| 모노레포 도구 | yarn workspace |
| 번들러 | vite |
| 테스트 | jest |
| 디자인시스템 | storybook |
monorepo-test / # 프로젝트별 앱 폴더
├── apps/
│ ├── normal/
│ └── group/
├── packages/ # 공통 요소 관리
│ ├── common/
│ │ └── img/
│ ├── ui/
│ └── utils/
│ └── test/
├── package.json
├── yarn.lock
└── .yarnrc.yml
mkdir monorepo-test
cd monorepo-test
mkdir -p apps/normal apps/group
mkdir -p packages/common/img packages/ui packages/utils/test
apps 폴더 안에 있는 두개의 프로젝트(normal, group)에 Vite + React + TypeScript 설치
cd apps/normal
yarn create vite ./
yarn
yarn dev
cd ../group
yarn create vite ./
yarn
yarn dev
yarn create vite ./ : 현재 파일에 vite 프로젝트 설치
개발환경 선택창 Select a framework > React, Select a variant > TypeScript 선택

root/.gitignore파일 작성git push를 하려면 미리 작성해둬야 git에 필요없는 파일이 올라가지 않는다.
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
vscode/
node_modules/
.pnp
.pnp.js
# testing
coverage/
# production
build/
dist/
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Storybook
*storybook.log
root/package.json파일 작성workspace 설정에서 가장 핵심되는 파일이다.
root/package.json에 공통으로 공유할 패키지에 대한 정보와 workspace에 대한 설정을 한다.
{
"name": "monorepo-test",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"normal": "yarn workspace normal",
"group": "yarn workspace group"
}
}
📌private: true *필수 입력
배포 방지
모노레포는 여러 패키지를 포함하고 있기 때문에, 모든 패키지가 외부에 배포될 필요가 없을 때 private: true가 유용합니다.
📌workspaces *필수 입력
모노레포로 관리할 프로젝트 명을 입력한다.
"workspaces": [
"apps/*",
"packages/*"
],
📌script *필수 입력
root package.json에 스크립트를 설정해주지 않으면 프로젝트를 실행할 때마다 매번 cd ../ 으로 폴더를 옮겨다녀야한다.
(이 설정을 몰랐을 때 내가 그렇게 작업했다.)
문법
"scripts": {
"사용할 이름": "yarn workspace {개별 프로젝트의 package.json 파일의 name 프로퍼티}",
},
실제 적용 예시
"scripts": {
"normal": "yarn workspace @doc/normal",
"group": "yarn workspace @doc/group",
},
적용 후 사용 예시
yarn normal start
yarn normal add {package}
yarn normal remove {package}
yarn normal build
yarn normal dev
yarn group start
yarn group lint
yarn group add {package}
yarn group remove {package}
이렇게 각 프로젝트의 package.json의 script를 root 경로에서 경로 이동 없이 바로 사용할 수 있다.
scripts의 key 값인 normal과 group을 그때 사용하는 용도이다.
📌dependencies 필수 아님
중복되는 패키지가 있다면 프로젝트 패키지의 중복을 막기 위해 root에 분리한다.
typescipt의 tsconfig.json을 공유하기 위해 설치했다.
yarn add -D typescript -W
-W 명령어 : 모노레포의 모든 워크스페이스에 대해 실행된다.root/tsconfig.json 파일 작성yarn workspace에서는 tsconfig.json 작성 방식중 2가지를 선택할 수 있다.
2번은 1번보다 설정이 복잡하지만 공통되는 설정을 root에서 관리할 수 있다는 장점이 있어서 2번 설정으로 선택했다.
{
"compilerOptions": {
"removeComments": true,
"strict": true,
"allowJs": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"composite": true,
},
"include": ["apps/**/*", "packages/**/*"],
"references": [
{
"path": "./apps/group"
},
{
"path": "./apps/normal"
}
],
"exclude": [
"node_modules",
"**/build",
"**/dist",
"**/__tests__",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.stories.tsx",
"**/.storybook",
"coverage",
"storybook-static",
"public",
"**/eslint.config.js",
"**/jest.config.js"
]
}
📌compilerOptions
두 프로젝트가 공통으로 사용하는 compilerOptions(TypeScript 컴파일러가 사용하는 다양한 설정)을 설정한다.
removeComments: truestrict: truenoImplicitAny, strictNullChecks, strictFunctionTypes 등)을 모두 포함한 설정입니다. 이를 통해 코드의 타입 안전성을 최대한으로 보장한다.noImplicitAny: trueany 타입을 허용하지 않도록 설정한다. 타입이 명시되지 않으면 any로 간주되는데, 이를 방지하여 모든 변수와 매개변수에 명시적으로 타입을 지정하도록 강제한다.strictNullChecks: truenull과 undefined를 엄격히 구분한다. 즉, null이나 undefined가 될 수 있는 값에 대해 타입이 명시되지 않으면 컴파일 오류가 발생한다. 이를 통해 런타임 오류를 줄일 수 있는 강력한 타입 안전성을 제공한다.allowJs: true.js)도 TypeScript 프로젝트 내에서 허용되도록 설정한다. 이를 통해 JavaScript와 TypeScript가 혼합된 프로젝트에서도 TypeScript가 문제없이 작동한다.resolveJsonModule: trueimport하여 사용할 수 있다.noFallthroughCasesInSwitch: truecase 구문이 다른 case로 이어지는 것(fallthrough)을 방지합니다. 의도적으로 break가 생략된 경우라도 오류를 발생시킵니다."forceConsistentCasingInFileNames": true파일 이름의 대소문자 일관성을 강제한다. 즉, 대소문자를 구분하여 파일 이름을 다르게 참조하는 것을 방지한다.
import { myComponent } from './MyComponent'
import { myComponent } from './mycomponent'
동일하게 취급하지 않습니다.
composite: truecomposite: true는 TypeScript 프로젝트 간 참조를 가능tsconfig.json에 명시적인 composite 설정tsconfig.json 파일에서 하위 프로젝트에 대한 references를 설정할 때, TypeScript는 모든 참조된 프로젝트가 composite 모드인지 확인합니다. 따라서 루트 tsconfig.json이 하위 프로젝트들을 참조하고 있다면, 루트 파일도 composite 모드를 설정해 빌드 의존성을 명확히 해야 합니다.composite 모드를 활성화하면 TypeScript는 각 하위 프로젝트의 빌드 결과를 .tsbuildinfo 파일로 저장하고, 이를 바탕으로 변경된 파일만 다시 빌드하여 효율성을 높입니다. 루트 파일에서 하위 프로젝트들을 제대로 인식하기 위해 루트에서도 composite을 활성화해야 합니다.📌references
📌exclude
"node_modules", // 패키지 매니저가 설치한 외부 모듈
"**/build", // 빌드된 파일 폴더 (예: Webpack, Vite 빌드 파일)
"**/dist", // 배포용 빌드 결과물 폴더
"**/__tests__", // 테스트 코드 폴더
"**/*.test.ts", // 테스트 파일 (확장자는 프로젝트에 맞게 조정 가능)
"**/*.test.tsx",
"**/*.spec.ts", // 테스트 스펙 파일
"**/*.spec.tsx",
"**/*.stories.tsx", // 스토리북 파일
"**/.storybook", // 스토리북 설정 파일
"coverage", // 테스트 커버리지 폴더
"storybook-static", // 스토리북 빌드 파일
"public",
"**/eslint.config.js"위 내용을 제외하고 react에 종속된 설정은 개별 tscofig에 둔다.
apps/프로젝트/tsconfig.json 파일 작성{
"extends": "../../tsconfig.json", // 추가
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
references : tsconfig.json 파일이 tsconfig.node.json과 tsconfig.app.json을 참조하고 있음을 나타낸다.root에 tsconfig 파일로 compoilerOption을 분리했을 경우 프로젝트 레벨에서는 extends 프로퍼티의 값으로 root tsconfig 파일의 경로를 반드시 입력해서 연결해야한다.

이 문제는 tsconfig.json 파일이 eslint.config.js와 같은 JavaScript 파일을 빌드 대상으로 인식하고, 그로 인해 TypeScript가 이 파일을 처리하려다 발생하는 충돌이다. tsconfig.json의 exclude 섹션에 eslint.config.js 파일을 추가하여 TypeScript가 이 파일을 컴파일하려고 시도하지 않게 되며, 무시하도록 설정하면 관련된 에러가 해결된다.
해결 : root/tsconfig.json 수정
{
"compilerOptions": {
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strict": true,
"allowJs": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true
},
"include": ["apps/**/*", "packages/**/*"],
"references": [
{
"path": "./apps/group-version"
},
{
"path": "./apps/normal-version"
}
],
"exclude": [
"node_modules",
"**/build",
"**/dist",
"**/__tests__",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.stories.tsx",
"**/.storybook",
"coverage",
"storybook-static",
"public",
**"**/eslint.config.js" // 추가**
]
}

composite: true는 TypeScript 프로젝트 간 참조를 가능하게 하며, 이때 declaration: true를 통해 타입 선언 파일(.d.ts)을 생성해 타입 정보를 공유한다. noEmit: true는 결과물 생성 없이 타입 체크만 진행하며, allowImportingTsExtensions: true는 확장자를 명시적으로 사용할 수 있게 하는데, noEmit 옵션이 켜져 있을 때만 유효하다. noEmit을 제거하고 확장자 설정을 삭제하면 정상적으로 동작하게 된다.
해결 : 📁apps/normal, 📁apps/group tsconfig.app.json과 tsconfig.node.json 수정
composite: true 추가"declaration": true 추가noEmit: true 제거allowImportingTsExtensions: true 제거{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
// "allowImportingTsExtensions": true, // 제거
"isolatedModules": true,
"moduleDetection": "force",
// "noEmit": true, // 제거
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"composite": true, // 추가
"declaration": true // 추가
},
"include": ["src"]
}
📌composite: true 추가
tsconfig.json에서 composite: true를 추가함으로써 TypeScript 프로젝트가 "프로젝트 참조(Project References)"를 사용할 수 있다. 이는 프로젝트의 일부가 다른 프로젝트에 의존할 때 사용된다. composite 옵션이 활성화되면 TypeScript는 declaration 파일을 생성하고, 프로젝트가 다른 프로젝트에 참조될 수 있도록 준비한다. 이 옵션이 없으면 references를 사용한 프로젝트 참조가 제대로 동작하지 않아 에러가 발생할 수 있습니다.
📌"declaration": true 추가
TypeScript에서 타입 선언 파일(.d.ts)을 생성하기 위해 사용된다. 이 옵션이 활성화되면 TypeScript는 컴파일 과정에서 .ts 또는 .tsx 파일을 변환할 때, 해당 파일의 타입 정보를 포함하는 .d.ts 파일을 함께 생성한다.
프로젝트 참조와 composite: composite 옵션을 사용할 때 declaration: true가 필수적이다. 이는 프로젝트가 서로 참조할 때 타입 정보를 기반으로 올바른 종속성을 유지할 수 있게 해준다. 따라서, "declaration": true는 프로젝트 간 타입 정의를 공유하거나, 외부 프로젝트에서 올바르게 타입을 참조할 수 있도록 하기 위한 중요한 설정이다.
📌noEmit: true 제거
noEmit: true는 TypeScript가 코드를 컴파일해 JS 파일을 생성하지 않고, 타입 검사만 수행하도록 한다.composite 프로젝트(다중 모듈 간 참조를 사용하는 설정)에서는 코드의 타입 선언 파일(.d.ts)을 생성해야 하는데, noEmit: true가 있으면 이 선언 파일도 생성되지 않아서 composite 설정이 정상적으로 작동하지 않는다.noEmit: true 옵션을 제거해, TypeScript가 타입 선언 파일(declaration 파일 등)을 생성하도록 한다.📌allowImportingTsExtensions: true 삭제
allowImportingTsExtensions: true는 TypeScript가 .ts나 .tsx 파일을 명시적으로 확장자까지 써서 가져와야 하도록 요구하는 설정이다..ts, .tsx 등을 자동으로 인식해서 처리할 수 있기 때문에, 굳이 확장자를 쓰지 않아도 충분히 파일을 인식한다.allowImportingTsExtensions: true를 삭제하고, TypeScript가 기본 방식대로 확장자를 생략해도 파일을 인식하도록 한다.즉, 컴파일 결과물을 만들지 않는 noEmit: true일 때만 allowImportingTsExtensions: true를 허용하여, 타입 체크 과정에서 확장자 명시를 요구할 수 있다.
noEmit: true와 allowImportingTsExtensions: true의 관계
noEmit: true로 타입 검사만 수행할 때는 확장자를 명시하는 설정을 추가해도 괜찮지만, 실제로 빌드를 할 때는 확장자를 생략하는 방식이 더 일반적이다.packages/공통요소/package.json 설정packages에는 공통적으로 사용할 요소(Common, UI, Utils 등)를 모아서 관리한다. 후에 packages의 각 패키지를 설치해서 apps 내의 프로젝트에서 사용할 것이다.
common, utils, ui패키지를 사용하기 위해서 각 폴더의 package.json에서 의존성을 설정한다.
각 폴더는 apps 프로젝트에서 사용할 패키지가 된다.
// packages/common/package.json
{
"name": "@test/common",
"version": "1.0.0",
"main": "index.ts",
}
// packages/ui/package.json
{
"name": "@test/ui",
"version": "1.0.0",
"main": "index.ts",
}
// packages/utils/package.json
{
"name": "@test/utils",
"version": "1.0.0",
"main": "index.ts",
}
📌 "name"
💡심볼릭 링크(Symbolic Link)
파일이나 폴더의 별명을 만들어 둔다고 생각하면 된다. 모노레포에서는 각 패키지를 설치할 때 실제 파일 복사본이 아닌 별명(=심볼릭 링크)으로 연결된다. 이렇게 하면 중복 파일이 생기지 않고, 모노레포 내에서 패키지들이 서로를 쉽게 참조할 수 있다.
예를 들어서 packages/common 에 @test/common 이라는 이름을 붙이면, 모노레포 구조 내 다른 프로젝트에서도 @test/common이라는 별명으로 이 패키지를 쉽게 쓸 수 있다.
yarn workspace normal add @test/common
💡@(스코프)
패키지 이름에 @test/common 처럼 스코프를 붙이면, 패키지들을 그룹으로 묶을 수 있다.
yarn workspace normal add @test/common
패키지를 설치하면 node_modules에 심볼릭 링크가 생성된다.

스코프 설정을 통해 node_modules에 생성되는 심볼릭 링크 폴더 구조가 스코프별로 그룹화된다.
📌 "main"
패키지의 진입점을 지정하는데 사용된다. 이 필드를 통해 패키지를 사용할 때 가장 먼저 불러올 파일을 정의한다. main 필드를 설정하지 않으면 기본적으로 index.js 파일을 찾는다. 하지만 index.js 파일이 없으면 도구가 여러 파일을 검색하게 되어 비효율적이기 때문에 필수는 아니지만 설정하는 것을 권장한다. main 필드를 명시해 두면, 패키지를 사용하는 도구나 개발자가 특정 진입 파일을 빠르게 찾을 수 있어 성능과 유지 보수성 측면에서 유리하다. 특히 모노레포 외부 환경이나 타 도구와의 호환성도 높아진다.
💡애플리케이션 vs 라이브러리
애플리케이션 (apps 폴더)
해당 위치의 폴더는 실제로 실행 가능한 애플리케이션을 포함한다.
각 애플리케이션은 독립적으로 실행되며, 다른 애플리케이션에서 직접적으로 import될 필요가 없다.
따라서 애플리케이션은 자체적인 실행 환경을 갖추고 있기 때문에 main 필드를 설정할 필요가 없다. 대신 애플리케이션의 구조와 실행 파일을 통해 애플리케이션의 진입점이 결정된다.
라이브러리 (packages 폴더)
반면, packages 폴더에 위치한 패키지들은 재사용 가능한 컴포넌트, 유틸리티 등을 제공한다. 이러한 패키지들은 다른 애플리케이션이나 패키지에서 재사용될 수 있다. 라이브러리 패키지에는 main 필드를 설정하여 명확한 진입점을 지정하는 것이 중요하다. 이렇게하면 해당 패키지를 사용할 때 어떤 파일이 가장 먼저 로드될지를 정의할 수 있다.
예시
index.ts를 각 패키지의 진입파일로 지정
// packages/common/index.ts
export { default as bgImg } from './img/bgimg.jpg';
불러오는 방식
1. 패키지에서 내보낸 값 가져오기
import { imgPath } from '@doc_packages/common';
import bgimg from '@doc_packages/common/img/bgimg.jpg';
root/package.json script 필드 추가{
"name": "monorepo-test",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"normal": "yarn workspace normal",
"group": "yarn workspace group",
"common": "yarn workspace @test/common", // 추가
"ui": "yarn workspace @test/ui", // 추가
"utils": "yarn workspace @test/utils" // 추가
},
"devDependencies": {
"typescript": "^5.6.3"
}
}
apps/프로젝트 에서 packages/common/img 에 있는 이미지를 사용하고 싶을 경우pakages/common/img에 bgImg.jpg 이미지를 하나 넣고, apps/normal 프로젝트에서 사용해보자
1. 패키지 설치
apps/normal 에 packages 내에서 사용하고 싶은 패키지를 설치한다.
yarn normal add @test/common@1.0.0
💡스코프 패키지("name": "@doc_packages/common") 설치 시 주의할 점
yarn berry 작업 시에는 패키지가 정상적으로 설치되었지만,
yarn classic은 패키지 설치가 동작하지 않았다.
Yarn Berry (Yarn 2 이상)
PnP(Plug'n'Play) 모드를 지원하여 패키지를 노드 모듈 폴더가 아닌 Yarn의 PnP 캐시 디렉토리에서 관리한다. 이로 인해, 스코프 패키지(예: @packages/common)가 로컬 패키지로 인식되고, 종속성이 효과적으로 해결된다. 기본적으로 Yarn Berry는 모든 스코프 패키지를 자동으로 링크하여 사용할 수 있도록 처리한다.
Yarn Classic
노드 모듈 폴더를 사용하여 종속성을 설치하며, 스코프 패키지를 처리할 때 해당 패키지가 실제로 공용 레지스트리(npm, yarnpkg)에 존재하는지 확인한다. 이로 인해 로컬에 존재하는 스코프 패키지를 찾지 못하고, 오류가 발생할 수 있다.
해결 방법
https://github.com/yarnpkg/yarn/issues/4878


Yarn은 버전이 없을 때 항상 레지스트리에서 해결하려고 시도하기 때문에 설치 시 패키지 뒤에 버전을 입력하면 정상 동작한다.

설치 후 apps/normal package.json에서 설치된 걸 확인할 수 있다.
2. apps/normal에서 import 후 사용
import "./App.css";
import bgimg from "@test/common/img/bgimg.jpg";
function App() {
return (
<>
<img src={bgimg} alt="background image" />
</>
);
}
export default App;
개발모드를 실행하서 적용을 확인한다.
yarn normal dev
모노레포 프로젝트에서 여러 앱이 공용으로 사용하는 packages/utils 에 있는 함수를 별칭으로 사용하는 설정을 단계별로 구성한다. 이 과정에서 tsconfig.json의 paths 별칭 설정과 Vite의 vite-tsconfig-paths 플러그인을 사용하여 절대 경로로 utils 모듈을 불러올 수 있다.
packages/utils 폴더에 함수 파일을 생성한다.
// packages/utils/sumArray.ts
export function sumArray(numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0);
}
root/tsconfig.json 파일에서 baseUrl과 paths 설정을 추가한다.
@utils 별칭은 packages/utils 디렉터리를 참조한다.
{
"compilerOptions": {
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strict": true,
"allowJs": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
**"composite": true,**
**"baseUrl": ".", // 추가
"paths": {
"@utils/*": ["packages/utils/*"] // 추가
}**
},
"include": ["apps/**/*", "packages/**/*"],
"references": [
{
"path": "./apps/group"
},
{
"path": "./apps/normal"
},
**{
"path": "./packages/utils"
}**
],
"exclude": [
"node_modules",
"**/build",
"**/dist",
"**/__tests__",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.stories.tsx",
"**/.storybook",
"coverage",
"storybook-static",
"public",
"**/eslint.config.js"
]
}
compilerOptions에 baseUrl, paths 설정
📌baseUrl
.)를 기준으로 별칭 경로를 시작하도록 설정한다.baseUrl을 설정한 곳이 기준점이된다.📌paths
"@utils/*": ["packages/utils/*"]를 통해 @utils 별칭으로 packages/utils 경로를 참조한다."paths": {"@utils/*": ["./packages/utils/*"]}"paths": {"@utils/*": ["packages/utils/*"]}packages/utils에서 모노레포 루트의 tsconfig.json 설정을 상속받기 위해 extends를 사용한다.
// packages/utils/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true
}
}
extends 에는 root/tsconfig.json의 경로 입력한다.
utils/logSum.ts 함수 생성 후 처음 만든 함수 sumArray를 별칭으로 불러온다.
// packages/utils/logSum.ts
import { sumArray } from "@utils/sumArray";
export function logSum(numbers: number[]): void {
const result = sumArray(numbers);
alert(`배열 [${numbers.join(", ")}]의 합은 ${result}입니다.`);
}
apps/프로젝트에서 utils 패키지 사용하기4-1. 패키지를 설치한다.
apps/normal에 @test/utils 패키지를 설치한다.
yarn normal add @test/utils@1.0.0
4-2. vite-tsconfig-paths 플러그인을 설치한다.
Vite가 TypeScript paths 별칭을 인식하도록 apps/normal에 vite-tsconfig-paths을 설치한다.
yarn normal add vite-tsconfig-paths -D
4-3. vite.config.ts를 설정한다.
vite-tsconfig-paths 플러그인을 Vite 설정 파일에 추가한다.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
**import tsconfigPaths from 'vite-tsconfig-paths'; // 추가**
export default defineConfig({
plugins: [react(), **tsconfigPaths()**], **// 추가**
});
apps/normal에서 utils 함수 사용하기App.tsx에서 logSum 함수 사용한다.
// apps/normal/src/App.tsx
import { logSum } from '@doc_packages/utils/logSum';
import bgimg from '@doc_packages/common/img/bgimg.jpg';
function App() {
return (
<>
<img src={bgimg} alt="bgimg" onClick={() => logSum([1, 2, 3, 4, 5])} />
</>
);
}
export default App;
개발모드를 실행하서 적용을 확인한다.
yarn normal-version dev
