Yarn Workspace를 이용한 Monorepo 구성
by jingi & seunghwan
현재 Fastcampus Language(이하 FCL) 개발팀에서 진행하고 있는 Fastone Online 프로젝트에는 학생, 강사, 관리자 총 세 가지 프론트엔드가 있다. 원래대로라면 관리자는 기존 관리자 전용 사이트에 합쳐지고, 학생과 강사는 각각의 저장소를 만들었을 것이다.
하지만 우리 팀 슈퍼 디자이너 상은님(쌍따봉)의 놀라운 능력으로 학생과 강사 두 프론트엔드가 같은 컴포넌트들을 공유하도록 디자인 됐고!(두둥) 컴포넌트들 크기나 레이아웃만 변경해서 구현이 가능할 것 같다는 생각이 들었다.
“공용 컴포넌트들 위한 저장소를 따로 파서 관리해볼까?”라는 생각도 해봤지만 이를 관리하는 것도 쉽지 않을 것 같아 Monorepo를 구성해보기로 했다.
일단 직접 import 해보자!
구글링해보니 모노레포를 구성하기 위한 다양한 방법들이 나왔지만 너무 무겁고 복잡해보였다. 그래서 도구를 사용하지 않고 디렉토리 분리와 import
만 이용해 연결해보기로 했고, 다음과 같이 디렉토리 구조를 잡았다.
root
- common: 공통 컴포넌트
- package.json
- student: 학생 웹사이트
- package.json
- teacher: 강사 웹사이트
- package.json
상대경로만 잘 맞춰주니 생각보다 쉽게 import
할 수 있었고, 한 저장소내에서 상대경로를 이용하다보니 import
에 사용한 경로를 바꿔줄 필요도 없었다.
첫 번째 Error: 외부 경로 Transpile 불가능
unexpected token 에러가 발생했다. (쳇…)
원인은 common에서 가져온 컴포넌트가 transpile(이하 트랜스파일) 되지 않는 것이었다. 학생/강사 프론트엔드 프로젝트는 Next.js를 사용하고 있고, Next.js는 빌드 시 프로젝트 디렉토리 안의 파일들만 트랜스파일 한다. 프로젝트 외부의 common은 제외된 것이다.
그리고 한 포스트를 통해 next-transpile-modules
를 이용한 해결 방법을 알 수 있었다. next.config.js
파일에 아래와 같이 적용하면 외부 모듈도 트랜스파일이 가능했다.
const withTM = require("next-transpile-modules")(["../common"]);module.exports = withTM();
만약 이미 다른 모듈들(예를 들어 next-css라던가)을 중첩해 사용하고 있다면 withTM()
을 가장 바깥쪽에 위치하면 된다.
module.exports = withTM(withCSS());
두 번째 Error: 복수의 React copy 발생
외부 경로 트랜스파일 문제해결 후 또 다른 문제가 발생했다. (젠장…)
에러메시지가 친절하게도 세 가지 의심되는 원인을 알려줬고, 그 중 한 가지가 유력했다.
3. You might have more than one copy of React in the same app
외부 경로를 직접 import하다보니 패키지 관리도구(yarn, npm 등)를 거치지 않게되고, 의존성이 서로 다르게 관리되며 결국 common에서 가져온 컴포넌트는 또 다른 React Instance를 만들게 되는 에러로 파악됐다.
결국 모노레포 내 프로젝트들의 의존성을 한꺼번에 관리할 수 있는 도구가 필요해졌다.
Yarn Workspace?
Yarn Workspace의 가장 큰 특징은 Workspace Root(이하 워크스페이스 루트) 내 각각의 Workspace(이하 워크스페이스)를 마치 패키지처럼 다른 워크스페이스에서 의존성으로 연결할 수 있다는 것이다. 예를 들면 student의 package.json에 common을 의존성으로 추가하면 yarn install
시 자동으로 연결된다.
또한 각 워크스페이스의 의존성들은 Hoist되어 워크스페이스 루트의 node_modules 디렉토리에 설치된다. 공통된 패키지인데 버전만 다를 경우엔 해당 워크스페이스 node_modules에 설치된다.
이것을 이용하면 common도 student의 node_modules에 추가되어 마치 패키지처럼 사용이 가능하다. 그리고 공통으로 사용되는 패키지를 루트에 있는 package.json에서 한 번에 관리해 관리 포인트도 줄일 수 있다.
프로젝트에 적용!!
Yarn Workspace를 적용하면 기존과 디렉토리 구조는 같고, 루트 디렉토리에 package.json이 하나 더 생긴다.
root
- common
- package.json
- student
- package.json
- teacher
- package.json
- package.json
루트에 있는 package.json에 워크스페이스 경로(package.json이 있는 기준)와 공통으로 사용되는 패키지들을 설정했다. 워크스페이스 루트의 package.json 파일에 의존성을 추가하면 하위 워크스페이스 package.json에 의존성을 추가하지 않아도 사용이 가능하며, 워크스페이스 루트 레벨에서 공통으로 관리 할 수 있게 된다.
// 워크스페이스 루트의 package.json
{
"private": true,
"workspaces": [
"common",
"src_student"
],
"scripts": {
"dev:student": "yarn workspace student dev",
"build:student": "yarn workspace student build",
"start:student": "yarn workspace student start",
"test:student": "yarn workspace student test",
"test:w:student": "yarn workspace student test:w"
},
"dependencies": {
"antd": "^4.9.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"styled-components": "^5.1.1"
}
}
공식문서는 외부로 공개할 패키지가 아니기 때문에 private: true
를 추가하라고 강하게 권하고 있다.
Note that the
private: true
is required! Workspaces are not meant to be published, so we’ve added this safety measure to make sure that nothing can accidentally expose them. from Workspaces | Yarn
scripts에는 해당 프로젝트를 실행시키기 위한 스크립트들을 넣었다. 하위 워크스페이스 scripts를 실행시키기 위한 yarn workspace
명령어를 이용했다.
dev:student
를 예로 보면 yarn workspace student dev
는 name이 student인 워크스페이스의 scripts중 dev를 실행시킨다는 말과 같다. 각 프로젝트 디렉토리로 가지 않아도 root에서 원하는 프로젝트의 개발환경을 실행 할 수 있다.
워크스페이스를 의존성에 추가하게 되면 더 이상 common의 경로가 아닌 common의 package.json에 설정한 name을 사용할 수 있어서 일반적인 라이브러리 처럼 사용할 수 있다.
// next-transpile-modules 설정 예시
const withTM = require("next-transpile-modules")(["common"]);module.exports = withTM()// student에서 common의 Button을 가져오는 예시
import React from "react";
import Layout from "@/components/layout";
import { Button } from "common";export default function Home() {
return (
<Layout maxWidth="100%" marginTop={"0px"} hideFooterBorder={true}>
<Button />
</Layout>
);
}
드디어 모노레포 완성!!
배포는?
현재 우리팀에서는 Google Cloud Build와 Docker를 이용해 배포하고 있다. 현재와 같은 구조라면 워크스페이스 루트의 package.json에 추가된 scripts와 Dockerfile의 yarn 명령을 수정하는 정도로 배포가 가능할 것 같다.
소감
먼저 일관된 컴포넌트 디자인을 해주신 상은님께 감사를!(박수 X 1M) 디자인부터 컴포넌트화 되어있어서 그 효과가 더 높을 것으로 생각된다.
모노레포를 적용하기 전에는 디렉토리 나누고 심볼릭 링크 걸어서 가볍게 사용할 생각이었다. 그런데 직접 해보니 예상치 못한 오류들이 발생해 그럴 수 없었다. 그래도 새로 찾은 Yarn Workspace가 생각보다 가볍고 편한 부분이 많아 전화위복이 되지 않았나 싶다.
앞으로 개발에 불편한 점은 없을지 지속적으로 살펴보며 더 나은 방법을 찾아봐야겠다는 생각이 들었다.