Safari의 JavaScript 엔진에는 숨겨진 결함이 있습니다. 모듈 방식 Web Worker의 엔트리 스크립트가 번들 내 다른 곳에서 임포트되면, Safari는 해당 엔트리 스크립트를 두 번 실행합니다. 이 중복 실행은 싱글톤 상태를 깨뜨려, 공유 캐시나 단일 인스턴스 객체에 의존하는 워커를 조용히 망가뜨립니다.

이 문제는 Web Worker를 사용하여 ProRes 파일을 디코딩하는 브라우저 기반 비디오 처리 앱을 개발하던 중 발견되었습니다. Chrome과 Firefox는 문제없이 코드를 처리했지만, Safari에서는 비디오 로딩이 지속적으로 실패했습니다. 콘솔에는 단순히 “cannot read video”라는 일반적인 오류만 표시되었지만, 실제 원인은 워커의 초기화 코드가 두 번 실행되어 동일한 모듈의 독립적인 복사본 두 개가 메모리에 남았기 때문이었습니다.

버그가 발생하는 방식

현대적인 번들러(Vite, Rollup 등)는 지연 로딩되는 청크(lazy-loaded chunks)가 엔트리 포인트로부터 해당 코드를 다시 임포트할 수 있도록 공유 유틸리티를 워커의 엔트리 파일로 가져오는 경우가 많습니다. 표준 모듈 로더 동작을 따르는 브라우저에서는 엔트리 모듈이 인스턴스화되면 로더가 이후의 모든 임포트에 대해 동일한 모듈 객체를 반환하여 두 번째 실행을 방지합니다.

Safari는 이 기대와 다르게 동작합니다. 지연 로딩되는 청크가 워커의 엔트리 파일을 임포트하면, Safari는 해당 임포트를 새로운 모듈 요청으로 취급하고 엔트리 스크립트를 재실행합니다. 그 결과, 그곳에 정의된 모든 변수, 클래스 또는 싱글톤의 별개 인스턴스가 두 개씩 생성됩니다.

엔트리가 두 번 실행될 때 발생하는 문제

  • 싱글톤 및 캐시가 더 이상 데이터를 공유하지 않습니다. 한 복사본은 빈 캐시를 보고 다른 복사본은 캐시를 채웁니다.
  • 레지스트리(예: 메시지 핸들러 목록)가 두 인스턴스 사이에 분산되어 한쪽은 사실상 비어 있게 됩니다.
  • 이벤트 리스너가 두 번 연결되어 중복 처리나 메모리 팽창을 초래할 수 있습니다.
  • WebAssembly (WASM) 모듈이 두 번 로드되어 대역폭과 초기화 시간을 낭비합니다.
  • 오류가 조용히 발생합니다. 예외(exception)가 던져지지 않고, 누락된 상태에 의존하는 하위 로직이 오작동할 뿐입니다.

프로젝트에서 문제 탐지하기

빌드된 에셋에 대해 간단히 grep을 실행하면 어떤 청크가 워커의 엔트리 파일을 임포트하는지 확인할 수 있습니다.

grep -l 'from"./your.worker-' dist/assets/*.js

명령어 결과에 파일이 나열된다면, 해당 임포트들이 Safari에서 이 중복 실행 버그를 유발하고 있을 가능성이 높습니다.

실질적인 해결 방법

  1. 워커 엔트리에서 공유 코드를 추출합니다.
    번들러가 공통 라이브러리를 별도의 청크로 배치하도록 설정합니다(예: Rollup의 manualChunks 사용). 그러면 워커와 지연 로딩되는 모든 모듈이 해당 제3의 파일에서 라이브러리를 임포트하게 되어, 워커의 엔트리 포인트를 임포트할 필요가 없어집니다.

  2. 얇은(thin) 엔트리 파일을 사용합니다.
    워커의 엔트리 스크립트를 실제 구현을 다시 내보내는(re-export) 단 한 줄로 줄입니다.

    // worker-entry.js
    import("./main.js");
    

    다른 번들에서 worker-entry.js를 임포트하지 않는 한, Safari는 두 번째 임포트 요청을 받지 않으므로 엔트리는 한 번만 실행됩니다.

두 방식 모두 애플리케이션 전체에서 워커의 초기화 코드가 싱글톤 상태를 유지하도록 합니다.

이 버그가 중요한 이유

Web Worker는 비디오 인코딩, 이미지 처리, 암호화와 같은 무거운 연산을 메인 스레드에서 분리하기 위한 일반적인 패턴입니다. 조용한 상태 분리는 완벽하게 작동하던 기능을, 데스크톱과 모바일 기기의 상당 부분에서 기본 브라우저로 사용되는 Safari에서만 간헐적으로 발생하는 실패로 바꿀 수 있습니다. 오류가 일반적인 미디어 로드 실패로 나타나기 때문에, 개발자는 잘못된 증상을 쫓느라 몇 시간을 허비할 수 있습니다.

이 버그는 더 넓은 위험성을 시사합니다. 즉, 브라우저마다 균일하게 구현되지 않은 모듈 로더 시맨틱(semantics)에 의존하는 위험입니다. 번들러의 최적화 전략이 단일 공유 모듈 인스턴스를 가정할 때, 어떠한 편차라도 그 가정을 깨뜨릴 수 있습니다.

반론 및 열린 질문

Safari의 동작은 워커와 관련된 엣지 케이스에서 스펙과 미묘하게 다른 자체 모듈 해석 규칙을 따릅니다. 일부 개발자들은 번들러가 워커의 엔트리 파일에 공유 코드를 배치하는 것을 아예 피해야 한다고 주장하며, 이 문제를 브라우저 결함이 아닌 빌드 타임의 규율 문제로 보기도 합니다. 다른 이들은 Safari의 이러한 편차가 문서화되어 있지 않아 개발자가 이를 예측할 수 있는 신뢰할 수 있는 방법이 없다는 점을 지적합니다.

Apple은 이 문제를 공개적으로 인정하지 않았으며, 수정 일정에 대해서도 알려진 바가 없습니다. Safari의 로더가 변경될 때까지, 번들을 재구성하거나 CI 파이프라인에 탐지 로직을 추가하는 책임은 개발자에게 남아 있습니다.

다음에 주목할 점

  • 브라우저 업데이트 – module-worker 처리와 관련된 언급이 있는지 Safari 릴리스 노트를 주의 깊게 살펴보세요.
  • 번들러 커뮤니티 패치 – Vite, Rollup 및 기타 도구에서 버그를 유발하는 패턴을 피하기 위해 경고를 도입하거나 자동 청킹(chunking) 전략을 도입할 수 있습니다.
  • 테스트 방식 – 출시 전 Safari에서 실제 미디어 파일과 풀스택 워커 테스트를 포함하면 이러한 조용한 실패(silent failure)를 조기에 발견할 수 있습니다.

요약

Safari 사용자가 설명할 수 없는 워커 관련 오류를 경험한다면, 워커가 아닌 번들이 워커의 엔트리 스크립트를 임포트하고 있는지 확인하십시오. 이 이중 실행 버그는 싱글톤 상태(singleton state)를 소리 없이 파괴하지만, 공유 코드를 엔트리 포인트에서 분리하거나 엔트리를 얇은 재내보내기(re-export) 형태로 축소하면 브라우저 수정이 나올 때까지 기다리지 않고도 올바른 동작을 복구할 수 있습니다.