우수사례: DevTools를 사용한 Angular 디버깅 개선

디버깅 환경 개선

지난 몇 달 동안 Chrome DevTools팀은 Angular팀과 협업하여 Chrome DevTools의 디버깅 환경을 개선했습니다. 두 팀의 직원들이 힘을 합쳐 소스 언어와 프로젝트 구조 측면에서 친숙하고 관련성 있는 정보에 액세스함으로써 개발자가 작성의 관점에서 웹 애플리케이션을 디버그하고 프로파일링할 수 있도록 지원하는 조치를 취했습니다.

이 게시물에서는 이를 실현하기 위해 Angular 및 Chrome DevTools에서 어떤 변경이 필요했는지 자세히 살펴봅니다. 이러한 변경사항 중 일부는 Angular를 통해 표현되지만 다른 프레임워크에도 적용할 수 있습니다. Chrome DevTools 팀은 다른 프레임워크에서도 사용자에게 더 나은 디버깅 환경을 제공할 수 있도록 새로운 콘솔 API와 소스 지도 확장 지점을 채택하도록 권장합니다.

무시 목록 코드

Chrome DevTools를 사용하여 애플리케이션을 디버깅할 때 작성자는 일반적으로 자신의 코드만 보려고 하며, 아래에 있는 프레임워크의 코드나 node_modules 폴더에 숨겨진 일부 종속 항목은 보고 싶지 않습니다.

이를 위해 DevTools팀에서는 x_google_ignoreList라는 소스 맵 확장 프로그램을 도입했습니다. 이 확장 프로그램은 프레임워크 코드 또는 번들러에서 생성한 코드와 같은 서드 파티 소스를 식별하는 데 사용됩니다. 프레임워크에서 이 확장 프로그램을 사용하면 이제 작성자가 수동으로 구성하지 않아도 보거나 단계별로 진행하고 싶지 않은 코드를 자동으로 피할 수 있습니다.

실제로 Chrome DevTools는 스택 트레이스, 소스 트리, Quick Open 대화상자에서 이와 같이 식별된 코드를 자동으로 숨기고 디버거의 스테핑 및 재개 동작을 개선할 수도 있습니다.

DevTools의 전후를 보여주는 애니메이션 GIF 애프터 이미지에서 DevTools가 작성된 코드를 트리에 표시하고 더 이상 'Quick Open' 메뉴에서 프레임워크 파일을 제안하지 않으며 오른쪽에 훨씬 더 명확한 스택 트레이스를 표시합니다.

x_google_ignoreList 소스 지도 확장 프로그램

소스 맵에서 새 x_google_ignoreList 필드는 sources 배열을 참조하며, 해당 소스 맵에서 알려진 모든 서드 파티 소스의 색인을 나열합니다. 소스 맵을 파싱할 때 Chrome DevTools는 이를 사용하여 코드에서 무시 목록에 추가해야 하는 섹션을 파악합니다.

다음은 생성된 파일 out.js의 소스 맵입니다. 출력 파일 생성에 기여한 원본 sources 두 개(foo.jslib.js)가 있습니다. 전자는 웹사이트 개발자가 작성한 것이고 후자는 그들이 사용한 프레임워크입니다.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

sourcesContent는 이러한 원본 소스 모두에 포함되어 있으며 Chrome DevTools는 Debugger에서 기본적으로 이러한 파일을 표시합니다.

  • 소스 트리의 파일 형식
  • 바로 열기 대화상자의 결과
  • 중단점에서 일시중지된 동안 및 스테핑 중에 오류 스택 트레이스에서 매핑된 호출 프레임 위치.

이제 소스 맵에 다음 정보 중 하나를 추가하여 퍼스트 파티 코드와 서드 파티 코드 소스를 식별할 수 있습니다.

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

x_google_ignoreList 필드에는 sources 배열을 참조하는 단일 색인이 포함됩니다. 1. 이는 lib.js에 매핑된 지역이 무시 목록에 자동으로 추가되어야 하는 서드 파티 코드임을 나타냅니다.

