앱과 라이브러리 관리에 Monorepo?
공유하는 코드를 어떻게 관리하지?
현 직장에서 사내의 모든 FE 프로젝트를 하나의 팀에서 담당하게 되다 보니 어느 순간 의존성 관리 문제에 봉착하게 되었다. 라이브러리와 앱이 N:N으로 늘어나 의존성 관리가 복잡해지고 있다.
예를 들면. 쇼핑몰의 경우 보통 웹과 관리자 한 세트로 개발하여 운영한다. 만약 새로운 서비스를 오픈한다면 총 2세트가 되고. 서비스 간에 회원, 주문을 하나로 통합하는 경우 2세트가 더 추가된다. 결국 서로의 의존성이 얽혀있는 8개 이상의 리포지토리를 운영하게 된다.
이 글에서는 이 문제를 해결하는 방법 중 Monorepo(이하 모노레포)도입을 검토하며 논의했던 내용을 정리한다. 일단은 앞서 언급했던 문제를 고려하지 않고. 단순히 중복되는 코드를 공유하는 방법에 대해서 간단히 짚고 넘어가 보려 한다.
- npm
- git submodule, subtree
- 모노레포 (Yarn Workspace + Lerna)
npm 사용
라이브러리 빌드 결과물을 npm publish
로 업로드하고 써야 하는 곳에서 npm install
로 설치해 사용하는 방법이다. npm에 올리는 패키지들은 1.10.x
, 2.1.0
과 같이 버전을 명시하게 되어 있으므로 관리나 사용에 큰 도움이 된다.
불편한 점이 있다. 개발할 땐 앱이건 라이브러리건 구분 없이 빈번하게 수정해야 하는데. 라이브러리를 수정하고 나면 항상 npm publish
, npm install
을 통해 공유하는 절차를 거쳐야 하기 때문이다. 라이브러리 갯수가 두 개 이상이면 이런 절차에 시간이 많이 소요될 것이다.
그나마 npm link
를 이용하면 로컬에 빌드된 패키지를 전역에 설치하여 다른 패키지에서 설치해 쓸 수 있다. 한마디로 npm publish
를 하지 않아도 된다. 라이브러리 빌드 결과물 폴더에서 npm link
실행 후 사용을 원하는 곳에서 npm link <패키지명>
으로 연결하여 사용할 수 있다. 마찬가지로 라이브러리 개수가 늘면 불편하고. 개발이 끝난 후 전역에 설치된 패키지를 일일이 제거해주어야 하는 번거로움이 있다.
submodule, subtree 사용
리포지토리 내 특정 폴더에 다른 리포지토리를 사용하는 방식으로 부모 리포지토리에서는 폴더 자체를 특정 커밋으로 다루며 커밋은 자식과 별도로 독립적으로 관리한다. 부모 리포지토리에서 diff를 보면 서브모듈 폴더는 단순히 서브모듈의 여러 커밋 중 한 군데를 가르키는 해시값만 보인다.
따라서 서브모듈에 컨플릭이 일어났을 때 부모 리포지토리에서 바로 머지할 수 없고. 서브모듈 안에서 컨플릭을 해결하고 그 커밋을 가르키도록 부모 리포지토리를 수정해야 하는데 이게 번거롭기도 하고 git에 익숙하지 않은 개발자라면 실수할 여지가 있다.
서브트리는 부모 리포지토리에서 파일을 관리할 수 있다고 한다 사용해보지는 않았지만 Git subtree를 활용한 코드 공유를 참고했을 때 서브모듈보다 편리할 것으로 보인다.
글에서 언급하는 컨플릭트는 코드리뷰를 하는 팀이라면 머지 전에 사이드 이펙이 예상되는지 담당자들이 잘 검토하는 것도 방법이고. 특정 패키지가 수정되었을 때 영향이 있는 의존 패키지들을 알려주는 도구들도 있으니 응용한다면 효율적으로 관리할 수 있을 것으로 보인다.
typescript 프로젝트의 경우 각 모듈이 tsconfig.json에 정의된 root 경로 하위에 존재해야 참조할 수 있으므로. 서브모듈, 서브트리 모두 참조가 가능하도록 폴더 구조를 잡아야 한다.
모노레포 (Yarn Workspace + Lerna)
하나의 리포지토리에서 사내의 모든 코드를 관리하는 방식이다. nodejs 프로젝트의 경우 일반적으로 yarn workspace를 이용하여 패키지 의존성을 관리하고. lerna를 사용해 로컬 패키지 간 의존성 추가, 다수의 패키지에 특정 task를 수행, 사이드 이펙이 발생하는 앱들을 추려내거나 하는 형태로 운영한다. 먼저 장단점을 정리해 보면 다음과 같다.
코드 공유가 간편해진다
코드들이 모두 하나의 리포지토리 내에 있으므로 패키지들의 추가 및 제거가 간단하다. 중복 코드는 그냥 패키지용 폴더를 만들어 옮기고 lerna add
로 의존성 추가 후 yarn install
하면 되고. 제거하는 경우 그냥 삭제하면 된다.
로컬 패키지들 간의 의존성은 심볼릭 링크로 처리된다. 윈도우로 치면 폴더 바로 가기의 형태로 연결하기 때문에 npm install
로 사용하는 것과 똑같이 쓸 수 있지만, 공유를 위한 절차가 간단해져 편리해지는 것이다.
또 단순히 심볼릭 링크로 연결되어 있고. 라이브러리 소스도 같은 리포지토리에 있어서 디버깅이 매우 간편하다. 앱을 띄워 두고 라이브러리 코드를 직접 수정하면서 디버깅할 수 있다.
버전관리가 편해진다
lerna
에는 각 패키지의 버전 관리를 쉽게 할 수 있는 기능도 있다. 커밋 메시지를 Conventional Commits에 맞게 작성하는 경우. 배포 시점에 추가적인 옵션을 주면 쌓인 메시지를 읽어 각 패키지의 새로운 버전을 계산해 준다.
예를 들어 feat(core): 렌더링 로직 성능 향상
이라는 메시지라면 feat
이므로 minor 업데이트이며. (core)
로 여러 패키지 중 core 패키지가 업데이트 되는 것임을 인식한다. 따라서 다른 패키지의 버전은 올리지 않고. 오직 core 패키지의 버전만 올린다. 렌더링 로직 향상
이라는 메시지는 CHANGELOG.md
파일에 자동으로 정리해 준다. 그리고 변경사항이 있는 패키지를 추려 자동으로 npm에 publish까지 해 준다.
모노레포 운영 시 변경사항을 관리하는 것이 더욱더 중요한데 이런 점에서 큰 도움이 될 것으로 보인다. 이 기능은 꼭 모노레포 운영을 하지 않더라도. 하나의 리포지토리에서 여러 npm 패키지를 관리하는 경우 업무량을 크게 줄여줄 수 있을 것으로 보인다.
리포지토리의 크기가 커짐
일반적으로 많이 알려진 문제이다. angular의 소스를 clone 받을 때마다 느끼는 점으로 리포지토리 하나가 너무 커진다. 프로젝트 초기에는 별문제가 없겠지만 오랫동안 쌓이면 git 액션들의 처리 속도가 조금씩 느려질 것이다. 하지만 angular 컨트리뷰팅 과정에서 크게 느려서 못쓰겠다는 느낌을 받은 적은 없어서 괜찮을 것으로 생각된다.
사이드 이펙트 문제
도입 검토 중에 제일 논란이 되었던 문제다. 배포 시기가 다른 모바일 웹 서비스 A, B가 있다고 가정할 때. B의 기능 추가 커밋이 A에도 영향을 줄 수 있다는 것이다. 만약 기능 추가 중에 라이브러리를 수정했다면 A는 사이드 이펙트가 있을 수 있는데. 담당자가 이것들을 모두 사전에 검토할 수 있을지 모르겠다는 것이다.
현 회사는 모든 변경사항은 반드시 PR을 거치게 되어 있지만. 검토해야 하는 양이 많고 당장 해결해야 하는 업무가 많은 상황에서는 놓치는 경우가 많을 것이다. 이 이슈는 모노레포의 문제 중 하나로 팀원 개개인에게 적정한 수준의 역량을 요구한다는 것이다. 결국 전면적 도입은 힘들다는 결론이 났다.
모노레포 적용이 어려운 이유
한 회사에 서비스가 여럿이라면 보통 배포 시기가 다 다르기 마련이다. 앞서 언급한 대로 개개인의 역량이 따라준다고 하면 모르겠지만 서비스의 안정적인 운영을 고려한다면 같은 리포지토리 내에서 사이드 이펙트가 있을지 없을지 모르는 커밋이 쌓이는 것 자체가 문제가 될 여지는 있다.
또 현 회사는 테스트용으로 3개의 서버 (alpha, sandbox, beta), 실 배포용으로 1개의 서버(real)를 운영하는 4단계 phase 운영을 하므로 모든 리포지토리가 최소 두 개 이상의 배포 브랜치를 가지고 있다.
그러나 lerna publish
명령 자체가 만들어내는 버전 태그들은 유일해야 한다. 이런 단일 배포 브랜치 기반 프로세스를 보면 애초에 팀의 배포 프로세스부터가 맞지 않는 것으로 보인다.
결국은 전면적 도입은 하지 않기로
결국 단일 배포 브랜치를 갖는 라이브러리만 적용하고 서비스는 기존대로 운영하게 되었다. lerna를 이용해 멀티 패키지 라이브러리 리포지토리 관리를 하게 된 것만으로도 업무량이 크게 줄어서 도입을 검토했던 시간이 아깝지는 않았다. 환경이 맞지 않아 도입할 수 없었지만 적용에 따른 이슈도 크리티컬한 부분이라 아쉽다는 생각이 들지는 않았다.
이야기 중에 라이브러리에만 모노레포를 적용하자는 말을 많이 들었다. 그 자리에서 언급한 내용이기도 하지만 그렇게 되는 순간 이미 모노레포가 아니게 된다. 본문에서 언급했던 모노레포의 장점이 희석되기 때문이다.