DevTools 아키텍처 업데이트: 자바스크립트 모듈로 이전

Tim van der Lippe
Tim van der Lippe

아시다시피 Chrome DevTools는 HTML, CSS, JavaScript를 사용하여 작성된 웹 애플리케이션입니다. 수년간 DevTools는 더 많은 기능을 갖추고, 더 스마트해졌으며, 더 광범위한 웹 플랫폼에 대한 지식을 갖추게 되었습니다. DevTools는 지난 몇 년간 확장되었지만, 그 아키텍처는 여전히 WebKit의 일부였던 원래 아키텍처와 대체로 유사합니다.

이 게시물은 DevTools 아키텍처의 변경사항 및 빌드 방법을 설명하는 블로그 게시물 시리즈의 일부입니다. DevTools의 이전 작동 방식, 이점과 제한사항, 이러한 제한사항을 완화하기 위해 취한 조치를 설명합니다. 따라서 모듈 시스템, 코드를 로드하는 방법, 마지막으로 JavaScript 모듈을 사용한 방법에 대해 자세히 알아보겠습니다.

처음에는 아무것도 없었습니다.

현재 프런트엔드 환경에는 다양한 모듈 시스템과 이를 중심으로 빌드된 도구, 이제 표준화된 JavaScript 모듈 형식이 있지만 DevTools가 처음 빌드될 때는 이러한 환경이 없었습니다. DevTools는 12여 년 전 WebKit에서 처음 출시된 코드를 기반으로 빌드되었습니다.

DevTools에서 모듈 시스템이 처음 언급된 것은 2012년으로, 연결된 소스 목록이 포함된 모듈 목록의 도입이었습니다. 이는 당시 DevTools를 컴파일하고 빌드하는 데 사용된 Python 인프라의 일부였습니다. 후속 변경으로 2013년에는 모든 모듈을 별도의 frontend_modules.json 파일 (commit)로 추출하고, 2014년에는 별도의 module.json 파일 (commit)로 추출했습니다.

module.json 파일 예시:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

2014년부터 DevTools에서 모듈과 소스 파일을 지정하는 데 module.json 패턴이 사용되었습니다. 한편 웹 생태계는 빠르게 발전했고 UMD, CommonJS, 최종적으로 표준화된 JavaScript 모듈을 비롯한 여러 모듈 형식이 만들어졌습니다. 그러나 DevTools가 module.json 형식을 고수했습니다.

DevTools는 계속 작동했지만 표준화되지 않고 고유한 모듈 시스템을 사용하면 몇 가지 단점이 있었습니다.

  1. module.json 형식에는 최신 번들러와 유사한 맞춤 빌드 도구가 필요했습니다.
  2. IDE 통합이 없었기 때문에 최신 IDE에서 이해할 수 있는 파일을 생성하려면 맞춤 도구가 필요했습니다(VS Code용 jsconfig.json 파일을 생성하는 원본 스크립트).
  3. 모듈 간에 공유할 수 있도록 함수, 클래스, 객체가 모두 전역 범위에 배치되었습니다.
  4. 파일은 순서에 따라 다릅니다. 즉, sources가 표시된 순서가 중요합니다. 사람이 코드를 확인했다는 것 외에는 개발자가 사용하는 코드가 로드된다는 보장은 없습니다.

전반적으로 DevTools의 모듈 시스템과 더 널리 사용되는 다른 모듈 형식의 현재 상태를 평가한 결과, module.json 패턴이 해결하는 것보다 더 많은 문제를 일으키고 있으므로 이를 대체할 계획을 세워야 한다고 결론지었습니다.

표준의 이점

기존 모듈 시스템 중에서 JavaScript 모듈을 이전할 모듈로 선택했습니다. 이 결정을 내릴 당시 JavaScript 모듈은 여전히 Node.js의 플래그 뒤에 배송되고 있었으며 NPM에서 제공되는 많은 패키지에는 사용할 수 있는 JavaScript 모듈 번들이 없었습니다. 그럼에도 불구하고 JavaScript 모듈이 가장 적합한 옵션이라고 결론지었습니다.

JavaScript 모듈의 주요 이점은 JavaScript의 표준화된 모듈 형식이라는 점입니다. module.json의 단점 (위 참조)을 나열한 결과, 거의 모든 것이 표준화되지 않은 고유한 모듈 형식을 사용하는 것과 관련이 있다는 사실을 깨달았습니다.

표준화되지 않은 모듈 형식을 선택하면 유지보육자가 사용하는 빌드 도구 및 도구와의 통합을 빌드하는 데 시간을 투자해야 합니다.

