Transformers.js에서 제안된 Cross-Origin Storage API 실험하기
요약
Transformers.js를 사용하여 브라우저 환경에서 AI 모델을 실행할 때 발생하는 Cross-Origin 저장소 문제를 다룹니다. 서로 다른 오리진의 앱이 동일한 모델이나 Wasm 런타임을 사용할 경우, 브라우저가 이를 재다운로드하여 저장 공간과 네트워크를 낭비하는 현상을 실험을 통해 보여줍니다.
핵심 포인트
- Transformers.js는 브라우저에서 모델 리소스와 Wasm 파일을 자동으로 캐싱함
- Cross-Origin 환경에서는 동일한 모델이라도 오리진이 다르면 중복 다운로드 발생
- Wasm 런타임(ONNX Runtime) 또한 오리진별로 별도 다운로드되어 자원 낭비 초래
- 효율적인 웹 AI 구현을 위해 Cross-Origin Storage API 활용 가능성 시사
Transformers.js는 웹 개발자들에게 작업별 파이프라인 (pipelines)을 통해 웹 앱에서 트랜스포머 (transformers)의 강력한 기능을 사용할 수 있는 간단한 방법을 제공합니다. 브라우저에서 추론 (inference)을 실행하기 위해, 개발자는 pipeline() 인스턴스를 생성하고
파이프라인을 사용하고자 하는 작업을 지정합니다. 구체적인 예로, 다음 코드 스니펫은 자동 음성 인식 (ASR) 파이프라인을 설정하는 방법을 보여줍니다.
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';
const asr = await pipeline(
'automatic-speech-recognition',
...
소스 코드에서 제가 모델로 Xenova/whisper-tiny.en을
지정한 것을 확인할 수 있는데, 이는 일반적인 영어 자동 음성 인식 (ASR) 작업에 매우 괜찮은 선택입니다. 사실, 링크된 발췌문에 따르면 Transformers.js의 기본 모델 결정 방식에 따라 이것은 심지어 기본 모델이기도 합니다.
이 예제를 브라우저에서 실행하면, Transformers.js는 관련 모델 리소스와 Wasm 파일을 자동으로 다운로드하고 캐싱 (caching)합니다. 다음 스크린샷은 앱을 방문한 후의 Chrome DevTools Cache storage 섹션을 보여줍니다. 페이지를 새로고침하면 리소스가 Cache API로부터 제공되며, 모델이 거의 즉각적으로 결과를 반환합니다.
하지만 Xenova/whisper-tiny.en은
인기 있는 모델이기 때문에 (그리고 앞서 언급했듯이 Transformers.js의 기본 ASR 모델이기도 하므로), 여러분이 방문하는 앱 중 하나 이상이 이를 사용할 것이라고 충분히 상상할 수 있습니다. 이 상황을 시뮬레이션하기 위해, 이전과 동일한 예제 앱을 준비했지만 다른 오리진 (origin)에서 제공되도록 했습니다. 이 다른 오리진의 앱을 방문하면, 거의 즉시 사용할 수 있는 대신 브라우저는 모델 리소스가 이전과 바이트 단위로 완전히 동일하더라도 모든 모델 리소스를 다시 다운로드하고 캐싱해야 합니다. 이 간단한 예제에서도 Chrome DevTools Application 패널의 Storage 섹션에서 확인할 수 있듯이, 177 MB의 중복 다운로드 및 저장 공간이 추가됩니다. 이것이 얼마나 빠르게 누적될지는 충분히 짐작할 수 있습니다.
하지만 상황은 더 악화됩니다. 이 예제에 두 번째 파이프라인(pipeline)인 감성 분석(sentiment analysis)을 추가해 보겠습니다. 감성 분석은 기본적으로 Xenova/distilbert-base-uncased-finetuned-sst-2-english 모델을 사용합니다.
모델을 명시하지 않으면, Transformers.js의 기본 모델 해석(model resolution) 기능이 자동으로 해당 모델을 선택합니다.
const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
pre.append('\n\n' + JSON.stringify(sentiment, null, 2));
완전히 다른 두 개의 AI 모델이지만, 이들은 Transformers.js가 구축된 기반인 ONNX Runtime 라이브러리의 4,733 kB ort-wasm-simd-threaded.asyncify.wasm WebAssembly (Wasm) 런타임(runtime) 파일에 의존합니다. 확장된 데모를 다른 오리진(origin)에서 열어보면, 네트워크(Network) 탭에서 Wasm 런타임 또한 다시 다운로드되고 캐시되는 것을 확인할 수 있습니다.
따라서 동일한 AI 모델을 공유하지 않는 앱을 실행하더라도, 브라우저는 이미 보유하고 있는 공유 Wasm 리소스에 대해 여전히 중복 요청을 보내며, 게다가 이를 다시 캐시하여 하드 디스크 공간을 소모하게 됩니다.
기본적으로 **AI 모델 리소스(AI model resources)**는 Hugging Face Hub, 궁극적으로는 Hugging Face CDN에서 가져옵니다. 브라우저는 https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json와 같은 리소스에 대해 요청을 보내며, 이 요청은 이 경우 https://huggingface.co/api/resolve-cache/models/Xenova/distilbert-base-uncased-finetuned-sst-2-english/0b6928efcb76139cae2c6881d49cda67fe119f42/config.json?%2FXenova%2Fdistilbert-base-uncased-finetuned-sst-2-english%2Fresolve%2Fmain%2Fconfig.json=&etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22와 같은 최종 CDN URL로 리다이렉트(redirect)됩니다.
**Wasm 런타임 리소스(Wasm runtime resources)**는 기본적으로 jsDelivr CDN을 통해 제공됩니다. 예를 들어, 이 글을 쓰는 시점을 기준으로 ort-wasm-simd-threaded.asyncify.wasm은 https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm에서 가져옵니다.
이제 여러분은 서로 다른 오리진 (Origin)에서 실행되더라도, 결국 동일한 CDN URL에서 리소스를 제공한다면 최종 URL이 같기 때문에 캐싱(Caching)에 문제가 없을 것이라고 말할 수도 있습니다. 불행히도, 브라우저의 캐싱 방식은 오랫동안 그렇게 작동하지 않았습니다. "캐시 파티셔닝을 통한 보안 및 개인정보 보호 강화 (Gaining security and privacy by partitioning the cache)"라는 기사에서 모든 세부 사항을 다루고 있지만, 본질적으로 캐시는 타이밍 공격 (Timing attacks)을 방지하기 위해 오리진별로 격리되어 있습니다. 웹사이트가 HTTP 요청에 응답하는 데 걸리는 시간은 브라우저가 과거에 동일한 리소스에 접근했음을 드러낼 수 있으며, 이는 브라우저를 보안 및 개인정보 유출에 취약하게 만듭니다.
구체적인 구현은 브라우저마다 다를 수 있지만, Chrome에서는 캐시된 리소스가 리소스 URL (Resource URL) 외에도 네트워크 격리 키 (Network Isolation Key)를 사용하여 키(Key)가 지정됩니다. 네트워크 격리 키는 **최상위 사이트 (Top-level site)**와 **현재 프레임 사이트 (Current-frame site)**로 구성됩니다. https://googlechrome.github.io와 https://rawcdn.rawgit.net 오리진에서 호스팅되는 이전의 간단한 예제들을 살펴보겠습니다. 만약 두 예제 모두 https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm에서 Wasm 런타임을 사용한다면, 그들의 캐시 키는 다음 표와 같이 나타날 것입니다.
| 네트워크 격리 키 (Network Isolation Key) | 리소스 URL (Resource URL) |
|---|---|
| 최상위 사이트 (Top-level site) | 현재 프레임 사이트 (Current-frame site) |
| https://googlechrome.github.io | https://googlechrome.github.io |
| https://rawcdn.rawgit.net | https://rawcdn.rawgit.net |
따라서 리소스 URL이 정확히 동일하더라도 네트워크 격리 키가 일치하지 않기 때문에 캐시 히트 (Cache hit)가 발생하지 않으며, 이는 중복 다운로드와 중복 저장으로 이어집니다. 이것이 바로 Cross-Origin Storage 제안이 해결하고자 하는 과제입니다.
💡 참고: Cross-Origin Storage API는 아직 확정되지 않은 초기 단계의 제안입니다. 제안된 API가 아직 어떤 브라우저에도 네이티브로 구현되어 있지는 않지만, 실험을 위해 기다릴 필요는 없습니다. Cross-Origin Storage 확장 프로그램을 설치하면 모든 페이지에 navigator.crossOriginStorage 폴리필 (polyfill)을 주입하여 전체 흐름을 테스트할 수 있습니다.
제안된 Cross-Origin Storage (COS) API는 웹 앱이 URL이 아닌 암호화 해시 (cryptographic hash)로 식별되는 대용량 파일을 오리진 경계를 넘어 저장하고 검색할 수 있도록 하는 전용 navigator.crossOriginStorage 인터페이스를 도입합니다.

암호화 해시에 관한 마지막 포인트가 핵심입니다. COS는 파일의 URL이나 오리진이 아닌 **해시 (hash)**를 통해 파일을 식별하기 때문에, https://googlechrome.github.io를 방문하는 동안 다운로드한 동일한 ort-wasm-simd-threaded.asyncify.wasm Wasm 런타임은, 두 오리진 중 어느 곳에서 가져왔는지에 관계없이 https://rawcdn.rawgit.net이 요청하려는 파일과 동일한 것으로 인식됩니다. 기본적인 흐름을 보여주는 다음 코드 스니펫을 참조하세요.
const hash = {
algorithm: 'SHA-256',
value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
...
리소스가 COS에 있다면, getFile()을 통해 직접 블롭 (blob)을 읽을 수 있는 FileSystemFileHandle을 반환받습니다 (결과물인 File은 Blob을 상속받습니다). 리소스가 COS에 없다면 네트워크로 폴백 (fallback)하며, 해당 리소스를 COS에 기록하여 다음에 이를 필요로 하는 앱(본인의 앱일 수도 있고, 완전히 다른 오리진에 있는 관련 없는 다른 앱일 수도 있음)이 사용할 수 있도록 합니다.
이 API는 여러분이 Origin Private File System (OPFS) API를 통해 익숙할 법한 File System 표준의 FileSystemDirectoryHandle.getFileHandle()과 의도적으로 유사한 형태로 설계되었습니다. hash 파라미터는 OPFS의 name 파라미터와 동일한 역할, 즉 리소스를 고유하게 식별하는 역할을 수행합니다. options.create 플래그도 동일하게 작동합니다: 읽기 전용 액세스의 경우 생략하거나 false로 설정하며, true...
쓰고자 할 때 사용합니다.
모든 리소스가 전역적으로 공유되어야 하는 것은 아닙니다. COS는 파일을 저장할 때 origins 옵션을 통해 개발자에게 가시성(visibility)에 대한 정밀한 제어권을 제공합니다.
origins: '*'를 설정하면 파일을 **전역적으로 사용 가능(globally available)**하게 만듭니다. 어떤 오리진(origin)이든 해시(hash)를 통해 파일을 찾을 수 있습니다. 이는 Transformers.js 예제의 AI 모델 리소스나 Wasm 런타임에 적합한 선택입니다. 즉, 웹상의 모든 앱이 단일 캐시된 복사본의 혜택을 받는 것이 핵심이기 때문입니다.origins: ['https://write.example.com', 'https://calculate.example.com']와 같이 특정 오리진 목록을 전달하면, 해당 사이트로 액세스를 **제한(restricts)**합니다. 이는 상용 오피스 제품군에서 사용되는 독점적인 교정 AI 모델처럼, 다른 누구에게도 발견되어서는 안 되며 기업 소유의 자산 간에 공유되는 독점 리소스에 적합합니다.origins를 완전히 생략하면 파일은 **동일 사이트 오리진(same-site origins)**에만 사용 가능해집니다. 이는 조직의 모든 서브도메인 간에 공유되는 리소스에는 합리적인 기본값이지만, 조직의 경계를 넘어서도록 의도된 것은 아닙니다.
한 가지 중요한 규칙이 있습니다: 가시성은 업그레이드될 수는 있지만, 절대 다운그레이드될 수 없습니다. 만약 파일이 이미 전역적으로 사용 가능하다면, 나중에 제한된 origins 목록으로 해당 파일을 저장하려는 시도는 조용히 무시됩니다. 이는 악의적인 행위자가 공개된 리소스를 다시 저장하여 가용 범위를 좁히는 것을 방지합니다. 반대의 경우는 가능합니다: 처음에 제한된 origins 목록으로 저장된 파일은 나중에 더 허용적인 상태로 만들 수 있습니다. 원래 저장한 사이트뿐만 아니라 어떤 사이트든 동일한 해시(해시는 비밀이 아닙니다)에 대해 create: true 및 더 넓은 origins 값을 사용하여 requestFileHandle()을 호출할 수 있으며, 브라우저가 해시가 일치하는지 확인하면 그 시점부터 해당 리소스는 더 넓은 사용자층에서 사용할 수 있게 됩니다. 단, 업그레이드하는 사이트는 반환된 핸들(handle)을 통해 반드시 전체 파일을 다시 써야(write) 한다는 점에 유의하세요. 이 요구 사항은 사이트들이 업그레이드 경로를 사이드 채널(side-channel)로 악용하여 특정 파일이 이미 COS에 저장되어 있는지 탐지하는 것을 방지하기 위해 존재합니다.
COS의 미묘하지만 중요한 속성 중 하나는 파일을 쓸 때 브라우저가 **해시를 검증(verifies the hash)**한다는 점입니다. 만약 작성하려는 데이터가 선언된 해시와 일치하지 않으면, 쓰기 작업은 오류와 함께 실패합니다. 이는 무결성 검사(integrity checking)를 자동으로 만들어 줍니다. 즉, COS에서 파일을 읽는 앱은 자신이 예상한 정확한 바이트를 가져오고 있다는 확신을 가질 수 있습니다. 이는 네트워크 다운로드 후에 직접 해시를 계산했을 때 얻을 수 있는 것과 동일한 보장입니다.
이러한 특성은 Transformers.js 시나리오에서 두 배로 유용하게 작용합니다. 현재는 모델 가중치(model weights)를 다운로드한 후, 대부분의 앱이 CDN이 올바른 바이트를 제공했는지 확인할 실질적인 방법이 없습니다. COS를 사용하면, 공식 Hugging Face CDN이든 임의의 사이트에서 호스팅하는 미러(mirror) 사이트든 상관없이, 저장소의 모든 파일은 쓰기 시점에 암묵적으로 검증됩니다.
물론 교차 출처 공유 캐시(cross-origin shared cache)는 분할된 HTTP 캐시(partitioned HTTP cache)와 반대되는 동일한 문제를 제기합니다. 만약 어떤 사이트든 해시를 통해 파일의 존재 여부를 탐색할 수 있다면, 공격자가 예를 들어 게임 엔진의 Wasm 모듈이 캐시되어 있는지 확인함으로써 사용자의 브라우징 기록에 대해 무언가를 알아낼 수 있지 않을까요?
COS는 두 가지 상호 보완적인 메커니즘을 통해 이 문제를 해결합니다:
- 첫째,
origins필드입니다. 단순히 전역적으로 탐색되어서는 안 되는 독점적 리소스(proprietary resources)는origins: '*'와 함께 저장해서는 안 됩니다.
, 이는 **개발자 교육 (developer education)**을 통해, 개발자들이 합리적이라고 판단되는 경우 언제든 고려하도록 권장되는 사항입니다. - 둘째, 가용성 게이팅 (availability gating): 전역적으로 선언된 파일이라 할지라도, 브라우저는 충분히 많은 수의 서로 다른 오리진 (origins)에서 해당 파일이 발견되지 않았다면 파일의 존재 확인을 억제할 수 있습니다. 단 한두 개의 사이트에서만 나타나는 파일은 여전히 교차 사이트 식별자 (cross-site identifier)로 작용할 수 있으므로, 브라우저는 디스크에 실제로 파일이 있는지 여부와 관계없이 마치 파일이 아예 없는 것처럼 에러를 반환할 수 있습니다. Chrome 팀은 흔치 않은 리소스가 유발할 수 있는 잠재적인 개인정보 유출 가능성을 인지하고 있으며, 정확히 어떤 리소스를 캐싱할 수 있는지 제한함으로써 이를 완화할 계획을 세우고 있습니다. 구체적인 완화 방안은 아직 구체화되는 과정에 있습니다.
결정적으로, 이는 에러가 확정적인 답변이 아님을 의미합니다. 이는 "저장되지 않음"을 의미할 수도 있고, "저장되었지만 브라우저가 알려주지 않음"을 의미할 수도 있습니다. 앱은 항상 동일한 방식으로 이를 처리해야 합니다: 즉, 네트워크로 폴백 (fall back)해야 합니다.
이전의 예시들로 돌아가 보겠습니다: ort-wasm-simd-threaded.asyncify.wasm
AI 자동 생성 콘텐츠
본 콘텐츠는 Hugging Face Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기