Konstantin raev cover

Facebook은 Node_Modules를 어떻게 관리할까요?

Node.js 디펜던시 관리 문제는 JavaScript 개발자를 자주 괴롭힙니다. GitHub에 노드 모듈을 넣는 것이 좋을까요? 아니면 shrinkwrap.json을 사용해야 할까요? Facebook에서 노드 모듈을 오랫동안 다뤄온 Konstatin이 소스 컨트롤 팀, 디펜던시 관리 팀, 보안 팀, 앱 개발자 등 여러 팀들의 요구를 모두 만족할 수 있는 해결책을 공유합니다.


소개

저는 Facebook에서 일하는 Konstantin입니다. 프런트엔드 JavaScript 개발자로서 Angular로 웹사이트를 구축했고, 백엔드 개발자로서 Node.js, Webpack, GALT, Grunt 등 여러 도구들을 사용하고 있습니다.

Facebook에서 노드 모듈을 다루는 방법

Facebook의 React Native 팀에는 일 년쯤 전에 합류했습니다. 최근 확인해보니 GitHub에서 상위 10명의 공헌자에 들었더군요. 최근에는 주로 버그 픽스와 테스트와 관련된 커밋을 했습니다.

뭔가 잘못된 것을 발견하면 해당 기능을 고치거나 그 부분을 커밋한 사람에게 알리는 방식으로 Facebook 내부뿐만 아니라 외부의 다른 개발자나 팀이 프로젝트에 공헌하도록 돕고 있습니다.

Node.js와 NPM

Node.js에 대해 간단히 설명하자면 서버에서 JavaScript를 실행할 수 있는 환경입니다. Node 등의 언어를 위한 패키지 등록 도구인 NPM을 지원하며, Node 설치 프로그램에서 NPM 클라이언트를 함께 제공하므로 쉽게 패키지를 설치할 수 있습니다.

NPM은 최근 급격히 성장하면서 다른 패키지 등록 기구들보다도 많은 수의 패키지들을 보유하고 있습니다.

NPM의 성공 요인

왜 NPM은 이렇게 성공했을까요? JavaScript가 유명해졌기 때문일까요? 아니면 JavaScript가 NPM 덕분에 성공한 걸까요?

첫 번째 이유는 NPM에 배포하는 것이 정말 쉽기 때문입니다. 지나치게 쉽다고 생각하는 사람들까지 있죠. 얼마냐 쉽냐면 몇 자 타이핑하고 엔터를 누르면 단 몇 초만에 패키지가 등장할 정도입니다.

또 다른 이유는 NPM이 종속성을 해결하는 방법에 있습니다. 어떤 특별한 방법을 사용하는지 예를 들어 설명하겠습니다.

NPM에서 종속성을 해결하는 방법

A 애플리케이션이 B 디펜던시에 종속적이고, B가 다시 C 디펜던시에 종속적인 상황을 생각해 보겠습니다. Java인 경우 이 모두를 다운로드해서 컴파일하고 함께 링크해서 애플리케이션을 만들어야 하겠죠.

이 경우 A 애플리케이션 자체에서 C 디펜던시의 다른 버전인 C2를 사용하면 C와 C2와의 충돌이 발생하게 됩니다.

이런 개발 뉴스를 더 만나보세요

Gradle에서는 둘 중 하나만 선택하도록 강요하며 선택하지 않을 경우 다음 단계로 진행할 수가 없습니다. 호환이 안 되는 경우도 있고 심지어 선택도 안 되는 경우까지 생깁니다. 애플리케이션 컴파일이 불가능한 최악의 상황이 닥칠 수도 있습니다.

Node.js는 이런 상황을 아예 만들지 않습니다. Node.js 패키지와 NPM은 애플리케이션 A용으로 설치되면서 node_module 폴더에 모입니다.

A 애플리케이션의 디펜던시는 node_modules 폴더 아래 직접 B와 C2 형태로 저장되는데, B도 자신의 C 디펜던시를 내부 디렉터리에 가집니다. 이 경우 별다른 방해 없이 B의 코드에서 C 라이브러리를 참조할 수 있습니다.