이러한 통합은 종종 취약하고 기능 지원이 부족하여 추가 유지보수 시간이 필요했고, 때로는 미묘한 버그가 발생하여 결국 사용자에게 전송되기도 했습니다.

JavaScript 모듈이 표준이므로 VS Code와 같은 IDE, Closure Compiler/TypeScript와 같은 유형 검사기, Rollup/축소기와 같은 빌드 도구가 작성한 소스 코드를 이해할 수 있습니다. 또한 새로운 유지관리자가 DevTools팀에 합류할 때 독점 module.json 형식을 배우는 데 시간을 들이지 않아도 되는 반면, 이미 JavaScript 모듈에 익숙할 가능성이 높습니다.

물론 DevTools가 처음 빌드되었을 때는 위의 이점 중 어느 것도 존재하지 않았습니다. 표준 그룹, 런타임 구현, JavaScript 모듈을 사용하는 개발자의 수년간의 노력과 의견 수렴을 통해 지금의 단계에 도달할 수 있었습니다. 하지만 JavaScript 모듈을 사용할 수 있게 되자 자체 형식을 계속 유지하거나 새 형식으로 이전하는 데 투자해야 했습니다.

반짝이는 새 제품의 가격

JavaScript 모듈에는 사용하고 싶은 이점이 많았지만 비표준 module.json 환경을 계속 사용했습니다. JavaScript 모듈의 이점을 활용하려면 기술 부채를 정리하고 기능을 손상시키고 회귀 버그를 도입할 수 있는 이전을 실행하는 데 상당한 투자를 해야 했습니다.

이 시점에서 중요한 것은 'JavaScript 모듈을 사용할까요?'가 아니라 'JavaScript 모듈을 사용하려면 얼마나 많은 비용이 드나요?'였습니다. 여기서는 회귀로 인해 사용자에게 발생할 수 있는 위험, 엔지니어가 이전하는 데 소요되는 많은 시간과 비용, 일시적으로 악화되는 상태 간의 균형을 맞춰야 했습니다.

이 마지막 요소가 매우 중요했습니다. 이론적으로는 JavaScript 모듈을 가져올 수 있지만 이전 중에 module.json 및 JavaScript 모듈을 모두 고려해야 하는 코드가 생성됩니다. 이는 기술적으로 달성하기 어려웠을 뿐만 아니라 DevTools 작업을 하는 모든 엔지니어가 이 환경에서 작업하는 방법을 알아야 한다는 의미였습니다. 개발자는 계속해서 '코드베이스의 이 부분이 module.json 모듈인가요? 아니면 JavaScript 모듈인가요?' 그리고 어떻게 해야 변경할 수 있는지 자문해야 했습니다.

미리보기: 동료 유지보육자에게 이전을 안내하는 데 숨겨진 비용이 예상보다 컸습니다.

비용 분석 결과 JavaScript 모듈로 이전하는 것이 여전히 유리하다는 결론을 내렸습니다. 따라서 Google의 주요 목표는 다음과 같습니다.

  1. JavaScript 모듈을 최대한 활용해야 합니다.
  2. 기존 module.json 기반 시스템과의 통합이 안전하고 부정적인 사용자 영향(회귀 버그, 사용자 불만)을 초래하지 않는지 확인합니다.
  3. 모든 DevTools 유지보수자를 대상으로 이전을 안내합니다. 주로 실수로 인한 오류를 방지하기 위해 견제 및 균형 조정 기능이 내장되어 있습니다.

스프레드시트, 변환, 기술 부채

목표는 분명했지만 module.json 형식의 제한사항으로 인해 해결하기가 어려웠습니다. 만족스러운 솔루션을 개발하기까지 여러 번의 반복, 프로토타입, 아키텍처 변경이 필요했습니다. 최종적으로 결정한 이전 전략을 포함하는 설계 문서를 작성했습니다. 설계 문서에는 초기 예상 기간인 2~4주도 명시되어 있습니다.

스포일러 경고: 이전 중 가장 집중적인 부분은 4개월이 걸렸고 처음부터 끝까지는 7개월이 걸렸습니다.

그러나 초기 계획은 시간이 지남에 따라 유지되었습니다. DevTools 런타임에 scripts 배열에 나열된 모든 파일을 이전 방식을 사용하여 module.json 파일에 로드하고 modules 배열에 나열된 모든 파일을 JavaScript 모듈 동적 가져오기를 사용하여 로드하도록 안내했습니다. modules 배열에 있는 모든 파일은 ES 가져오기/내보내기를 사용할 수 있습니다.

또한 2단계(exportimport 단계, 아래 참고)로 마이그레이션을 실행합니다. 마지막 단계는 2개의 하위 단계로 나뉩니다. 대규모 스프레드시트에서 어떤 모듈이 어떤 단계에 있는지 추적했습니다.

