⚡Yarn berry + Turborepo 를 이용한 프론트엔드 Monorepo 구축기 (Part 2)

Yoon Han
LBOX Team
Published in
20 min readFeb 17, 2023

안녕하세요! 엘박스 프론트엔드 엔지니어 한 윤 입니다. 이전 편에서는 모노레포를 구축하기 이전에 필요한 여러 제반 사항들을 세팅해 보았습니다. 본편에서는 본격적으로 모노레포 하위에 속한 workspace 들을 하나씩 구성해보도록 하겠습니다.

LBox 모노레포 예제 GitHub repository

💫 ’nextjs’ workspace 생성하기

지난 글에서 저희가 구축해 볼 모노레포의 전체적인 디렉터리 구조를 알아보았습니다. 이제 본격적으로 모노레포의 하위 workspace 들을 세팅해 보도록 하겠습니다.
그 첫 타자는 Next.js 프레임워크를 사용하는 workspace 입니다.

터미널을 켜시고 monorepo-example/apps 디렉터리로 이동해주세요.

cd apps && pwd
cd apps && pwd 실행 결과

이동하셨다면 아래 명령어를 통해 Next.js 프로젝트를 생성해주세요.
Next.js 에 TypeScript, ESLint 를 추가해서 설치해 보도록 하겠습니다.

yarn create next-app nextjs --typescript --eslint
yarn create next-app nextjs — typescript — eslint 실행 결과

설치 도중 나오는 질문들에는 전부 엔터를 누르고 넘어가도록 하겠습니다.

설치가 정상적으로 잘 되었다면, monorepo-example/apps/nextjs workspace 에 다음과 같은 파일들이 생성된 것을 확인하실 수 있습니다.

monorepo-example/apps/nextjs

이제 개발 서버가 잘 실행 되는지 확인해보겠습니다.
monorepo-example/apps/nextjs 디렉터리에서 yarn dev 명령어를 터미널에서 실행시켜 줍니다.

yarn dev

개발서버 구동 후 http://localhost:3000 URL 에 접속해보면 아래와 같은 에러 문구가 보입니다.

Yarn PnP strict mode 에서 발생하는 에러

이 에러는 Yarn PnP strict mode 에서 발생하는 문제인데요, 명시되지 않은 dependency 에 접근하려고할 때 발생하는 에러입니다. 에러 원인을 조금 더 부연설명하고 넘어가도록 하겠습니다.

[package A] tried to access [package B], but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.

Yarn PnP 시스템은 엄격하기 때문에 package.json 파일에 의존성을 명시하지 않고 접근할 경우 위와 같은 에러가 발생합니다.

사내에서 관리하는 모노레포 내부에서 이런 에러가 발생한다면 의존성을 package.json 파일에 명시적으로 추가해주면 될 일이지만, 만약 해당 문제가 third-party 라이브러리에서 발생한다면 건드리기가 매우 난감합니다.

이럴 경우에 대비해 PnP 시스템은 일종의 우회로를 확보해두었는데, 모노레포 root 에 있는 .yarnrc.yml 파일을 다음과 같이 수정해주면 됩니다.

nodeLinker: pnp

pnpMode: loose <-- 추가

yarnPath: .yarn/releases/yarn-3.3.1.cjs

packageExtensions: <-- 추가
"@next/font@*": <-- 추가
dependencies: <-- 추가
next: '*' <-- 추가

pnpModeloose 로 설정한 경우 PnP 시스템은 package.json 파일에 명시되지 않았는데도 접근되고 있는 의존성들을 packageExtensions 에 선언된 내용을 참조해 fallbackPool 에 넣어두고 관리합니다. 아래 스크린샷처럼요!

fallbackpool 데이터

앞서 작성한 .yarnrc.yml 파일 기준으로는 @next/font 및 이와 관련된 의존성들이 fallbackPool 에 들어갈 것입니다.

.yarnrc.yml 파일을 수정한 뒤에는 반드시 yarn install 명령어를 실행해서 fallbackPool 을 업데이트 해주어야 합니다.

yarn install

추후에도 비슷한 에러 문구를 마주하게 된다면 packageExtensions 필드에 의존성을 명시해주면 되겠습니다!

에러를 고친 후 개발 서버를 재기동하면 정상적으로 작동하는 것을 확인할 수 있습니다.

⚡ Yarn workspaces + Turborepo 세팅하기

❓ Yarn workspaces 란

Yarn workspaces 는 Yarn 에서 제공하는 모노레포 관리 도구입니다.

이전 섹션에서 package.json 파일에 workspaces 필드를 선언하고 값을 넣어준 것을 기억하시나요? 이 부분이 바로 Yarn workspaces 를 설정해주는 부분이었습니다. 파일의 내용을 다시 한번 살펴보도록 하겠습니다.

