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

향상된 디버깅 환경

지난 몇 개월 동안 Chrome DevTools팀은 Angular팀과 협력하여 Chrome DevTools의 디버깅 환경을 개선했습니다. 두 팀의 담당자가 함께 협력하여 개발자가 작성 관점에서 웹 애플리케이션을 디버그하고 프로파일링할 수 있도록 소스 언어 및 프로젝트 구조 측면에서 익숙하고 관련성 높은 정보에 액세스할 수 있도록 조치를 취했습니다.

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

무시 목록 코드

Chrome DevTools를 사용하여 애플리케이션을 디버그할 때 작성자는 일반적으로 하위 프레임워크의 코드나 node_modules 폴더에 숨겨진 일부 종속 항목이 아닌 자신의 코드만 보고 싶어 합니다.

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

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

DevTools의 전후를 보여주는 애니메이션 GIF입니다. 후 이미지에서 DevTools가 트리에 작성된 코드를 표시하고, 더 이상 '빠른 열기' 메뉴에 프레임워크 파일을 추천하지 않으며, 오른쪽에 훨씬 더 깔끔한 스택 트레이스를 표시하는 것을 볼 수 있습니다.

x_google_ignoreList 소스 맵 확장자

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

아래는 생성된 파일 out.js의 소스 맵입니다. 출력 파일 생성에 기여한 원본 sourcesfoo.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는 디버거 전체에 기본적으로 이러한 파일을 표시합니다.

  • 소스 트리의 파일로
  • 빠른 열기 대화상자의 결과로 표시됩니다.
  • 중단점에서 일시중지되어 있을 때와 스테핑 중에 오류 스택 트레이스의 매핑된 호출 프레임 위치

이제 소스 맵에 포함하여 이러한 소스 중에서 퍼스트 파티 코드인지 서드 파티 코드인지 식별할 수 있는 정보가 하나 더 있습니다.

{
  ...
  "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 단계에 후크를 연결하고 webpack에서 생성하고 브라우저에서 로드하는 최종 애셋의 소스 맵에 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 인스턴스를 사용하여 태스크를 실행합니다.

이 변경사항은 풀 리퀘스트 #46693#46958을 통해 Angular의 NgZone 0.11.8에 적용되었습니다.

친숙한 호출 프레임

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

Angular에서는 스택 트레이스에 AppComponent_Template_app_button_handleClick_1_listener과 같은 이름이 있는 호출 프레임이 표시되는 경우가 많습니다.

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

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

Angular의 친숙한 호출 프레임

Angular에서 호출 프레임의 이름을 바꾸는 작업은 계속 진행 중입니다. 이러한 개선사항은 시간이 지남에 따라 점진적으로 적용될 예정입니다.

작성자가 작성한 HTML 템플릿을 파싱하는 동안 Angular 컴파일러는 TypeScript 코드를 생성하며, 이 코드는 결국 브라우저에서 로드하고 실행하는 JavaScript 코드로 트랜스파일됩니다.

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

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

향후 계획

Angular를 테스트 파일럿으로 사용하여 작업을 확인하는 것은 멋진 경험이었습니다. 프레임워크 개발자의 의견을 듣고 이러한 확장 포인트에 관한 의견을 제공해 주시기 바랍니다.

YouTube는 더 많은 영역을 탐색하고자 합니다. 특히 DevTools에서 프로파일링 환경을 개선하는 방법을 알아봅니다.