Electron Internals: Building Chromium as a Library
Electron은 Google의 오픈소스 프로젝트인 Chromium을 기반으로 한다. Chromium은 원래 다른 프로젝트에서 사용하기 위해 설계된 것은 아니다. 이 글은 Chromium이 어떻게 Electron에서 사용할 수 있는 라이브러리로 구축되었는지, 그리고 빌드 시스템이 시간이 지나며 어떻게 발전해 왔는지를 소개한다.
CEF 사용하기
Chromium Embedded Framework(CEF)는 Chromium을 라이브러리로 변환하고, Chromium 코드베이스를 기반으로 안정적인 API를 제공하는 프로젝트다. Atom 에디터와 NW.js의 초기 버전은 CEF를 사용했다.
안정적인 API를 유지하기 위해 CEF는 Chromium의 모든 세부 사항을 숨기고 Chromium의 API를 자체 인터페이스로 감싼다. 따라서 웹 페이지에 Node.js를 통합하는 것과 같이 Chromium의 내부 API에 접근해야 할 때, CEF의 장점이 오히려 방해 요소가 되었다.
결국 Electron과 NW.js는 Chromium API를 직접 사용하는 방식으로 전환했다.
Chromium의 일부로 빌드하기
Chromium은 공식적으로 외부 프로젝트를 지원하지 않지만, 코드베이스가 모듈식으로 구성되어 있어 최소한의 브라우저를 쉽게 구축할 수 있다. 브라우저 인터페이스를 제공하는 핵심 모듈은 Content Module이라고 한다.
Content Module을 사용해 프로젝트를 개발하려면, Chromium의 일부로 프로젝트를 빌드하는 것이 가장 간단한 방법이다. 이를 위해 먼저 Chromium의 소스 코드를 체크아웃한 다음, 프로젝트를 Chromium의 DEPS
파일에 추가한다.
NW.js와 초기 버전의 Electron은 이 방식을 사용해 빌드했다.
하지만 Chromium은 매우 큰 코드베이스이기 때문에 강력한 머신이 필요하다. 일반적인 노트북에서는 5시간 이상 걸릴 수 있다. 이는 프로젝트에 기여할 수 있는 개발자 수를 크게 제한하고, 개발 속도도 느리게 만든다.
Chromium을 단일 공유 라이브러리로 빌드하기
Content Module을 사용하는 Electron은 대부분의 경우 Chromium 코드를 수정할 필요가 없다. 따라서 Electron 빌드를 개선하는 명확한 방법은 Chromium을 공유 라이브러리로 빌드한 다음, Electron에서 이를 링크하는 것이다. 이 방식은 개발자가 Electron에 기여할 때 Chromium 전체를 빌드할 필요가 없게 해준다.
이러한 목적으로 @aroben이 libchromiumcontent 프로젝트를 만들었다. 이 프로젝트는 Chromium의 Content Module을 공유 라이브러리로 빌드하고, Chromium 헤더와 사전 빌드된 바이너리를 다운로드할 수 있게 제공한다. libchromiumcontent 초기 버전의 코드는 이 링크에서 확인할 수 있다.
brightray 프로젝트도 libchromiumcontent의 일부로 탄생했으며, Content Module 주변에 얇은 레이어를 제공한다.
libchromiumcontent와 brightray를 함께 사용하면 개발자는 Chromium 빌드의 세부 사항에 깊이 들어가지 않고도 빠르게 브라우저를 빌드할 수 있다. 또한 이 방식은 프로젝트 빌드를 위해 빠른 네트워크와 강력한 머신이 필요하지 않게 해준다.
Electron 외에도 Breach browser와 같은 다른 Chromium 기반 프로젝트들도 이 방식으로 빌드되었다.
내보낸 심볼 필터링
Windows에서는 하나의 공유 라이브러리가 내보낼 수 있는 심볼의 수에 제한이 있다. Chromium 코드베이스가 커지면서 libchromiumcontent에서 내보내는 심볼의 수가 이 제한을 넘어섰다.
이 문제를 해결하기 위해 DLL 파일을 생성할 때 필요하지 않은 심볼을 필터링하는 방법을 도입했다. 이 방법은 링커에 .def
파일을 제공한 후, 특정 네임스페이스 아래의 심볼을 내보낼지 여부를 판단하는 스크립트를 사용해 구현했다.
이 접근 방식을 통해 Chromium이 새로운 내보낸 심볼을 계속 추가하더라도, libchromiumcontent는 더 많은 심볼을 제거함으로써 공유 라이브러리 파일을 생성할 수 있었다.
컴포넌트 빌드
libchromiumcontent의 다음 단계를 설명하기 전에, 먼저 Chromium의 컴포넌트 빌드 개념을 소개하는 것이 중요하다.
Chromium은 거대한 프로젝트이기 때문에 빌드 시 링크 단계에서 많은 시간이 소요된다. 일반적으로 개발자가 작은 변경을 가할 때, 최종 결과물을 확인하려면 10분 이상 걸릴 수 있다. 이를 해결하기 위해 Chromium은 컴포넌트 빌드를 도입했다. 이 방식은 Chromium의 각 모듈을 별도의 공유 라이브러리로 빌드하여, 최종 링크 단계에서 소요되는 시간을 거의 없앤다.
원시 바이너리 배포
Chromium이 계속 성장하면서, Chromium에서 내보내는 심볼이 너무 많아져 Content Module과 Webkit의 심볼도 제한을 초과했다. 단순히 심볼을 제거해서 사용 가능한 공유 라이브러리를 생성하는 것은 불가능했다.
결국, Chromium의 원시 바이너리를 배포하는 방법을 선택했다. 단일 공유 라이브러리를 생성하는 대신 이 방식을 채택했다.
앞서 소개한 것처럼 Chromium에는 두 가지 빌드 모드가 있다. 원시 바이너리를 배포하게 되면서, libchromiumcontent에서 두 가지 다른 바이너리 배포판을 제공해야 했다. 하나는 static_library
빌드로, Chromium의 일반 빌드에서 생성된 각 모듈의 정적 라이브러리를 모두 포함한다. 다른 하나는 shared_library
빌드로, 컴포넌트 빌드에서 생성된 각 모듈의 공유 라이브러리를 모두 포함한다.
Electron에서는 Debug 버전이 libchromiumcontent의 shared_library
버전과 링크된다. 이 버전은 다운로드 크기가 작고, 최종 실행 파일을 링크하는 데 시간이 적게 걸리기 때문이다. 반면 Release 버전은 libchromiumcontent의 static_library
버전과 링크된다. 이렇게 하면 컴파일러가 디버깅에 중요한 전체 심볼을 생성할 수 있고, 링커가 필요한 오브젝트 파일을 정확히 알고 있기 때문에 더 나은 최적화를 수행할 수 있다.
일반적인 개발 과정에서는 개발자가 Debug 버전만 빌드하면 된다. 이 버전은 네트워크나 강력한 머신이 필요하지 않다. Release 버전은 더 좋은 하드웨어가 필요하지만, 더 최적화된 바이너리를 생성할 수 있다.
gn
업데이트
세계에서 가장 큰 프로젝트 중 하나인 Chromium을 빌드하기에는 일반적인 시스템이 적합하지 않다. Chromium 팀은 자체 빌드 도구를 개발했다.
이전 버전의 Chromium은 gyp
를 빌드 시스템으로 사용했지만, 속도가 느리고 복잡한 프로젝트에서는 설정 파일을 이해하기 어려웠다. 수년간의 개발 끝에 Chromium은 더 빠르고 명확한 구조를 가진 gn
으로 빌드 시스템을 전환했다.
gn
의 개선점 중 하나는 source_set
을 도입한 것이다. source_set
은 오브젝트 파일 그룹을 나타낸다. gyp
에서는 각 모듈이 static_library
나 shared_library
로 표현되었고, Chromium의 일반적인 빌드에서 각 모듈은 정적 라이브러리를 생성한 후 최종 실행 파일에 함께 링크되었다. gn
을 사용하면 이제 각 모듈이 오브젝트 파일 묶음을 생성하고, 최종 실행 파일이 모든 오브젝트 파일을 함께 링크한다. 따라서 중간 정적 라이브러리 파일은 더 이상 생성되지 않는다.
이러한 개선은 libchromiumcontent에게 큰 문제를 일으켰다. libchromiumcontent는 실제로 중간 정적 라이브러리 파일이 필요했기 때문이다.
이 문제를 해결하기 위한 첫 번째 시도는 gn
을 패치하여 정적 라이브러리 파일을 생성하게 하는 것이었다. 이 방법은 문제를 해결했지만, 적절한 해결책과는 거리가 멀었다.
두 번째 시도는 @alespergl이 오브젝트 파일 목록에서 커스텀 정적 라이브러리를 생성하는 방법을 제안한 것이다. 이 방법은 더미 빌드를 먼저 실행해 생성된 오브젝트 파일 목록을 수집한 후, 그 목록을 gn
에 제공해 실제로 정적 라이브러리를 빌드하는 트릭을 사용했다. 이 방법은 Chromium의 소스 코드를 최소한으로 변경했고, Electron의 빌드 구조를 그대로 유지할 수 있었다.
요약
Chromium을 일부로 포함해 Electron을 빌드하는 방식과 Chromium을 라이브러리로 빌드하는 방식을 비교해 보면, 후자가 더 많은 노력과 지속적인 유지보수를 필요로 한다. 하지만 Chromium을 라이브러리로 빌드하면 강력한 하드웨어 없이도 Electron을 빌드할 수 있어, 더 많은 개발자가 Electron을 빌드하고 기여할 수 있게 된다. 이러한 노력은 충분히 가치가 있다.