모든 글
뉴스 해석2026-09-147 min read

Safari의 top-level await 버그는 당신 코드 탓이 아니다

TLA 모듈을 import()로 연속 호출하면 완료 순서가 뒤집히고 'Cannot access X before initialization'이 난다. 엔진 버그다. AI가 구조적으로 못 잡는 종류의 버그이기도 하다.

Safari에서 top-level await를 쓴 모듈을 import()로 여러 번 부르면 완료 순서가 뒤집힌다. 1, 2, 3 순서로 부르면 2, 3, 1로 끝난다. 그 결과 나오는 에러는 이렇게 생겼다.

Cannot access 'someArray' before initialization

이 메시지를 보면 누구나 자기 코드의 TDZ 문제를 의심한다. 아니다. 엔진 버그다. WebKit이 모듈 로더를 통째로 다시 써서 고쳤고, Safari 27에 들어간다.

확인된 것

WebKit 블로그 기준이다.

  • 증상: TLA가 포함된 같은 모듈을 import()로 연속 호출하면, 모듈 평가가 끝나기 전에 promise가 먼저 resolve된다. 원문 표현으로 "due to a bug in the old module loader, it resolves immediately".
  • 그래서 아직 초기화되지 않은 export에 접근하게 되고, 위의 에러가 난다.
  • 수정본은 Safari 27에 포함된다. 지금은 Safari 27 beta와 Safari Technology Preview 251에서 확인할 수 있다.
  • 기존 모듈 로더는 JavaScriptCore 안에 self-hosted JavaScript로 구현돼 있었다. 이번에 네이티브 C++로 옮겼다.
  • 옮긴 이유: "it's slower to start up because it has to be compiled at run time, unlike native code, which is fully compiled well in advance... overall performance is less stable and predictable than it is for C++".
  • 방식: 기존 JS 파일을 번역한 게 아니라 ECMAScript 명세에서 다시 구현했다. 의존성 없는 leaf부터 올라가는 bottom-up이었고 — "Leaf functions like ExecuteModule and ModuleRequestsEqual were ideal to implement early because they didn't depend on any other functions" — 검증은 퍼저로 했다. "we wrote a fuzzer that produced complex graphs of modules... comparing the output of JavaScriptCore with the new module loader to the output of other JavaScript engines".
  • 성능 수치는 공개되지 않았다. 안정성 개선에 초점을 둔 글이다.

내 판단

1. 이건 AI 코딩 도구가 구조적으로 못 잡는 종류의 버그다.

에러 메시지가 Cannot access 'X' before initialization이다. 이 문자열을 모델에 던지면 거의 확실하게 TDZ 설명이 돌아오고, 선언 순서를 바꾸거나 let을 var로 바꾸라는 제안이 따라온다. 그 제안대로 고치면 증상이 옮겨다니기만 한다. 원인이 내 코드에 없기 때문이다.

이건 추측이지만 근거는 있다. 모델은 "이 에러 메시지에 가장 흔히 이어지는 수정"을 내놓는데, 플랫폼 버그는 정의상 흔하지 않다. 게다가 이 수정은 방금 공개됐다. 어떤 모델의 학습 데이터에도 아직 들어 있지 않다.

실무 규칙 하나: 특정 브라우저에서만 재현되는 버그는 AI에게 묻기 전에 브라우저를 바꿔서 한 번 돌려본다. 한 엔진에서만 나면 그때부터는 내 코드를 보는 게 아니라 그 엔진의 이슈 트래커를 봐야 한다. 이 한 단계를 건너뛰면 모델과 함께 몇 시간을 엉뚱한 데 쓴다.

2. 자주 안 도는 코드일수록 최적화기에 맡기면 안 된다.

C++로 옮긴 이유가 흥미롭다. 모듈 로더는 hot path가 아니다. 그런데 hot path가 아니라는 바로 그 이유로 JIT 최적화가 제대로 걸리지 않고, 성능이 예측 불가능해진다. 자주 도는 코드는 런타임이 알아서 잘 만들어주지만, 한 번만 도는 초기화 경로는 아무도 돌봐주지 않는다.

내 코드에 그대로 옮겨진다. 부팅·설정 로드·마이그레이션처럼 한 번만 도는 경로가 느릴 때, 프로파일러는 별 도움이 안 되고 런타임도 도와주지 않는다. 그 구간은 처음부터 단순하고 예측 가능하게 짜는 수밖에 없다.

3. 레거시를 걷어낼 때 참조할 만한 방식이다.

기존 구현을 보고 옮긴 게 아니라, 명세를 펴놓고 leaf부터 다시 만들었다. 그리고 정답지를 사람이 쓰는 대신 퍼저로 복잡한 모듈 그래프를 생성해 다른 엔진의 출력과 비교했다.

소규모 팀에 그대로 쓸 수 있는 형태다. 재작성의 진짜 위험은 새 코드가 틀리는 게 아니라 어디가 틀렸는지 모르는 것인데, 기존 구현을 참조하면 기존 버그까지 같이 옮긴다. 대신 스펙이든 경쟁 구현이든 외부 기준을 정답지로 두고 입력을 자동 생성하면, 사람이 상상하지 못한 조합이 걸린다. 스펙이 있는 영역(파서, 직렬화, 프로토콜, 날짜·통화 처리)이라면 지금 당장 쓸 수 있는 방법이다.

모르는 것

  • Safari 정확히 몇 버전부터 이 버그가 있었는지는 글에 없다. "old module loader"라고만 쓰여 있다. 내 프로젝트가 영향을 받는지는 Safari 27 beta나 STP 251에서 직접 재현해보는 게 빠르다.
  • 성능이 실제로 얼마나 개선됐는지 수치가 없다. C++ 전환 근거는 설명됐지만 결과는 제시되지 않았다.
  • 사용자들의 Safari가 27로 올라가는 데 얼마나 걸릴지는 별개 문제다. 버그가 고쳐지는 것과 내 사용자 브라우저에서 사라지는 것은 다른 날짜다.

댓글

불러오는 중...