{
"name": "monorepo-example",
"private": true,
"packageManager": "yarn@3.3.1",
"workspaces": [
"apps/*",
"packages/*"
]
}

workspaces 필드에 담긴 배열의 값들을 보면, apps/* , packages/* 두 가지 값이 있는데요 apps/packages/ 하위의 모든 디렉터리를 workspace 로써 바라보겠다는 의미입니다.

이전 섹션에서 nextjs workspace 를 정상적으로 생성하셨다면, monorepo-example/yarn.lock 파일 내에서 ‘nextjs@workspace’ 라는 키워드로 검색을 해보시기 바랍니다. 그러면 다음과 같은 내용을 확인하실 수 있습니다. (혹시 검색이 되지 않는다면 모노레포 root 디렉터리 위치에서 yarn install 명령을 한 번 실행시켜 주세요)

monorepo-example/yarn.lock

위의 스크린샷 첫줄 앞부분에 보이는 ‘nextjs’ 라는 문자열은 apps/nextjs/package.json 파일의 name 필드 값을 참조한 것입니다. nextjs@workspace workspace 를 apps/nextjs 디렉터리로 mapping 해주고 있네요.

이렇게 Yarn workspaces 설정이 끝났다면 추후에 특정 workspace 에 의존성 설치가 필요한 경우 다음의 명령을 통해서도 패키지를 설치할 수 있게 됩니다.

yarn workspace [workspace-name] add [package-name] [--options]

❓ Turborepo 를 선택한 이유

모노레포 관리 도구를 리서치해보면 Lerna , Nx 등등 여러 도구들이 나옵니다.

저희는 우선 많은 기능이 필요치 않고 초기 세팅에 크게 공수를 들이고 싶지 않았기 때문에 상대적으로 가벼운 도구를 찾아보고 있었는데요, 그 중에서도 Turborepo 가 눈에 띄었습니다.

Turborepo 공식 사이트 feature 소개글

다음의 3가지 부분이 마음에 들었습니다.

  • Next.js 프레임워크를 관리하고 있는 Vercel 사에서 만든 도구라는 점
  • 기존에 있던 프로젝트에 JSON 파일 하나만 생성해주면 쉽게 도입할 수 있다는 점
  • 저희에게 당장 필요할 것으로 보이는 Remote CachingTask pipeline feature 를 지원한다는 점

아래 그림에서는 Turborepo 의 multitasking feature 를 표현하고 있습니다.
Yarn workspaces 는 task 들을 병렬적으로 처리하지 못하지만, Turborepo 는 task 병렬 처리를 지원합니다.

Turborepo 의 multitasking feature 동작 모식도

💿 Turborepo 설치하기

터미널에서 다음의 명령어를 실행해 Turborepo 를 설치해 줍니다.

npm i -g turbo

전역으로 설치해주는 이유는 현재 Turborepo 의 버전(1.7.4)이 Yarn berry PnP mode 를 공식적으로 지원하지 않기 때문입니다.(https://github.com/vercel/turbo/issues/3280)
따라서 이 문제가 해결될 때 까지는 전역적으로 설치해두고 사용하는 것이 좋습니다.

📄 turbo.json 파일 작성하기

모노레포 root 디렉터리에 turbo.json 파일을 하나 생성하고, 다음 내용을 기입해 줍니다.

// turbo.json
{
"$schema": "https://turbo.build/schema.json"
}

$schema 필드는 주어진 URL 로부터 JSON 스키마를 가져와서, 해당 JSON 파일을 작성할 때 VS Code 의 intellisense 기능을 활용할 수 있도록 도와줍니다.

turbo configuration 파일(turbo.json)을 통해 task pipeline 을 상세히 정의할 수 있지만, 본 아티클에서는 Turborepo 를 통한 간단한 script 실행법만을 다루도록 하겠습니다.

🖥️ Turborepo 로 package.json script 실행하기

저희는 이전에 nextjs workspace 의 package script 를 실행시키기 위해 직접 해당 workspace 디렉터리로 이동했었습니다.

이번에는 방식을 바꾸어 Turborepo CLI 명령어를 이용해 모노레포 root 에서 nextjs workspace 의 package script 를 실행해보도록 하겠습니다.

기본적인 명령어 구조는 다음과 같습니다.

turbo run [script-name] --filter=[workspace-name]

script-name 부분에는 turbo.json 파일의 pipeline 필드에 정의된 값들이 들어갈 수 있고, filter 옵션을 통해서는 해당 Turborepo CLI 명령어를 실행할 workspace 를 특정지을 수 있습니다. 백문이 불여일견. 직접 해보도록 하겠습니다.

먼저, Turborepo CLI 에게 저희가 실행하길 원하는 package script 를 알려주어야 합니다. 이것은 turbo.json 파일의 pipeline 필드를 건드려주면 되는데요,

// turbo.json
{
...
"pipeline": {
"dev": {
"cache": false
}
}
}

pipeline 필드 값에 dev 라는 필드를 추가해 주었습니다. 저희는 monorepo-example/apps/nextjs/package.json 안의 ‘dev’ script 를 실행하고 싶기 때문입니다. cache 필드의 값은 Turborepo CLI 가 해당 명령 수행의 결과의 캐싱 여부를 결정하는데요, 필요하다면 true 로 설정해주면 됩니다.

여기까지 설정해두고 모노레포 root 의 package script 에 다음과 같이 작성해줍니다.

// package.json
{
...
"scripts": {
"dev:nextjs": "turbo run dev --filter=nextjs"
}
}

dev:nextjs 라는 script 에 Turborepo CLI 명령어를 연결시켜주었습니다. filter 옵션을 통해 dev 라는 script 를 가진 workspace 들 중에 nextjs workspace 의 dev script 만 실행되도록 필터링 해주었습니다.

이제 모노레포 root 에서 yarn dev:nextjs 명령어를 실행하면 다음과 같이 http://localhost:3000 URL에 개발서버가 구동됩니다.

개발 서버 구동 후 터미널 console

💫 ’storybook’ workspace 생성하기

이제 monorepo-example/packages/ 디렉터리 하위에 storybook workspace를 하나 생성해 보겠습니다.

monorepo-example/packages 디렉터리로 이동한 뒤 CRA 명령어를 통해 React 를 먼저 설치해 봅니다.

yarn create react-app storybook --template typescript --use-pnp

--use-pnp 옵션을 붙여주어야 PnP 모드로 CRA 프로젝트를 구성할 수 있습니다.

설치를 완료하면 monorepo-example/packages/storybook 디렉터리가 생성이 되는데요, 같이 생성된 monorepo-example/node_modules 폴더와 monorepo-example/package-lock.json 파일은 삭제해주시면 됩니다.

monorepo-example/node_modules
monorepo-example/package-lock.json’

이후에 yarn install 을 한 번 더 실행해주셔야 monorepo-example/packages/storybook 디렉터리가 workspace 로 잘 인식됩니다.

yarn install

프로젝트 생성을 마쳤으니 nextjs workspace 처럼 storybook workspace 의 start script 도 turbo 에 등록해 줍니다.

// monorepo-example/packages/storybook/package.json
{
...
"pipeline": {
"dev": {
"cache": false
},
"start": {
"cache": false
}
}
}
// monorepo-example/package.json
{
...
"scripts": {
"dev:nextjs": "turbo run dev --filter=nextjs",
"start:storybook": "turbo run start --filter=storybook"
},
...
}

Turborepo CLI 세팅도 마친 뒤 yarn start:storybook 명령어를 통해 React 개발 서버를 구동하면 아래와 같은 오류가 발생합니다.

개발 서버 구동 후 브라우저 화면

@types/testing-library__jest-dom 의존성이 없어서 생기는 문제입니다. 해당 의존성을 설치해 줍니다.

yarn workspace storybook add -D @types/testing-library__jest-dom

// 또는 monorepo-example/packages/storybook 디렉터리로 이동 후
yarn add -D @types/testing-library__jest-dom

의존성 설치 후 다시 yarn start:storybook 명령어를 통해 개발 서버를 구동시켜보면 에러 없이 실행 되는 것을 확인할 수 있습니다.

개발 서버 정상 구동 후 브라우저 화면

이제 이 workspace 에 Storybook 을 붙여보도록 하겠습니다.

⚡ Storybook 세팅하기

monorepo-example/packages/storybook 디렉터리로 이동한 뒤에 다음의 명령어를 통해 Storybook 을 세팅해 줍니다.

yarn dlx -p @storybook/cli sb init

명령어를 실행해주면 다음과 같은 문구를 보여주면서 Storybook 설치가 진행됩니다.

yarn dlx -p @storybook/cli sb init 명령어 실행 후 console 화면

설치를 완료하고 Turborepo CLI 관련 설정 또한 추가해 줍니다.

// monorepo-example/packages/storybook/turbo.json
{
...
"pipeline": {
...
"storybook": {
"cache": false
}
}
}
// monorepo-example/package.json
{
...
"scripts": {
...
"start:storybook": "turbo run start --filter=storybook",
"storybook": "turbo run storybook --filter=storybook"
},
...
}

start:storybook script 는 React 개발 서버를 구동하는 명령어이고, storybook script 는 스토리북 개발 서버를 구동하는 명령어입니다.

Storybook 개발서버를 구동시켜 보면 터미널 console 에 다음과 같은 에러 로그가 찍혀있는 것을 볼 수 있습니다.

Storybook 개발서버 구동 시 console 에 나타나는 에러 메시지

monorepo-example/packages/storybook/package.json 파일 안의 eslintConfig > extends 필드에 react-app 이 보이는데요, 이 ESLint 관련 config 를 제대로 불러오지 못해서 생긴 오류입니다.

저희는 나중에 따로 ESLint 설정을 할 것이기 때문에 이 부분은 삭제해 주도록 하겠습니다.

// monorepo-example/packages/storybook/package.json
{
...
"eslintConfig": {
"extends": [ <-- 삭제
"react-app", <--
"react-app/jest" <--
], <--
...
},
...
}

삭제 후 Storybook 개발 서버를 다시 구동시켜 보면 제대로 동작이 되는 것을 확인할 수 있습니다.

Storybook 개발 서버 정상 구동 시 터미널 console 에 보여지는 내용

💫 ’shared’ workspace 생성하기

shared workspace 는 별도의 프론트엔드 프레임워크나 라이브러리에 의존하지 않도록 구성해 보겠습니다.

monorepo-example/packages/shared 디렉터리로 이동한 뒤, src 디렉터리를 생성해 줍니다.

shared workspace 생성 후 디렉터리 구조

src 디렉터리를 생성한 뒤 해당 디렉터리에 const.ts 파일을 생성해주고 다음 내용을 기입해 줍니다.

export const test = 'test'

위 파일의 test 라는 변수는 이후에 다른 workspace 에서 공통적으로 사용될 변수입니다.

⚠️ TypeScript 에러 해결하기

monorepo-example/apps/nextjs/pages/index.tsx 파일을 VS Code 로 열어봅니다.

파일을 열자마자 빨간 밑줄이 많이 보입니다. 다음 단계로 넘어가기 전에 이 에러들을 먼저 해결해 보도록 하겠습니다.

import 구문에서의 TypeScript 에러
JSX 에서의 TypeScript 에러

💿 VS Code 용 Yarn berry SDK 설치하기 (+ TypeSciprt, ESLint, Prettier)

Yarn berry 에서는 VS Code 와의 호환성을 위한 SDK 를 자체적으로 제공합니다. 필요한 SDK 를 설치하기 전에 모노레포 root 에 의존성을 추가해주겠습니다.

모노레포 root 디렉터리로 이동한 뒤, 다음의 명령어를 통해 TypeScript, ESLint, Prettier 를 모노레포의 개발 의존성(devDependencies)에 추가해 주세요.

yarn add -D typescript eslint prettier

이어서 모노레포 root 위치에서 다음의 명령어를 실행해 주면 설치가 완료됩니다.

yarn dlx @yarnpkg/sdks vscode
Yarn SDKs for VS Code 가 설치된 모습

💿 TypeScript 버전 교체하기

monorepo-example/apps/nextjs/pages/index.tsx 파일의 module import 부분에서 발생하는 에러를 제거하기 위해서는 기존에 사용하던 TypeScript의 버전을 VS Code 에 내장된 버전에서, Yarn SDK 를 통해 제공된 버전으로 교체해주어야 합니다.

이를 위해서는 VS Code command palette(맥 기준, Cmd ⌘ + Shift ⇧ + P) 를 열고 TypeScript 로 검색하면 TypeScript: Select TypeScript Version... 이 나오는데 이것을 선택해줍니다.

선택한 뒤에 Use Workspace Version 4.9.4-sdk 로 설정해주면 됩니다.

설정 후 위에서 보았던 에러메시지가 모두 사라진 것을 확인할 수 있습니다.

monorepo-example/apps/nextjs/pages/index.tsx

💿 ZipFS VS Code Extension 설치하기

Yarn 의 PnP 시스템 하에서 import 된 module 들에 대한 ‘Go To Definition’ 기능이 VS Code 내에서 올바르게 동작하게 하려면 별도의 Extension 을 설치해 주어야 합니다.

ZipFS 라는 Extension 인데요, zip 파일로 압축된 의존성 패키지들의 탐색을 지원해주는 도구입니다.

VS Code Marketplace 로 이동해줍니다.

검색어를 ZipFS 로 입력해 검색한 뒤 설치를 진행해 줍니다.

검색어를 ZipFS 로 입력해 검색한 뒤 설치를 진행해 줍니다.

이제 에디터 내에서 ‘Go To Definition’ 기능이 잘 동작하는 것을 확인할 수 있습니다.

Go To Definition 기능이 정상 동작 하는 모습

🏁 마치며

이번 편에서는 모노레포 하위에 속하는 3가지 workspace(nextjs, storyboook, shared) 들을 구성해 보았습니다.

마지막 편에서는 workspace 간 코드를 import 해서 사용하기 위한 세팅부터 ESLint & Prettier 설정, 그리고 모노레포 구축 시 겪었던 문제에 관한 Troubleshooting 까지의 내용을 다루어 보도록 하겠습니다.

긴 글 읽어주셔서 감사합니다 😄

--

--