따라서 디펜던시 충돌 해결에 대해 고민할 필요가 전혀 없습니다. 문제는 C 라이브러리가 애플리케이션 내에 4번, 20번, 심지어 200번까지도 중복으로 존재할 수 있다는 점입니다.

node_Modules 관리

파일이 아주 많은 경우 관리가 쉽지 않을 수 있습니다. 게다가 너무 많은 파일을 보유하면 애플리케이션이 불안정해질 수도 있죠.

애플리케이션 그래프를 다시 볼까요? Node.js와 NPM은 package.json 파일로 나타냅니다. 이름과 B, C 디펜던시가 보이죠? 일반적으로 디펜던시 버전을 정확하게 지정하지는 않으므로 패치 등으로 버전이 올라가면 애플리케이션에도 적용되곤 합니다.

여기서는 직접 의존하는 디펜던시만 넣고 하위 디펜던시는 명시하지 않았습니다. 문제는 개발 머신이나 테스트 서버에 뭔가 설치하고 애플리케이션을 테스트하는 경우에 발생합니다. 오른쪽 아래에 B3 디펜던시가 있으므로 B3에 종속적이겠죠.

이 경우 테스트를 잘 통과했지만 프로덕션을 디플로이 하거나 빌드하는 시점에 새 디펜던시인 디펜던시 B가 새로운 패치 버전, B4로 올라갈 수도 있습니다. 버전을 명확히 지정하지 않았으므로 애플리케이션이 가장 최신 버전의 디펜던시를 가져올 테니까요.

이런 예측하지 못한 문제가 자주 발생할 수 있습니다. 심지어 어떤 패키지는 NPM에서 사라져 버리는 경우도 발생합니다. 설치하는 동안 네트워크 파티션 문제로 접근이 안 되는 경우도 있습니다.

중요한 애플리케이션을 개발하는 데 이런 문제가 발생하면 곤란하겠죠. 하지만 Facebook도 같은 문제를 겪곤 했습니다.

NPM 패키지 설치 문제

두 가지 문제가 있습니다. 첫 번째는 node_modules가 점점 커진다는 점입니다. 두 번째는 같은 패키지나 json으로 설치해도 다른 애플리케이션이 생성될 수 있다는 점입니다. 즉, 디펜던시를 사용할 수 없거나 시간이 지나서 디펜던시가 변하는 경우입니다.

NPM 설치를 안정화하는 법

Node.js 커뮤니티 소속 회사나 개발자들이 애플리케이션을 안정화하고 예측 가능한 애플리케이션 개발 사이클을 만들기 위해 사용하는 방법을 몇 가지 알려드리겠습니다.

먼저 shrink wrap을 사용하는 방법입니다.

디펜던시 shrink wrap 사용

앞서 그래프처럼 A 애플리케이션에 같은 디펜던시 그래프가 있는 경우를 생각해 보겠습니다. 여기서 package.json 대신 shrink wrap을 생성합니다.

shrink wrap은 package.json과 같지만 트리의 모든 디펜던시를 나열해서 이를 고정한다는 차이점이 있습니다. 따라서 디펜던시 버전이 업데이트되지 않습니다. 즉, 디펜던시는 정확한 tar.gz zip 파일의 URL을 해석해서 해당 리모트에서 다운로드하고 압축을 풉니다.

하지만 문제는 shrink wrap를 사용해도 등록되었던 디펜던시가 삭제되는 것까지 막을 수는 없다는 점입니다. 네트워크 장애 문제도 마찬가지이고요. 빌드 서버가 네트워크 연결에 의존적인 것도, DDos 공격에 노출되는 것도 피하는 것이 좋을 겁니다.

shrink wrap은 애플리케이션에 도입하도록 추천하는 좋은 방법이지만, 이것만으로는 안정적인 빌드 시스템을 아직 완성할 수 없습니다.

node_Modules 체크인

두 번째 방법은 node_modules 체크인입니다. 꽤 오래전에 소개된 방법으로 이미 알고 계실지도 모르겠습니다.