JavaScript 모듈 이전 스프레드시트

진행 상황 시트의 스니펫은 여기에서 공개적으로 확인할 수 있습니다.

export단계

첫 번째 단계는 모듈/파일 간에 공유되어야 하는 모든 기호에 export 문을 추가하는 것입니다. 폴더별로 스크립트를 실행하여 변환이 자동화됩니다. 다음 기호가 module.json 세계에 존재한다고 가정해 보겠습니다.

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

여기서 Module은 모듈의 이름이고 File1은 파일의 이름입니다. Sourcetree에서는 front_end/module/file1.js입니다.)

이는 다음과 같이 변환됩니다.

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

처음에는 이 단계에서 동일 파일 가져오기도 다시 작성할 계획이었습니다. 예를 들어 위의 예에서는 Module.File1.localFunctionInFilelocalFunctionInFile로 다시 작성합니다. 하지만 이 두 변환을 분리하면 더 쉽게 자동화하고 더 안전하게 적용할 수 있다는 것을 알게 되었습니다. 따라서 '동일한 파일의 모든 기호 이전'이 import 단계의 두 번째 하위 단계가 됩니다.

파일에 export 키워드를 추가하면 파일이 '스크립트'에서 '모듈'로 변환되므로 많은 DevTools 인프라를 적절하게 업데이트해야 했습니다. 여기에는 런타임 (동적 가져오기 포함)뿐만 아니라 모듈 모드에서 실행하는 ESLint와 같은 도구도 포함되었습니다.

이러한 문제를 해결하면서 한 가지 발견한 점은 테스트가 '잘못된' 모드로 실행되었다는 것입니다. JavaScript 모듈은 파일이 "use strict" 모드에서 실행된다고 암시하므로 이는 테스트에도 영향을 미칩니다. 알고 보니 with 문 😱을 사용한 테스트를 포함하여 상당한 양의 테스트가 이 부주의에 의존하고 있었습니다.

결국 export 문을 포함하도록 첫 번째 폴더를 업데이트하는 데 약 1주일relands를 사용한 여러 번의 시도가 걸렸습니다.

import-phase

모든 기호가 export 문을 사용하여 내보내지고 전역 범위(기존)에 남아 있으므로 ES 가져오기를 사용하려면 교차 파일 기호에 대한 모든 참조를 업데이트해야 했습니다. 최종 목표는 모든 '기존 내보내기 객체'를 삭제하여 전역 범위를 정리하는 것입니다. 폴더별로 스크립트를 실행하여 변환이 자동화됩니다.

예를 들어 module.json에 존재하는 다음 기호의 경우는 다음과 같습니다.

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

다음과 같이 변환됩니다.

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

그러나 이 접근 방식에는 몇 가지 단점이 있습니다.

  1. 모든 기호가 Module.File.symbolName로 이름이 지정된 것은 아닙니다. 일부 기호는 Module.File 또는 Module.CompletelyDifferentName로만 이름이 지정되었습니다. 이 불일치로 인해 이전 전역 객체에서 새 가져온 객체로 내부 매핑을 만들어야 했습니다.
  2. moduleScoped 이름 간에 충돌이 발생하는 경우도 있습니다. 가장 눈에 띄는 것은 특정 유형의 Events를 선언하는 패턴으로, 각 기호의 이름은 Events로 지정되었습니다. 즉, 여러 파일에 선언된 여러 유형의 이벤트를 리슨하는 경우 이러한 Eventsimport 문에 이름 충돌이 발생합니다.
  3. 파일 간에 순환 종속성이 있는 것으로 확인되었습니다. 모든 코드가 로드된 후에 기호가 사용되었으므로 전역 범위 컨텍스트에서는 문제가 없었습니다. 그러나 import가 필요한 경우 순환 종속 항목이 명시됩니다. 전역 범위 코드에 DevTools에도 있었던 부작용 함수 호출이 없는 한 즉각적인 문제는 아닙니다. 전반적으로 변환을 안전하게 수행하려면 수술과 리팩터링이 필요했습니다.

JavaScript 모듈이 있는 완전히 새로운 세상

2019년 9월에 시작된 지 6개월 후인 2020년 2월에 ui/ 폴더에서 마지막 정리가 실행되었습니다. 이로써 마이그레이션이 비공식적으로 종료되었습니다. 문제가 해결된 후 이전을 2020년 3월 5일에 완료됨으로 공식적으로 표시했습니다. 🎉