아래 표시된 더 복잡한 예에서 색인 2, 4, 5는 lib1.ts, lib2.coffee, hmr.js에 매핑된 리전이 모두 무시 목록에 자동으로 추가되어야 하는 서드 파티 코드임을 지정합니다.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

프레임워크 또는 번들러 개발자는 빌드 프로세스 중에 생성된 소스 맵에 이 필드가 포함되도록 해야 Chrome DevTools의 새로운 기능을 활용할 수 있습니다.

Angular x_google_ignoreList

Angular v14.1.0부터 node_moduleswebpack 폴더의 콘텐츠는 '무시'로 표시됩니다.

이는 webpack의 Compiler 모듈에 후크하는 플러그인을 만들어 angular-cli를 변경하는 방식으로 구현되었습니다.

Google 엔지니어가 만든 webpack 플러그인PROCESS_ASSETS_STAGE_DEV_TOOLING 단계에 후크를 연결하고 웹팩에서 생성하고 브라우저가 로드하는 최종 애셋의 소스 맵에 x_google_ignoreList 필드를 채웁니다.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

연결된 스택 트레이스

스택 트레이스는 '어떻게 된 것'이라는 질문에 답하지만 대부분 머신의 관점에서 비롯되며 개발자의 관점이나 애플리케이션 런타임의 멘탈 모델과 반드시 일치하는 것은 아닙니다. 이는 일부 작업이 나중에 비동기적으로 실행되도록 예약된 경우 특히 그렇습니다. 이러한 작업의 '근본 원인' 또는 예약 측면에 대해 알아보는 것은 여전히 흥미로울 수 있지만, 이는 정확히 비동기 스택 트레이스의 일부가 아닙니다.

V8에는 내부적으로 setTimeout와 같은 표준 브라우저 예약 프리미티브가 사용될 때 이러한 비동기 작업을 추적하는 메커니즘이 있습니다. 이러한 경우 이 작업이 기본적으로 실행되므로 개발자가 이미 이를 검사할 수 있습니다. 그러나 보다 복잡한 프로젝트에서는 그렇게 간단하지 않습니다. 특히 영역 추적, 사용자 지정 작업 대기열을 수행하거나 시간이 지남에 따라 실행되는 여러 작업 단위로 업데이트를 분할하는 프레임워크와 같은 고급 예약 메커니즘이 있는 프레임워크를 사용할 때는 더욱 간단하지 않습니다.

이를 해결하기 위해 DevTools는 console 객체에 'Async Stack Tagging API'라는 메커니즘을 노출합니다. 이를 통해 프레임워크 개발자는 작업이 예약된 위치와 이러한 작업이 실행되는 위치를 모두 힌트를 얻습니다.

Async Stack Tagging API

비동기 스택 태그를 사용하지 않으면 프레임워크에서 복잡한 방식으로 비동기식으로 실행되는 코드의 스택 트레이스가 예약된 코드에 연결되지 않은 채 표시됩니다.

예약된 시간에 대한 정보가 없는 비동기 실행 코드의 스택 트레이스 `requestAnimationFrame` 에서 시작하는 스택 트레이스만 표시되지만 예약된 시점의 정보는 없습니다.

비동기 스택 태그를 사용하면 이 컨텍스트를 제공할 수 있으며 스택 트레이스는 다음과 같습니다.

예약된 시점에 관한 정보가 포함된 비동기 실행 코드의 스택 트레이스 이전과 달리 스택 트레이스에 `businessLogic` 및 `schedule` 이 포함됩니다.

이렇게 하려면 Async Stack Tagging API에서 제공하는 console.createTask()라는 새로운 console 메서드를 사용합니다. 서명은 다음과 같습니다.

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

console.createTask()를 호출하면 나중에 비동기 코드를 실행하는 데 사용할 수 있는 Task 인스턴스가 반환됩니다.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