Node와 NPM 커뮤니티가 커지면서 여러 가지 이슈가 발견됐습니다. 예를 들어 shell.js라는 디펜던시를 사용하는 Node.js 애플리케이션이 있고, 이 라이브러리를 0.6에서 0.7로 올리는 마이너 패치를 하면 228개의 파일이 변경됩니다. 변경되는 파일이 지나치게 많으므로 이런 node_modules를 체크인한 후 다시 모든 디펜던시를 체크인해서 해당 쌍을 리뷰하는 것이 정말 어려워집니다.

단지 파일 숫자가 많아서 피곤한 것만은 아닙니다. Facebook의 사례를 말씀드리면 하나의 큰 저장소를 사용하는데요. 한 저장소에 모든 애플리케이션을 보관하면 라이브러리를 공유할 수 있어서 편리하고, 디펜던시의 나락에 빠지는 것을 방지하기 위해서입니다.

또한 Facebook은 디펜던시 관리를 위해 버전 관리 시스템을 신속하게 관리하고 리뷰하는 특별 전속 팀을 따로 두고 있습니다.

디펜던시를 쉽고 안정적으로 유지하고 네트워크에 연결하지 않고도 사용할 수 있도록, 우리는 React Native을 위한 node_modules을 저장소에 커밋할 때 10만 개 이상의 파일을 커밋합니다. 이는 전체 Facebook 저장소와도 비슷한 엄청난 크기죠.

매주 한 두 개의 버전을 업데이트하면 20만 개의 파일이 변경된다고 생각해 보세요. 버전 관리 팀이 우리에게 와서 “모든 팀이 리베이스할 때마다 수십만 개의 파일을 업데이트해야 해서 고통받고 있어요”라고 주의를 줄 겁니다.

앞서 shrink wrap이 tar.gz zip 파일에 의존적이라고 말씀드렸는데, 만약 node_modules에 체크인하는 숫자가 십만 개의 파일에서 좀 줄어든다면 어떨까요? shrink wrap으로 사이즈를 약간 축소했으므로 node_modules 파일 숫자를 줄일 수 있어서 좋지 않을까요?

tar.gz zip 파일 측면에서 생각해보면 주로 30kb 이하의 사이즈로 체크인하기 좋을 만큼 작은 사이즈입니다. 이런 파일이 천 개 이하로 있다면 앞서 말한 대규모의 파일을 처리하는 것보다는 훨씬 수월하겠죠.

Google의 Addy Osmani는 “로컬 디스크에 있는 파일로 node_modules를 설치할 수 있을까”라는 주제의 관련 블로그 글에서 이 방법이 왜 불가능한지, 다른 해결책은 무엇인지 설명했습니다.

결론은 NPM이 서버에서 디펜던시 업데이트 여부를 체크하므로 NPM 클라이언트는 서버가 필요하다는 것입니다. 서버가 없다면 캐시에서 가져오긴 하지만 결국에는 반드시 서버가 필요합니다.

따라서 버전 관리 팀을 괴롭히지 않으려면 다른 해결책이 필요했습니다. 버전 관리에 node_modules를 저장하지 않고 다른 곳에 저장하는 방법을 찾아야 했죠.

서드 파티 저장소

GitHub에 올리는 기본 프로젝트를 생각해 보겠습니다. node_modules은 GitHub에 올리지 않고 대신 네트워크 저장소나 Dropbox 폴더 등에 올리면 어떨까요? 모듈 리스트를 담는 package.json을 바꿀 때마다 node_modules를 압축해서 네트워크에 보내고 이를 package.json과 연결합니다.

팀원들이 마스터 브랜치로 리베이스해서 새로운 package.json을 볼 때마다 업로드해둔 zip 파일을 네트워크로 받아오는 도구가 있으면 상당히 편리할 겁니다. 하지만 문제는 팀 공동 작업을 위한 버전 관리 시스템처럼 네트워크 저장소를 사용할 수 없다는 겁니다.

예를 들어 저나 우리 팀원 누구라도 파일을 변경할 수 있습니다. GitHub을 사용한다면 충돌이 없는 이상 변경 사항을 자동으로 머지해줄 겁니다.

하지만 저장소를 사용하는 경우는 얘기가 다릅니다. 제가 package.json의 디펜던시를 하나 변경했는데 팀메이트가 거의 같은 시간에 다른 디펜던시를 변경했다면 양쪽 모두 네트워크 저장소에 해당 파일을 업로드하겠죠.

