FFmpeg.wasm 코어가 로드는 되는데 왜 이렇게 느린가 했더니, 헤더 하나가 빠져서 싱글스레드로만 돌고 있었더라고요. media-crop 저장소next.config.ts 얘기예요. FFmpeg.wasm처럼 무거운 WebAssembly 모듈을 브라우저에서 돌리는 프로젝트라면 한 번쯤 마주칠 설정이에요.

모든 응답에 헤더 두 개를 붙였어요

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
          { key: "Cross-Origin-Embedder-Policy", value: "credentialless" },
        ],
      },
    ];
  },
};

source: "/(.*)"는 이 프로젝트의 모든 경로에 두 헤더를 무조건 붙인다는 뜻이에요. 특정 페이지에서만 FFmpeg.wasm을 쓰는데도 전역으로 설정한 이유는, 브라우저가 문서 전체의 격리 상태를 기준으로 SharedArrayBuffer 사용 가능 여부를 판단하기 때문이에요.

SharedArrayBuffer가 왜 막혀있었나

브라우저는 Spectre류 사이드채널 취약점 터진 뒤로 SharedArrayBuffer를 기본적으로 꺼뒀어요. 이 타입은 여러 스레드(Worker)가 같은 메모리를 직접 공유하게 해주는데, 문서가 신뢰 안 되는 다른 출처 자원을 같이 로드하고 있으면 이 공유 메모리로 다른 사이트 데이터를 유추하는 공격이 가능해지거든요. @ffmpeg/ffmpeg의 멀티스레드 코어는 이 타입에 내부적으로 의존해서, 헤더 없이는 코어 로드 단계에서 조용히 실패하거나 싱글스레드로만 돌더라고요.

credentialless를 고른 이유

COEP 값엔 require-corpcredentialless 두 선택지가 있어요. require-corp는 페이지가 불러오는 모든 외부 리소스(폰트, 이미지, 스크립트)가 Cross-Origin-Resource-Policy 헤더를 명시적으로 줘야만 로드를 허용해요. media-crop처럼 CDN에서 FFmpeg 코어랑 wasm 파일을 받아오는 프로젝트가 이 값을 쓰면, 그 CDN이 해당 헤더를 안 주는 이상 리소스 로딩 자체가 막혀버려요. credentialless는 쿠키 같은 자격 증명은 안 보내는 조건으로 외부 리소스를 좀 더 유연하게 허용하면서도 격리 요구사항은 만족시켜서, 이 프로젝트의 CDN 의존 구조랑 훨씬 잘 맞았어요.

다만 이 헤더들은 문서 전체의 격리 모드를 바꾸는 설정이라, 나중에 새로운 외부 위젯이나 iframe을 추가할 때 예상 못 한 로딩 문제가 생길 수 있어요. 지금은 이 프로젝트가 쓰는 CDN 구성에서 문제없었다는 것만 확인한 거고, 모든 외부 리소스 조합에서 안전하다고 장담은 못 해요.