비동기 작업도 중첩될 수 있으며 '근본 원인'이 스택 트레이스에 순서대로 표시됩니다.

태스크는 횟수에 제한 없이 실행할 수 있으며, 각 실행마다 작업 페이로드가 다를 수 있습니다. 예약 사이트의 호출 스택은 작업 객체가 가비지로 수집될 때까지 저장됩니다.

Angular의 Async Stack Tagging API

Angular에서는 비동기 작업에서 지속되는 Angular의 실행 컨텍스트인 NgZone이 변경되었습니다.

작업을 예약할 때는 가능한 경우 console.createTask()를 사용합니다. 결과 Task 인스턴스는 추가 사용을 위해 저장됩니다. 작업을 호출하면 NgZone은 저장된 Task 인스턴스를 사용하여 작업을 실행합니다.

이러한 변경사항은 pull 요청 #46693#46958을 통해 Angular의 NgZone 0.11.8에 적용되었습니다.

친근한 통화 프레임

프로젝트를 빌드할 때 프레임워크는 HTML 형식의 코드를 결국 브라우저에서 실행되는 일반 JavaScript로 변환하는 Angular 또는 JSX 템플릿과 같이 프로젝트를 빌드할 때 모든 종류의 템플릿 언어로 코드를 생성하는 경우가 많습니다. 이렇게 생성된 함수 유형에 친숙하지 않은 이름이 지정되기도 합니다. 축소된 후의 단일 문자 이름이나 별다른 구분 없이 모호하거나 생소한 이름이 사용되기도 합니다.

Angular에서는 스택 트레이스에서 AppComponent_Template_app_button_handleClick_1_listener과 같은 이름을 가진 호출 프레임을 볼 수 있습니다.

자동 생성된 함수 이름이 있는 스택 트레이스 스크린샷

이 문제를 해결하기 위해 이제 Chrome DevTools는 소스 맵을 통해 이러한 함수의 이름을 바꿀 수 있습니다. 소스 맵에 함수 범위의 시작 (즉, 매개변수 목록의 왼쪽 괄호)에 대한 이름 항목이 있는 경우 호출 프레임은 스택 트레이스에 이 이름을 표시해야 합니다.

Angular의 친근한 호출 프레임

Angular에서 호출 프레임의 이름을 변경하는 작업은 꾸준히 이루어지고 있습니다. 이러한 개선은 시간이 지남에 따라 점진적으로 적용될 것으로 예상됩니다.

Angular 컴파일러는 작성자가 작성한 HTML 템플릿을 파싱하는 동안 TypeScript 코드를 생성합니다. 이 코드는 결국 브라우저가 로드 및 실행하는 JavaScript 코드로 변환 컴파일됩니다.

이 코드 생성 프로세스의 일부로 소스 맵도 생성됩니다. 현재 소스 맵의 '이름' 필드에 함수 이름을 포함하고 생성된 코드와 원본 코드 간의 매핑에서 해당 이름을 참조하는 방법을 모색하고 있습니다.

예를 들어, 이벤트 리스너의 함수가 생성되고 이름이 친숙하지 않거나 축소 중에 이름이 삭제되면, 이제 소스 맵의 'names' 필드에 이 함수의 더 친숙한 이름을 포함할 수 있으며, 함수 범위 시작의 매핑에서 이 이름 (즉, 매개변수 목록의 왼쪽 괄호)을 참조할 수 있습니다. 그러면 Chrome DevTools가 이 이름을 사용하여 스택 트레이스에서 호출 프레임의 이름을 바꿉니다.

향후 계획

Angular를 테스트 파일럿으로 사용하여 우리의 작업을 확인하는 것은 멋진 경험이었습니다. Google에서는 프레임워크 개발자의 의견을 언제나 경청하고 이러한 확장 포인트에 관한 의견을 제공해 드리고자 합니다.

더 자세히 살펴보고 싶은 영역이 있습니다. 특히 DevTools에서 프로파일링 환경을 개선하는 방법을 다룹니다.