Remove @ts-expect-error in BackgroundServiceModel

MERGED2026devtoolsrefactor
2026. 8. 15.jsy8315 프로필 이미지jsy8315

Chrome DevTools 의 JSDoc → TypeScript 마이그레이션 과정에서 억제해둔 타입 오류를 실제로 해결했습니다. front_end/panels/application/BackgroundServiceModel.ts@ts-expect-error 를 제거하고 Platform.assertNotNullOrUndefined 로 코드의 전제를 명시했습니다.

문제 설명

DevTools 프론트엔드는 과거 JSDoc 주석으로 타입을 달던 JavaScript 코드베이스였고, TypeScript 로 전환하는 과정에서 당장 해결하기 어려운 타입 오류를 억제문으로 덮어두고 넘어갔습니다. 그 흔적이 지금도 남아 있습니다.

// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
this.events.get(backgroundServiceEvent.service).push(backgroundServiceEvent);

Map.prototype.get() 의 반환 타입은 V | undefined 이므로 곧바로 .push() 를 호출하면 타입 오류가 발생합니다. 억제문으로 컴파일은 통과시켰지만, 키가 없을 때 런타임에 TypeError 가 발생한다는 사실은 그대로 남아 있고 코드에서는 보이지 않게 되었습니다.

추적 이슈 기준으로 이런 잔재가 65건 남아 있습니다.

no-explicit-any     32
@ts-expect-error    25
naming-convention    6

관련 CL 이 2024년 2월 이후 올라오지 않아 2년 반가량 정체된 영역이었습니다.

추적 이슈 번호는 Monorail 에서 issues.chromium.org 로 이관되며 재발급되었습니다. 소스 주석은 옛 번호(crbug.com/1172300)를 그대로 쓰고 있지만, 커밋 메시지의 Bug: 에는 새 번호 40166562 를 적어야 합니다.

해결 내용

1. 대상 선정

한 파일에 억제문이 여러 개 있거나 타입을 고치면 호출부까지 파급되는 건은 제외하고, 근거가 코드로 증명되는 한 건을 골랐습니다.

후보 판단
panels/application/IndexedDBModel.ts getter 의 선언 타입과 실제 반환 타입이 달라 호출부까지 파급
models/persistence/IsolatedFileSystem.ts 실제 잠재 버그이나 동작 변경이 필요해 합의가 선행되어야 함
panels/media/TickingFlameChart.ts 생성자 시그니처 불일치
panels/profiler/HeapProfileView.ts 한 파일에 5건
panels/application/BackgroundServiceModel.ts 한 건이고 전제를 같은 파일에서 확인 가능

2. 수정 내용

+import * as Platform from '../../core/platform/platform.js';
 import * as SDK from '../../core/sdk/sdk.js';
   backgroundServiceEventReceived({backgroundServiceEvent}:
                                      Protocol.BackgroundService.BackgroundServiceEventReceivedEvent): void {
-    // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
-    // @ts-expect-error
-    this.events.get(backgroundServiceEvent.service).push(backgroundServiceEvent);
+    const events = this.events.get(backgroundServiceEvent.service);
+    Platform.assertNotNullOrUndefined(events);
+    events.push(backgroundServiceEvent);
     this.dispatchEventToListeners(Events.BackgroundServiceEventReceived, backgroundServiceEvent);
   }

assertNotNullOrUndefinedasserts val is NonNullable<T> 시그니처를 가지므로, 이 호출을 통과한 뒤에는 컴파일러가 events 의 타입을 좁혀 인식합니다. 별도 캐스팅이 필요하지 않습니다.

변경 규모는 AUTHORS 한 줄을 포함해 2개 파일, 5줄 추가 3줄 삭제입니다.

3. 왜 옵셔널 체이닝이 아니라 assert 인가

억제문을 지울 때 무엇으로 대체하느냐가 이 CL 의 실질적인 판단이었습니다.

방법 전제가 깨졌을 때
get(s)?.push(e) 조용히 무시 → 버그를 숨김
?? []set() 정상처럼 처리
assertNotNullOrUndefined 명확한 메시지와 함께 예외