GitHub이라면 각각 머지한 다음 마스터 브랜치로 접근하면 양쪽의 변화가 잘 보이겠지만, 바이너리인 두 개의 zip 파일을 이렇게 세 방향으로 자동으로 머지하는 방법은 없습니다.

따라서 다시 두 가지 문제에 봉착합니다. 앞서 말한대로 바이너리 파일을 세 방향으로 머지할 수 없습니다. 게다가 오프라인으로 작업할 수도 없습니다.

Facebook은 런던, 미국, 아시아 오피스가 있습니다. 직원들은 1년에 두 번 이상 오피스 간에 이동하기 때문에 인터넷 접근이 안 되는 비행기 안에서 시간을 보내게 되죠.

Git이나 Mercurial로 브랜치에 체크아웃하면 캐시가 생겨서 기능 사이를 쉽게 전환할 수 있습니다. 하지만 zip 파일이 있는 네트워크에 연결되지 않는다면 애플리케이션을 실행할 수도, 컴파일할 수도 없습니다.

따라서 더 나은 방법을 위해 다른 옵션을 찾아봤습니다. 캐시가 되는 네트워크를 만드는 게 나을지, 아니면 세 방향 머지를 해결할지, 아니면 어떻게든 자동화할지 등을 고려했습니다.

더 나은 방법의 탐색

다시 npm의 작동 방법을 살펴보고 세 번째 옵션을 향상하기 위해 어떤 노력이 필요한지 확인해 보겠습니다.

Nutshell의 NPM CLI

A 애플리케이션의 디펜던시 그래프를 다시 떠올려 볼까요? package.json이 있고, 이제 모든 디펜던시를 등록된 곳에서 다운로드합니다. 압축을 풀고 폴더에 넣습니다.

원격 폴더에서 뭔가 가져오는 것이 정말 앞서 설명한 해결책보다 어려울까요?

이 문제를 조사하면서 NPM 2 버전이 3 버전으로 마이그레이션 되는 큰 변화가 있다는 것에 영감을 받았습니다. NPM 3은 이전보다 훨씬 느려졌기 때문에 Facebook의 많은 개발자들이 최신 노드 버전으로 올리기 싫어했죠. 이 새 NPM 버전이 느려진 이유는 폴더에서 파일을 추출하는 방법을 최적화하는 방법을 사용했기 때문입니다.

예를 들어 트리 여러 곳에서 C 디펜던시의 같은 버전을 중복해서 설치했다면 C를 한 번만 설치하도록 최적화했습니다. 같은 디펜던시는 한 번만 설치하고 링크로 사용하는 것이죠. 이런 최적화 작업 때문에 설치 성능이 크게 저하되었습니다.

NPM 2에서 NPM 3 버전을 올리는 경우의 속도 저하

위 그림은 NPM 2로 패키지를 설치할 때의 네트워크 흐름입니다. 맨 처음 NPM 2가 있고 다음으로 많은 패키지들이 스택처럼 꽤 괜찮은 흐름을 보입니다. 소스로 접근해서 다운로드만 하면 되므로 빈 공간이 많이 발생하지 않습니다.

NPM 3로 업데이트한 다음을 나타내는 두 번째 그림에서는 빈 공간이 많이 발생하는 것을 볼 수 있습니다. NPM 2로 단 12초가 걸렸던 설치 시간이, NPM 3로는 50초의 상당히 긴 시간으로 증가했습니다.

이 상황을 보면서 해결 방법이 없는지 모색하기 시작했습니다. Facebook 사람들은 보통 이런 식으로 작동하는 해결책을 찾기 위해 일하곤 합니다. 그 결과 Yarn이 탄생했습니다.

Yarn

Yarn은 기본적으로 NPM의 리버스 엔지니어링 클라이언트입니다.

Facebook 런던 오피스에서 만들기 시작하면서 가능성을 확인하고, Tilde, Google, Exponent 등 다른 회사의 사람들도 합류해 왔습니다. 다른 회사들이 참여하면서 한 회사에 종속되기보다는 커뮤니티 프로젝트를 만들어서 돕는 방식으로 개발되었죠.

