[payments] Move manifest parser error strings to native_error_strings
결제 매니페스트 파서(PaymentManifestParser)가 인라인으로 조립하던 에러 메시지 26곳을 //components/payments의 공용 에러 문자열 모음(native_error_strings)으로 옮겼습니다(crbug.com/40681786). 이슈 트래커가 아니라 코드에 박힌 TODO를 역추적해 발굴한 첫 기여이고, 한 이슈에 걸린 세 가지 작업 중 첫 번째 CL입니다. 2026-09-09 머지(3f65cb3afb2d1).
문제 설명
- issue url: https://crbug.com/40681786
- 파서는 매니페스트 형식 오류를 devtools 콘솔에 로그하는데, 메시지를
base::StringPrintf("\"%s\" must be a list.", kDefaultApplications)처럼 printf 스타일로 그 자리에서 조립합니다. - //components/payments의 나머지 코드는 에러 문자열을
components/payments/core/native_error_strings.{h,cc}에 모아두고$1플레이스홀더 +base::ReplaceStringPlaceholders()로 씁니다(다운로더가 그 예). 파서만 이 컨벤션에서 벗어나 있었고, 코드에TODO(crbug.com/40681786): Move the error message strings to components/payments/core/native_error_strings.cc.가 박혀 있었습니다. - 이슈 자체는 더 큰 그림입니다: ① 파서 에러를 호출자에게 반환해
PaymentRequest.show()거부 메시지에 상세를 붙이기 → ② 그 뒤default_applicationssame-origin 검사를 파서에서 조기 수행 → ③ 문자열 이동. ③은 ①이 문자열을 재사용할 수 있게 하는 준비 작업입니다.
해결 내용
핵심은 "복사"가 아니라 컨벤션 변환이었습니다. printf 포맷 문자열을 그대로 extern 상수로 빼면 base::StringPrintf의 format 인자가 비-리터럴이 되어 -Wformat-nonliteral에 걸립니다. 그래서 26곳 전부를 $N 플레이스홀더로 바꾸고 호출부를 base::ReplaceStringPlaceholders()로 교체했습니다.
// before
log.Error(base::StringPrintf("\"%s\" must contain at most %zu entries.",
kDefaultApplications, kMaximumNumberOfItems));
// after
log.Error(base::ReplaceStringPlaceholders(
errors::kManifestMemberTooManyEntries,
{kDefaultApplications, base::NumberToString(kMaximumNumberOfItems)},
nullptr));- 같은 포맷은 상수 하나로 합쳤습니다(26곳 → 상수 23개).
default_applications와supported_origins의 개수 제한 메시지를 합치면서 후자에 있던 "entires" 오타가 함께 고쳐졌고, 커밋 메시지에 그 사실을 명시했습니다. - 헤더가 엄격한 알파벳순이 아니라 기능별 그룹(SPC 등)이 뒤에 붙는 구조임을 확인하고, "파서 에러" 그룹 블록으로 추가했습니다.
- 범위 판단: 남은
StringPrintf13곳은 전부log.Warn()경고라 TODO의 "error message"에 해당하지 않아 유지했고, 커밋 메시지에 "Warning messages are left as they are"로 밝혔습니다. - 이슈의 본론(①)이 남으므로
Fixed:대신Bug: 40681786을 썼습니다. - BUILD.gn도 한 줄 필요했습니다. 처음에는 파서 타깃이 이미
//components/payments/core에 의존하니 그만이라고 판단했는데,core는:error_strings를 **privatedeps**로만 갖고 있어 헤더 사용 권한이 전파되지 않습니다.//components/payments/content/utility:utility에//components/payments/core:error_strings를 직접 추가해야 했고, 이 사실은 CQ가 알려줬습니다(아래).
테스트 방법
동작 불변 리팩토링이므로 기존
PaymentManifestParserTest90/90 통과가 검증입니다(unittest는 에러 문자열 텍스트를 검증하지 않아 깨질 것도 없었습니다).진짜 검증은 CQ였습니다. 리뷰어가 승인하고 커밋 큐를 돌리자 GN 헤더 검사에서 실패했습니다:
ERROR at //components/payments/content/utility/payment_manifest_parser.cc:23:11: Can't include this header from here. #include "components/payments/core/native_error_strings.h" The target: //components/payments/content/utility:utility is including a file from the target: //components/payments/core:error_strings로컬 컴파일은 멀쩡히 통과합니다 — include 경로 자체는 유효하니까요. GN의 의존성 검사만 잡아내는 종류의 실패이고, 컨트리뷰터에게는 tryjob 권한이 없어 업로드 후에야 알 수 있었습니다. BUILD.gn 한 줄을 추가해 재업로드했고, 그 다음 CQ에서 통과·머지됐습니다.
빌드에서 값비싼 교훈을 얻었습니다. 브랜치를 빌드된 트리와 같은 base에서 땄는데도, 그 사이 다른 브랜치를 ToT로 리베이스했다가 돌아오면서 6일치 파일 수천 개의 mtime이 바뀌어 50,579스텝 풀빌드(4코어 16시간) 가 됐습니다. 여러 브랜치를 오갈 땐 전부 같은 base에 두거나 풀빌드를 각오해야 합니다.
배운 점
- 이슈 발굴은 트래커에서만 하는 게 아닙니다.
grep "TODO(crbug.com/"로 코드에서 위로 올라가면, 팀이 이미 하겠다고 적어둔 일이 나옵니다. 클레임이 필요 없고 "왜 하냐"는 반박도 없습니다. - 한 이슈 = 한 CL이 아닙니다. TODO 세 개가 한 이슈에 걸려 있으면 시리즈로 쪼개고, 첫 CL은 가장 기계적인 것부터.
Fixed:는 마지막 CL에만. - "단순 이동"처럼 보이는 일도 컨벤션이 다르면 변환 작업입니다. 옮기기 전에 목적지의 관용구(여기선
$N+ReplaceStringPlaceholders)와 컴파일러 경고 규칙을 먼저 확인해야 합니다. - 범위 결정은 TODO 문구를 기준으로. 경고 메시지까지 옮기면 더 깔끔해 보이지만, 요청받지 않은 변경은 리뷰 부담만 늘립니다. 필요하면 리뷰어가 요청할 수 있게 커밋 메시지에 남겨두는 편이 낫습니다.
- 헤더를 새로 include하면 GN 의존성도 따라와야 합니다. 컴파일이 되는 것과 GN이 허용하는 것은 다릅니다. 특히 목적지 타깃이 상위 타깃에 private
deps로만 물려 있으면 헤더 사용 권한이 전파되지 않습니다. 새 컴포넌트의 헤더를 처음 include할 때는 그 헤더가 어느 GN 타깃 소속인지, 내 타깃이 그 타깃에 직접 의존하는지 먼저 확인하는 편이 CQ 왕복 한 번을 아낍니다. - 승인을 받은 뒤 코드를 고치면 표는 사라집니다. CQ 실패를 고쳐 새 패치셋을 올리자 받아둔
Code-Review+1세 개가 전부 outdated로 제거됐습니다. 고칠 게 확실할 때는 승인을 받기 전에 올리는 편이 낫고, 이미 받았다면 재승인이 필요하다는 것을 염두에 둬야 합니다. - payments 컴포넌트 초기 리뷰는 개인이 아니라
chrome-payments-reviews@google.com알리아스로 요청하는 것이 관례입니다(직전 CL에서 배운 것을 적용).