Channel Talk
7월 27일
안녕하세요. 채널톡 웹팀 엔지니어 카를로스입니다 😊 채널톡에서 진행하고 있는 프로젝트 최신화에 관해 소개해드리려 합니다.
채널톡 웹팀에서는 많은 일들을 하고 있어요. 그래서 이 어플리케이션들을 관리하는 프로젝트도 많이 나뉘어져 있습니다.
채널톡 앱을 담당하는 desk, 고객의 문의 화면을 담당하는 front, 디자인 시스템인 bezier, 채팅 경험을 향상시키는 WYGIWIG 프로젝트인 prosemirror-toolkit, 100만건 이상의 테이블 데이터들을 잘 처리하기 위한 Dynamic table 등등.. 많은 저장소들이 있습니다.
다른 팀들도 채널팀과 마찬가지로 많은 프로젝트를 관리하고 있을거라 생각합니다.
많은 스타트업에서 성장 가능성을 보기 위해 작게 시작한 프로젝트들은 특정 시점을 지나면서 부터는 많은 것을 고려해야 합니다.
이용자가 늘어날수록 확장성을 고려해야 하고, 안정적으로 운영하고 손쉽게 새로운 기능을 더할 수 있도록 유지보수성도 있어야 하죠.
신뢰성이 높고 에러가 적은 어플리케이션을 개발도 고민해야 하고, 개발자가 늘어날수록 개발 편의성도 고려해야 합니다. 그리고 이 조건들을 채워가며 빠르게 성장도 해야 합니다.
성장 가능성을 보기 위한 프로젝트들은 성장과 함께 이제는 앞에 언급된 많은 내용들까지 포괄해야 하고, 그러기 위해 프로젝트 구조 최신화 작업을 진행 중 입니다.
위에 언급한 많은 조건들을 충족시키기 위한 고민은 오래전부터 있어왔습니다.
그 중 하나가 2000년 초 쯤에 나온 개념인 Monorepo 입니다.
In version control systems, a monorepo ("mono" meaning 'single' and "repo" being short for 'repository') is a software development strategy where code for many projects is stored in the same repository
Wikipedia에 나온 monorepo에 대한 설명으로 말 그대로 mono ‘repo’sitory 안에 프로젝트들이 모여 있음을 의미합니다.
Monorepo는 하나의 저장소 안에서 여러 프로젝트들을 통합하여 관리합니다.
언뜻 들으면 한 군데 모여있어서 확장성도 유지보수성도 개발 편의성도 떨어지는 선택처럼 보일 수 있습니다만, 하나의 저장소에서 여러 프로젝트들을 관리하며 생기는 이점은 프로젝트 최신화 2편에서 자세한 내용을 들려드리도록 하겠습니다.
채널톡 웹팀의 서비스들은 monolithic하게 구성되어 있습니다. 초기에 개발 속도가 중요하니 대부분의 회사는 monolithic하게 프로젝트를 구성합니다. 하지만 프로젝트가 점점 커지고 서비스도 거대해지게 되면서 monolithic의 장점들은 점점 희미해지고, 어려운 점 들만 부각됩니다.
브랜치의 관리, 테스트, 수많은 lint 과정들, 이 과정을 겪는 분들은 이미 많은 고민들을 해왔고, 현재 그 방향은 모노레포 형태로 많이 굳어져 가는 것 같습니다. 모노레포와는 조금 다른 얘기이지만, 현재 그리고 있는 채널팀 웹 서비스의 목표는 MFA형태의 서비스가 될 것 같습니다.
MFA는 여러 독립적인 서비스들이 특정 목적을 띈 앱 안에서 각자 서비스 되는 것을 의미합니다. MSA와 거의 비슷한 맥락이지만, 좀 더 FE에 초점을 둬 MFA라 부릅니다.
아래 이미지는 채널톡에서 제공하는 서비스 중에 하나 인 데스크 라는 앱의 스크린 샷 입니다.
데스크 앱은 빨간 테두리로 나뉘어진 단위로 컴포넌트가 분리되어 있습니다. 컴포넌트의 분리는 잘 되어 있지만, 하나의 어플리케이션 안에 구성되어 있어 별도의 서비스로 동작하려면 해야 될 일이 많습니다.
필요한 관심사만큼 분리를 하고 별도의 서비스로 동작하고, 필요한 단위만큼 묶어서 앱으로 배포하는 것을 MFA 라고 합니다. (빨간 영역이 각각의 독립된 서비스로 구현될 수 있다!)
NPM은 2010년 첫 발표 이후 javascript 생태계에 많은 변화를 가져왔지만, 그 구조적 한계 때문에 여러 문제점들을 가지고 있었습니다.
대표적으로 얘기하면 NPM에서 패키지들을 node_modules에 적용하며 발생하는 유령 의존성이 있고, 프로젝트가 커질수록 속도와 성능 저하를 동반하는 과한 I/O를 유발하는 구조적 문제가 있습니다.
💡 이 글에서는 해당 문제점들을 자세히 다루진 않습니다.
그래서 채널톡 웹팀은 다음 패키지 매니저로 Yarn을 선택하게 됐습니다.
Yarn은 1.22를 마지막으로 Classic버전의 관리를 중단 했습니다. 2.X 이후부터는 Yarn Modern(berry)이라 부르고 새로운 구조로 패키지를 관리합니다. 압축 파일을 통한 패키지 의존성 관리, pnp를 통한 zero-install이 주된 내용입니다.
그리고 plugin 시스템이나 강화된 workspace 지원으로 lerna같은 추가 라이브러리가 없어도 모노레포 구조를 잘 지원합니다. Monorepo에 관한 내용은 2편에서 자세하게 살펴 볼 예정입니다.
Monorepo를 관리하기 위해선 여러가지 개념과 개발 정책, 저장소를 관리하기 위한 도구가 필요합니다.
채널팀에서는 여러 도구들 중 Yarn을 채택 했습니다.
초기에는 Yarn(classic)의 기능 지원이 적어 Lerna등을 병행해서 관리도구로 사용 했지만 현재는 Yarn(Berry)만으로도 모노레포 관리가 충분 한 상황입니다.
잠시 채널팀의 WYGIWYG개선 프로젝트 중 하나인 prosemirror-toolkit 프로젝트 구조를 살펴보겠습니다.
packages안에 있는 폴더 하나가 독립적인 프로젝트 구조 인데, 폴더 안을 살펴보면 독립된 프로젝트임을 알 수 있습니다.
개별 package.json, tsconfig.json이 보이고 코드를 담고 있는 src가 보입니다.
Yarn 에서는 workspaces로 지정되어 관리되며, package.json에서 workspaces로 지정해서 관리될 프로젝트들을 지정합니다. 하나의 프로젝트 안에 있지만 내부적으로는 packages안에서 개별 프로젝트 구조를 가집니다.
Yarn berry에서 주요 지원 사항중에 하나가 패키지들을 zip파일로 관리하는 것 입니다.
각 패키지들간의 의존성은 .pnp.js라는 파일에서 일괄적으로 관리 해서 빠르고 명확하게 의존 관계를 정리합니다.
node_modules에 존재하던 파일들을 zip파일로 압축해서 가지고 있어 용량이나 파일 수가 기존 대비 많이 줄어들었습니다.
Desk 기준
node_modules : 95,329개 파일 / 1.02GB
.yarn/cache : 2,120개 파일 / 145.7MB
그래서 프로젝트 안에 설치된 패키지들 압축파일을 전부 올려 버전 관리 대상으로 삼았고, pnp의 특성상 설치가 필요 없어 프로젝트를 내려받는 즉시 바로 실행할 수 있습니다. 이를 zero-install이라 합니다.
실제 github에 설치된 패키지 zip 파일까지 올려 관리를 하고, 다른 환경에서 동기화 하면 추가 설치(yarn install) 없이 바로 실행이 가능합니다.
실제 Yarn에서도 berry 프로젝트를 이 형태로 관리하고 있습니다.
zero-install은 ci에서도 많은 강점을 가집니다. 패키지 설치 단축을 위한 cache 공간이 필요 없고(저장소에서 checkout 받은 그대로 사용), 설치도 빠르기 때문에 배포에 필요한 시간을 줄일 수 있습니다.
실제 적용했을때 설치에만 들어가는 시간이 70~80초 이상 줄어들기도 했습니다.
결론부터 얘기하면, 채널팀에서 새로 적용한 Yarn berry는 반 만 성공했습니다. 우선 겪었던 몇 가지 문제점들을 정리해보겠습니다.
pnp 환경에서 jest 27 버전일때 max workers 옵션을 사용하면 무한 대기 상태가 되는 버그가 있습니다. 로그도 나오지 않는 환경이라 정상적인 트래킹이 불가능 하여 여러 사람의 도움을 받아 해결을 한 케이스 입니다. maxWokers 대신 runInBand 옵션을 사용 해서 우회를 했고, 이 때문에 ci 에서 jest의 실행 시간이 2배 이상 늘어나는 문제점이 발생 했습니다.
Typescript에서 node_modules가 존재하지 않아 정상적으로 타입을 유추하지 못하고 발생하는 문제로 yarnpkg에서 제공하는 pnpify를 설치해서 해결 했습니다.
pnpify는 pnp를 준수하지 않는 패키지를 사용하기 위해 가상의 node_modules 폴더를 생성해서 해당 경로가 정상적으로 존재하는 것 처럼 인식시켜줍니다. 프로젝트에서 사용중인 모든 프로젝트가 pnp를 준수한다면 필요 없는 기능입니다.
그 외 여러 설정된 기능들의 경로 문제가 있었고, eslint의 plugin이 import 순서를 정상적으로 인식하지 못하는 문제, webpack의 설정 문제로 빌드할때 리소스를 많이 먹어 기존 개발환경이 자꾸 터지던 문제, 기타 라이브러리 버전을 올리며 생기는 여러 사이드 이펙트들도 많았습니다.
힘들게 여러 문제점을 돌파해서 Yarn의 버전도 3.x대로 올리고 pnp도 적용하고 배포까지.. 성공적으로 마이그레이션 하는데 성공했었습니다만, 한 가지 큰 문제를 만나 pnp 기능을 포기하게 됩니다.
채널팀 데스크 프로젝트는 몇 가지 편의성을 위한 구조로 인해 몇 가지 모듈들을 Typing 해서 사용중인데, 그 중 레거시 코드중 하나가 큰 문제를 일으켰습니다. 빌드는 정상적으로 되는데 개발 툴 상에서 redux를 typing 해놔서 관련된 타입들이 전부 unknown, never로 유추되며 개발을 하기 어려워지는 문제가 있었습니다. 여러 시도를 했지만 결국 문제를 잡지 못하고 pnp 기능을 포기하게 됐습니다.
이 문제는 채널팀 프로젝트의 redux와 연관된 레거시 코드를 제거하면 자연스럽게 해결 될 것이라는 희망을 가지고 꾸준히 리팩토링을 진행하고 있습니다.
그나마 다행인점은 pnp가 정상적으로 동작 안 한다고 yarn의 버전을 낮출 필요는 없었습니다. yarn의 최신버전을 적용한 이유가 모노레포를 위한 workspace기능 이었음을 생각하면 pnp에 지나친 집착은 할 필요가 없었습니다.
yarn은 nodeLinker라는 옵션을 통해 pnp모드 외에 기존 node_modules를 사용하는 방식을 지원합니다.
조금 차이가 있는 부분은 cache에 zip파일들은 존재하지만 압축해제를 해서 node_modules 폴더를 생성하는 구조 입니다.
⚒️ pnp
-> .yarn/cache 폴더 안에 zip 파일들만 존재. zero-install 가능
⚒️ nodeLinker
-> zip파일들은 동일, 해당 zip파일들을 node_modules로 압축 해제. zip 파일을 그대로 사용하지 못해 zero-install 사용 불가능.
채널팀은 성장을 위해 만들었던 레거시 코드들을 효율적인 구조로 변환하는데도 많은 노력을 기울이고 있습니다.
성능개선을 위한 코어 개선작업도 이루어지고 있고, 팀이 커짐에 따라 여러 개발 문화들도 지속적으로 개선하는 노력을 하고 있어요. 그중에 높은 중요도로 프로젝트 구조의 최신화를 고민하고 여러 방면으로 노력하며 결과물들을 만들어 내고 있습니다.
다음 편에서는 비교적 신규 프로젝트인 WYGIWYG 개선 프로젝트를 가지고 monorepo에 대한 내용을 깊게 다뤄보겠습니다.
[이런 글도 추천드려요]
We Make a Future Classic Product
채널팀과 함께 성장하고 싶은 분을 기다립니다