이제 Yarn은 오픈 소스 커뮤니티에서 관리하며, 엔지니어링 역량 지원 등의 형식으로 큰 회사들의 도움을 받고 있습니다. Yarn이 출시된 후 단 2 주 만에 프로젝트 공헌자가 87명으로 늘어났습니다. 아직 많은 이슈가 있지만 문제를 해결하고 개선하기 위해 계속 노력하고 있습니다.

Yarn의 제약 사항

Yarn은 처음부터 몇 가지 제약 사항을 가지고 고안됐습니다. Facebook 내부에서 겪은 node_modules 관리의 고통에서 벗어나면서도 NPM보다 빠르기를 목표로 삼았습니다. 또한, 믿을 수 있는 해결책이면서도 네트워크가 없어도 되고 악성 패키지로부터 보호할 수 있어야 했습니다.

속도

Yarn과 NPM 3의 차이점을 살펴보겠습니다. 먼저 React Native를 설치하는 속도를 비교해보면, 붉은색이 NPM이고 푸른색이 Yarn으로 로컬 캐시를 사용하는지 아니면 이미 만들어진 로그 파일이 있는지에 따라 약간의 차이는 있지만 대부분 Yarn의 속도가 빠른 것을 확인할 수 있습니다.

신뢰성

두 번째 차이점은 따로 shrink wrap을 만들 필요가 없이 Yarn이 이를 만들어 주고 잘 관리한다는 것입니다. 예제를 보면 앞서 보여드린 것과 같은 shrink wrap이지만 이번에는 Yarn을 사용합니다.

NPM과의 차이점은 json 포맷을 없앴다는 점입니다. 또한 모든 패키지가 설치된 이후 이들 모두에 대한 체크섬을 추가했습니다.

Yarn을 사용하면 처음으로 개발 머신에 뭔가 설치하고 프로덕션에서 테스트했는데 이를 다시 설치하고 싶은 경우라도 인터넷에서 다운로드된 파일이 꼬여서 고생할 걱정이 없습니다.

오프라인 모드

이제 이전에 고려했던 옵션에 대해 다시 생각해 보겠습니다. NPM이 로컬 파일로 체크인해서 작업할 수 있을지에 대한 앞선 질문에 대한 답은 ‘예’입니다. 이것이 Yarn의 기본 아이디어죠.

228개의 파일이 바뀌던 결과를 기억하시나요? 이제 Yarn을 사용해서 0.6에서 0.7 버전으로 업데이트하면 위 그림과 같은 결과를 보입니다.

단지 다운로드된 3개의 tar.gz zip 파일만이 바뀝니다. 이를 저장소에 체크하면 package.json 파일이 바뀌고, 변경된 내용을 알고 싶다면 Yarn 로그 파일을 확인하면 됩니다.

그림에는 shell.js 파일로 편집한 새 디펜던시와 로컬 폴더의 위치가 보입니다. Yarn을 사용하면 리뷰도 쉽고 세 방향 머지도 간단해집니다.

결론

결론을 요약하자면, NPM은 커뮤니티에서 효과적으로 동작하는 멋진 툴이지만, 현재 도구로는 node_modules를 잘못 관리하기 쉽다는 것입니다. 그래서 Yarn이 만들어졌고, 개발자에게 아주 유용할 것이라고 자부합니다.

Yarn은 이제 막 시작했을 뿐이지만, 앞으로 JavaScript의 의존성 관리를 위해 많은 발전이 있을 겁니다.

다음: Realm은 모든 JavaScript를 지원합니다: Realm의 유니버설 Node.js를 만나보세요!

General link arrow white

컨텐츠에 대하여

2016년 10월에 진행한 Mobilization 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Konstantin Raev

Konstantin은 Facebook의 React Native 팀에서 일하는 개발자입니다. 최근 몇 년 동안 인프라스트럭처, Continuous Delivery, JavaScript와 안정적인 빌드를 위해 일해왔습니다. Facebook 전에는 뉴질랜드의 스타트업인 Booktrack에서 일했습니다.

4 design patterns for a RESTless mobile integration »

close