기존 동작이 이미 TypeError 였으므로 assert 는 런타임 동작을 바꾸지 않고 오류 메시지만 명확하게 합니다. 반면 옵셔널 체이닝은 "절대 undefined 면 안 되는 값"에 쓰면 프로토콜 계약 위반을 조용히 삼켜 원인 파악을 어렵게 만듭니다.

키가 항상 존재한다는 근거는 같은 파일에서 확인할 수 있었습니다.

  • enable()this.events.set(...) 을 먼저 호출하고 그 뒤에 startObserving() 을 호출합니다. 이벤트가 도착하는 시점에는 항상 키가 존재합니다.
  • 이 맵에서 키를 삭제하는 코드가 없습니다.

assertNotNullOrUndefined 는 DevTools 프로덕션 코드 여러 곳에서 쓰이는 관례이며, ui/legacy/Treeoutline.ts 가 같은 형태로 사용합니다.

테스트 방법

npm run build
npm run lint -- --lint-only front_end/panels/application/BackgroundServiceModel.ts
  • DevTools 는 autoninja 가 아니라 npm 스크립트로 빌드합니다. npm run build 가 타입 검사를 포함하므로, 억제문을 제거한 뒤에도 타입 오류가 없음을 확인했습니다.
  • npm run lint 는 로컬 기본값이 자동 수정 ON 이라 파일을 조용히 고치고 통과할 수 있습니다. 봇과 같은 조건(캐시 OFF, 자동 수정 OFF)으로 검증하려면 --lint-only 를 붙여야 하고, 실행 후 git status 로 파일이 변경되지 않았는지 확인해야 합니다.
  • 패치셋 2에서 CQ dry run 을 통과했습니다.
  • 런타임 동작 변화는 없습니다. 전제가 지켜지는 정상 경로에서는 동일하게 동작하고, 위반 시에만 오류 메시지가 명확해집니다.

배운 점

  • CQ 실패 요약문은 원인이 아니라 실패한 스텝의 이름입니다. 이 CL 의 dry run 이 Failure in Lint Check 로 실패했지만, 빌드 로그를 7단계 내려가 보니 실제 원인은 봇에서 third_party/depot_tools/vpython3 를 찾지 못한 것이었고 ESLint 는 시작조차 하지 못한 상태였습니다. 같은 봇의 다른 CL 두 건이 동일한 지점에서 실패한 것이 인프라 문제라는 결정적 근거가 되었습니다. 내 코드가 남의 CL 빌드를 깨뜨릴 방법은 없기 때문입니다. 이 근거를 코멘트로 정리해 남기자 리뷰어가 바로 상황을 확인해 주었습니다.

  • 외부 기여자는 Code-Review+1 이 두 개 필요합니다. Gerrit 의 제출 조건식이 커미터에게는 한 개, 그 외에는 두 개를 요구합니다. 최근 머지된 외부 기여자 CL 을 확인해 보면 대부분 처음부터 리뷰어를 2~3명 지정합니다. 한 명만 넣고 기다리면 정체됩니다.

  • 리뷰어가 배정을 다른 사람에게 넘기면 그 판단을 존중해야 합니다. 처음 지정한 리뷰어가 스스로를 제거하고 다른 담당자를 넣었는데, 이를 원래대로 되돌렸다가 반나절 넘게 리뷰가 진행되지 않았습니다. 넘긴 쪽에서는 이미 처리를 마친 CL 이라 다시 볼 이유가 없었던 것입니다. 리뷰어가 바뀌었을 때는 누가 바꿨는지를 먼저 확인해야 합니다.

  • git config diff.ignoreSubmodules all 이 필요합니다. buildthird_party/depot_tools 는 실제 git 서브모듈인데 gclient 가 기록된 리비전보다 새 커밋으로 체크아웃해 두어 항상 diff 가 남고, presubmit 의 CheckNoUncheckedFiles 가 이를 잡아 업로드를 막습니다. git checkout -- build 로는 인덱스만 되돌아가 즉시 재발하므로 설정을 바꿔야 합니다. 기본값은 depot_tools 가 넣어둔 dirty 이며, 커밋 포인터 차이까지 무시하려면 all 이 필요합니다.

참고 자료