Cross-Origin Storage API: 파일 중복 다운로드 방지하기
요약
브라우저의 HTTP 캐시 파티셔닝으로 인해 무력화된 공용 CDN의 효율성을 복구하기 위한 Cross-Origin Storage (COS) API를 소개합니다. 이 API는 URL 대신 콘텐츠의 암호화 해시를 사용하여 보안을 유지하면서도 대용량 파일을 여러 사이트 간에 공유할 수 있게 합니다.
핵심 포인트
- HTTP 캐시 파티셔닝으로 인한 공용 CDN 재사용성 저하 문제 해결
- URL 대신 SHA-256 콘텐츠 해시를 사용하여 추적 위험 차단
- navigator.crossOriginStorage 인터페이스를 통한 파일 핸들 요청
- React, 웹 폰트, WebAssembly 등 대용량 리소스의 효율적 공유 가능
공용 CDN에서 React를 로드하는 것이 진정한 성능 최적화 기법이었던 시절이 있었습니다. 충분히 많은 사이트가 동일한 CDN URL에서 동일한 react.production.min.js를 가져온다면, 귀하의 사이트를 처음 방문한 사용자는 이미 다른 사이트에서 방문했을 때 해당 파일을 캐시(cache)해 두었을 가능성이 높았습니다. 한 번의 다운로드로 어디에서나 재사용할 수 있었던 것이죠. 그것이 공유 CDN(shared CDNs)이 내세웠던 핵심 가치였습니다.
브라우저들은 이를 의도적으로 제거했습니다. 사이트들이 캐시를 교차 사이트 추적 신호 (cross-site tracking signal)로 사용하는 것을 막기 위해, HTTP 캐시를 최상위 사이트(top-level site)별로 파티셔닝(partitioning)하기 시작했습니다. 그들이 차단하려 했던 공격은 다음과 같습니다: 공유 리소스가 캐시에서 로드되는 시간을 측정함으로써 사용자가 다른 사이트를 방문했는지 여부를 감지할 수 있다는 점입니다. Chrome은 2020년 10월 버전 86에서, Firefox는 85에서, Safari는 그보다 훨씬 이전에 이를 파티셔닝했습니다. 이제 동일한 URL의 동일한 파일이라도 이를 사용하는 모든 사이트에 대해 각각 별도로 다운로드되고 저장되며, 과거의 "그냥 공용 CDN을 사용하라"는 조언은 대부분 의미가 없어졌습니다. Cross-Origin Storage (COS) API는 이러한 공유 캐시를 다시 가져오기 위한 WICG 제안입니다.
이 API의 핵심 기술은 공유 캐시의 키(key)를 결정하는 방식을 바꾸는 것입니다. 정보가 유출될 수 있었던 URL 대신, COS는 파일의 암호화 해시(cryptographic hash)를 기준으로 키를 지정하며, 사용자가 이미 정확한 바이트(bytes) 값을 알고 있는 파일에 대해서만 요청할 수 있도록 허용합니다. 이를 통해 기존 공유 캐시가 만들어냈던 추적 취약점을 차단합니다. React 한 카피, 혹은 웹 폰트, 또는 커다란 WebAssembly 바이너리가 이를 원하는 모든 오리진(origin) 간에 공유될 수 있게 됩니다.
아직 초기 단계입니다. 아직 네이티브로 구현되어 배포된 것은 없습니다. 하지만 이 API가 해결하려는 문제는 실재하며 점점 악화되고 있으므로, 지금 이해해 둘 가치가 있습니다.
Cross-Origin Storage API란 무엇인가?
Cross-Origin Storage는 URL 대신 **콘텐츠 해시 (content hash)**를 사용하여 대용량 파일을 저장하고 검색하기 위해 제안된 브라우저 API입니다. 이 API는 사용자의 기기를 공유 캐시로 활용하여 여러 오리진 (origin) 간에 공유할 수 있는 저장소에 파일을 보관합니다. 파일은 SHA-256 다이제스트 (digest)로 식별되므로, 서로 다른 두 사이트가 서로 다른 URL을 통해 가져온 동일한 바이트 데이터는 디스크 상의 단일 항목으로 매핑됩니다.
전체 인터페이스는 새로운 navigator.crossOriginStorage 인터페이스의 단일 메서드로 구성됩니다:
requestFileHandle(hash, options)
이 메서드는 File System API와 동일한 핸들 타입인 Promise<FileSystemFileHandle>을 반환하므로, 일단 핸들을 얻고 나면 이미 알고 있는 방식 그대로 파일을 읽을 수 있습니다. 별도로 배워야 하는 전용 store()/retrieve() 쌍은 없습니다. requestFileHandle()이 양방향을 모두 처리하며, create 플래그를 통해 결정됩니다.
이 제안은 Google의 Thomas Steiner와 François Beaufort, 그리고 Thinktecture의 Christian Liebel이 공동으로 제안하였으며, 현재 아직 인큐베이션 (incubation) 단계에 있습니다. 동기 부여가 된 사례들은 이미 많은 사이트가 바이트 단위로 공유하고 있는 파일들입니다: JavaScript 라이브러리, 웹 폰트 (web fonts), WebAssembly 모듈, 게임 엔진 등입니다. Google Fonts에서 제공하는 폰트는 수천 개의 사이트에서 요청되는데, 이를 한 번만 저장하면 각 사이트가 방문할 때마다 CDN에서 매번 가져오는 대신 로컬에서 읽을 수 있습니다.
AI 모델의 경우 이 문제는 극단적으로 나타납니다. 설명서의 주요 예시는 두 오리진이 모두 필요로 하는 단일 8GB 모델인데, COS가 없다면 16GB를 다운로드해야 하고 하나의 파일에 대해 16GB가 디스크를 차지하게 됩니다. 용량 목록을 더 살펴보면 Gemma 2는 1.35GB, Llama-3.1-70B는 33GB에 달합니다. 이 그룹이 가장 큰 고통을 느끼고 있지만, 작동 메커니즘은 두 사이트가 React의 복사본을 공유할 수 있게 해주는 것과 동일합니다.
왜 Cache API나 IndexedDB는 오리진 간에 파일을 공유할 수 없는가?
기존의 모든 저장 메커니즘은 오리진 (Origin)별로 파티셔닝 (Partitioned)되어 있으며, 그중 모든 메커니즘이 콘텐츠 (Content)가 아닌 URL이나 키 (Key)에 의해 주소가 지정되기 때문입니다. 이 두 가지 사실이 문제의 핵심입니다.
| 저장소 | 주소 지정 방식 | 오리진 간 공유 가능 여부 |
|---|---|---|
| Cache API | URL | 아니요 |
| ... |
Cache API는 요청 URL (Request URL)을 기준으로 엔트리 (Entry)를 키 (Key)로 지정합니다. 따라서 cdn-a.com/react.js와 cdn-b.com/react.js에서 제공되는 동일한 React 복사본이라 할지라도, 바이트 (Bytes)가 완전히 일치하더라도 서로 관련 없는 두 개의 엔트리가 됩니다. 또한 캐시 파티셔닝 (Cache partitioning)은 심지어 동일한 URL이라 할지라도 사이트마다 별개의 엔트리로 취급됨을 의미합니다. IndexedDB와 Origin Private File System은 설계 단계부터 단일 오리진 내에 격리되어 있습니다. 이들 중 그 어떤 것도 "이것이 어디서 왔는지는 상관하지 않습니다. 저는 이것이 8f43…으로 해싱 (Hash)된다는 사실이 중요합니다"라고 말할 수 없습니다. 콘텐츠 주소 지정 (Content addressing)은 교차 오리진 (Cross-origin) 공유를 시도할 때 안전하게 만들어주는 유일한 요소인데, 왜냐하면 정확한 바이트를 이미 알고 있는 파일만을 요청할 수 있기 때문입니다.
COS는 명시적으로 이들 중 그 무엇도 대체하려는 것이 아닙니다. 이는 좁은 범위의 사례, 즉 많은 사이트가 정당하게 공유하는 대용량 파일을 위한 네 번째 수단입니다.
requestFileHandle()은 파일을 어떻게 읽고 저장하는가?
파일의 SHA-256 해시 (Hash)를 계산하고, 핸들 (Handle)을 요청한 뒤, 이를 읽습니다. 다른 오리진이 이미 저장한 파일을 가져오는 과정은 다음과 같습니다:
const hash = {
algorithm: "SHA-256",
value: "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4",
...
만약 파일이 없다면, 일반적인 방식으로 파일을 가져온(fetch) 후, create: true 옵션과 공유하고자 하는 오리진들을 전달하여 저장합니다:
const handle = await navigator.crossOriginStorage.requestFileHandle(hash, {
create: true,
origins: ["https://example.com", "https://example.org"],
...
hash 인자는 { value, algorithm } 형태의 딕셔너리이며, 여기서 value는 64자리의 소문자 16진수 다이제스트 (hex digest)입니다. SHA-256은 이 스펙이 기반으로 삼는 알고리즘입니다. 쓰기 작업 시, 브라우저는 사용자가 전달한 바이트가 주장한 value와 실제로 일치하는 해시 값을 생성하는지 검증하며, 일치하지 않을 경우 DataError를 발생시킵니다. 따라서 저장된 파일은 자신의 정체성에 대해 결코 거짓을 말할 수 없습니다. 이러한 검증 덕분에 다른 오리진 (origin)이 파일을 다시 다운로드하지 않고도 나중에 해당 항목을 신뢰할 수 있습니다.
origins 옵션은 접근 제어 (access-control) 노브 (knob) 역할을 합니다. 이 옵션을 생략하면 파일은 자신의 사이트로 범위가 제한됩니다. 명시적인 리스트를 전달하면 해당 오리진들만 파일을 볼 수 있습니다. "*"를 전달하면 웹 전체에 파일을 제공하게 되는데, 바로 이 지점에서 프라이버시 설계가 흥미로워집니다.
Cross-Origin Storage는 어떻게 슈퍼쿠키 (supercookie)가 되는 것을 방지할까요?
이 질문은 전체 설계가 공전하는 핵심 질문입니다. 왜냐하면
전역 파일에는 대중성이 필요합니다. "*" 범위(scope)를 가진 파일의 존재 여부는 해당 파일의 해시(hash)가 **공개 해시 목록 (Public Hash List)**에 등록되어 있을 때만 외부 오리진(origin)에서 확인할 수 있습니다. 이 레지스트리는 파일이 k-익명성 (k-anonymity) 스타일의 인기도 기준을 통과한 후에만 참여할 수 있는데, 이는 최소한의 독립적인 오리진들 사이에서 바이트 단위로 동일하게 나타나야 함을 의미합니다 (참조용 public-hash-list는 현재 약 100개를 기준으로 사용합니다). 만약 어떤 파일이 충분히 대중적이어서 "당신이 그 파일을 가지고 있다"는 사실이 공격자에게 당신에 대한 구체적인 정보를 전혀 제공하지 않는다면, 그 존재를 공유하는 것은 안전합니다. 반면 파일이 희귀하다면, 브라우저는 확인을 거부합니다.
브라우저는 의도적으로 거짓말을 합니다. 존재 신호(presence signal)가 민감할 수 있는 파일의 경우, 사용자 에이전트(user agent)는 GREASE 기법을 적용할 수 있습니다: "[게이팅 (gating)]이 허용하는 상황임에도 불구하고, 마치 공개 가능한 항목이 없는 것처럼 가끔 응답하는 것"을 의미하며, 이를 통해 사이트가 실제 누락과 개인정보 보호를 목적으로 한 누락을 구분할 수 없도록 노이즈를 추가합니다. 여기에는 실용적인 예외 조항이 있습니다: 브라우저는 기가바이트(GB) 단위의 대규모 가중치(weights) 파일에 대해서는 GREASE를 적용하지 않습니다. 아무도 채굴(mining)하지 않는 신호를 보호하기 위해 수 기가바이트의 가짜 재다운로드를 강제하는 것은 손해이기 때문입니다.
결과적으로 NotFoundError는 의도적으로 모호하게 설계되었습니다. 이는 파일이 저장되어 있지 않다는 뜻일 수도 있고, 범위를 벗어났다는 뜻일 수도 있으며, 해시가 공개 해시 목록 (Public Hash List)에 없다는 뜻일 수도 있고, 혹은 브라우저가 그냥 알려주지 않기로 결정했다는 뜻일 수도 있습니다. 명세(spec)에는 이 오류가 "파일이 물리적으로 존재하지 않음을 증명하지 않는다"라고 명시되어 있습니다.
저를 놀라게 했고, 많은 2차 자료의 오류를 바로잡아 준 사실이 하나 있습니다: 파일별 권한 요청 프롬프트가 없으며, 사용자에게 사람이 읽을 수 있는 형태의 해시가 표시되지도 않습니다. 이 아이디어에 대한 초기 설명들은 사용자가 읽을 수 있는 지문(fingerprint)을 통해 파일의 존재를 확인하는 대화 상자를 상상했습니다. 현재의 설계는 이를 제외했습니다. COS에서 반환된 핸들(handle)은 이미 완전히 권한을 부여받은 상태이므로, getFile() 또는 createWritable()을 호출해도 "추가적인 권한 프롬프트가 절대 발생하지 않습니다." 사용자 제어권은 매 다운로드마다 나타나는 중간 단계(interstitial)가 아니라, 저장된 파일을 검사하고 삭제(evicting)할 수 있는 설정 UI에 존재합니다.
선언적 crossoriginstorage 속성은 어떻게 작동하나요?
대부분의 사이트가 직접 스크립트 태그에 해시(hash)를 생성할 필요는 없으므로, 이 제안은 브라우저가 사용자를 대신해 COS 조회를 수행하는 선언적 경로(declarative path)도 스케치하고 있습니다. 이는 이미 Subresource Integrity (SRI)를 위해 사용하고 있을 수 있는 integrity 속성(이 해시는 정확히 COS가 필요로 하는 것입니다)과 새로운 crossoriginstorage 속성에 의존합니다:
<script
src="popular-js-framework.js"
integrity="sha256-def456..."
...
동일한 아이디어가 JavaScript 임포트 속성(import attributes)과 CSS url() 내의 cross-origin-storage() 수정자(modifier)를 위해서도 제안되었습니다. 이것들은 COS 명세(spec) 자체에 정의된 것이 아닙니다. 각각은 해당 호스트 언어의 명세(HTML, TC39, CSS)에 반영되어야 하므로, 이를 API가 아닌 방향성으로 취급해야 합니다.
Cross-Origin Storage API는 현재 브라우저에서 지원되나요?
아니요. Cross-Origin Storage는 어떤 브라우저에도 구현되어 있지 않으며, 오리진 트라이얼(origin trial)도 없고, 활성화할 플래그(flag)도 없습니다. Emscripten의 자체 COS 문서에서도 명확하게 밝히고 있습니다: 이 API는 "아직 어떤 브라우저에도 출시되지 않았습니다(has not yet shipped in any browser)." 이는 Baseline에 포함되지 않으며, 근접해 있지도 않습니다.
현재 존재하는 것은 주변부에서의 실험들입니다. 라이브러리들이 이를 대상으로 개발할 수 있도록 API를 폴리필(polyfill)하는 Chrome 확장 프로그램이 있으며, 브라우저 내 AI(AI-in-the-browser) 진영은 이미 선택적 지원(opt-in support)을 연결하고 있습니다: Transformers.js는 이를 experimental_useCrossOriginStorage 플래그 뒤에 숨겨 제어하고 있으며, WebLLM과 wllama 또한 실험 중입니다. 이것이 자연스러운 첫 번째 구성원입니다. 한 번 다운로드하여 이를 실행하는 모든 사이트에서 재사용할 수 있는 33GB 모델은, 오리진(origin)마다 매번 다운로드하는 것보다 훨씬 더 나은 시나리오이기 때문입니다.
표준화 측면에서는 정말 초기 단계입니다. Mozilla는 2026년 6월 22일에 표준 위치 이슈 (standards-position issue)를 열었지만, 아직 공식적인 입장을 취하지 않았습니다. WebKit의 입장이나 W3C TAG 검토 결과는 전혀 찾을 수 없었습니다. 따라서 이것은 아직 확정된 구현자가 없는 한 브라우저의 제안 단계입니다.
검색하는 동안 이름이 겹치기 때문에 빠르게 용어를 정리해 드리자면, Cross-Origin _Storage_는 Storage Access API가 아닙니다. Storage Access API는 임베디드된 제3자 콘텐츠(third-party content)가 자신의 쿠키에 접근할 수 있도록 하는 것에 관한 것입니다. COS는 콘텐츠 주소 지정 방식(content-addressed)의 대용량 파일을 공유하는 것에 관한 것입니다. 서로 다른 문제임에도 이름이 혼동될 정도로 유사합니다.
Cross-Origin Storage API를 사용해야 할까요?
코드를 작성하여 적용할 단계는 아니지만, 추적할 가치는 충분합니다. 이 API가 목표로 하는 낭비는 갈수록 심화되고 있습니다. 캐시가 파티셔닝(partitioned)됨에 따라, 현재 모든 사이트는 동일한 공유 라이브러리와 폰트의 복사본에 대해 매번 전체 비용을 지불하고 있으며, 브라우저 내 AI는 파일 크기를 기가바이트(GB) 단위로 계속 밀어붙이고 있어 그 타격이 실제로 커지기 시작했습니다. 오리진(origin) 간에 공유되는 콘텐츠 주소 지정 방식의 캐시는 명백한 해결책이며, 흥미로운 엔지니어링 요소는 이를 작동하게 만드는 것이 아니라 어떻게 안전하게(safe) 만들 것인가에 전적으로 달려 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기