이제 DevTools의 모든 모듈은 JavaScript 모듈을 사용하여 코드를 공유합니다. 기존 테스트를 위해 또는 DevTools 아키텍처의 다른 부분과 통합하기 위해 여전히 전역 범위(module-legacy.js 파일)에 일부 기호를 배치합니다. 이러한 기능은 시간이 지남에 따라 삭제되지만 향후 개발을 방해하는 요소는 아닙니다. JavaScript 모듈 사용을 위한 스타일 가이드도 있습니다.

통계

이 이전에 참여하는 CL(변경사항 목록의 약어 - Gerrit에서 변경사항을 나타내는 용어로 GitHub의 pull 요청과 유사)의 수는 대략 250개로, 대부분 2명의 엔지니어가 수행합니다. 변경된 규모에 관한 확실한 통계는 없지만 변경된 행의 보수적인 추정치(각 CL의 삽입과 삭제 간의 절대 차이의 합계로 계산됨)는 약 30,000개(모든 DevTools 프런트엔드 코드의 약 20%)입니다.

export를 사용하는 첫 번째 파일은 Chrome 79에 제공되며 2019년 12월에 안정화 버전으로 출시되었습니다. import로 이전하는 마지막 변경사항은 2020년 5월에 안정화 버전으로 출시된 Chrome 83에서 제공되었습니다.

Chrome 안정화 버전에 출시되었으며 이번 이전의 일환으로 도입된 한 가지 회귀가 있습니다. 외부 default 내보내기로 인해 명령어 메뉴의 스니펫 자동 완성이 중단되었습니다. 다른 회귀도 몇 가지 있었지만 자동화된 테스트 모음과 Chrome Canary 사용자가 이를 신고하여 Chrome 안정화 버전 사용자에게 도달하기 전에 수정했습니다.

crbug.com/1006759에서 전체 여정 (모든 CL이 이 버그에 첨부된 것은 아니지만 대부분 포함)을 확인할 수 있습니다.

알게 된 점

  1. 과거에 내린 결정은 프로젝트에 오래 지속되는 영향을 미칠 수 있습니다. JavaScript 모듈 (및 기타 모듈 형식)을 오랫동안 사용할 수 있었지만 DevTools는 이전을 정당화할 수 없었습니다. 마이그레이션 시기 및 시기를 결정하는 것은 어렵고 정보에 기반한 추측에 근거합니다.
  2. 초기 예상 기간은 몇 개월이 아닌 몇 주였습니다. 이는 주로 초기 비용 분석에서 예상했던 것보다 더 많은 예상치 못한 문제를 발견했기 때문에 발생합니다. 이전 계획은 탄탄했지만 기술 부채가 원치 않게 자주 방해가 되었습니다.
  3. JavaScript 모듈 이전에는 (관련이 없어 보이는) 대량의 기술적 부채 정리가 포함되었습니다. 현대화된 표준화된 모듈 형식으로 이전함으로써 코딩 권장사항을 최신 웹 개발에 맞게 조정할 수 있었습니다. 예를 들어 맞춤 Python 번들러를 최소 롤업 구성으로 대체할 수 있었습니다.
  4. 코드베이스에 큰 영향을 미쳤음에도 불구하고 (코드의 20% 가 변경됨) 회귀는 거의 보고되지 않았습니다. 처음 몇 개의 파일을 이전하는 데는 여러 문제가 있었지만, 얼마 후에는 부분적으로 자동화된 안정적인 워크플로를 갖추게 되었습니다. 즉, 이번 이전은 안정적인 사용자에게 미치는 부정적인 영향이 최소화되었습니다.
  5. 동료 유지보수자에게 특정 이전의 복잡한 사항을 교육하는 것은 어렵고 때로는 불가능합니다. 이러한 규모의 마이그레이션은 따라가기 어려우며 많은 도메인 지식이 필요합니다. 이러한 도메인 지식을 동일한 코드베이스에서 작업하는 다른 사용자에게 전달하는 것은 그들이 수행하는 작업에 그 자체로 바람직하지 않습니다. 무엇을 공유하고 무엇을 공유하지 말아야 하는지 아는 것은 예술이지만 필수적인 예술입니다. 따라서 대규모 마이그레이션의 양을 줄이거나 적어도 동시에 수행하지 않는 것이 중요합니다.

미리보기 채널 다운로드

Chrome Canary, Dev 또는 베타를 기본 개발 브라우저로 사용하는 것이 좋습니다. 이러한 미리보기 채널을 통해 최신 DevTools 기능에 액세스할 수 있고, 최첨단 웹 플랫폼 API를 테스트할 수 있으며, 사용자보다 먼저 사이트에서 문제를 발견할 수 있습니다.

Chrome DevTools 팀에 문의하기

다음 옵션을 사용하여 DevTools와 관련된 새로운 기능, 업데이트 또는 기타 사항을 논의하세요.