LogoSEO Jing
  • All Posts
  • SEO Jing
  • KD Team
  • CLab CoreTeam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

WebAssemblyShikiRSCCloudflare Workers트러블슈팅DevLog

RSC 환경에서 WebAssembly가 차단되는 이유 — Shiki에서 rehype-prism-plus로

2026년 3월 16일·11분 읽기

WebAssembly란 무엇인가

WebAssembly(Wasm)는 브라우저와 서버에서 실행할 수 있는 저수준 바이너리 포맷이다. C, C++, Rust 같은 언어로 작성된 코드를 컴파일해서 .wasm 바이너리로 만들고, JavaScript에서 WebAssembly.instantiate()로 로드해 실행한다.

JavaScript보다 빠른 성능이 필요할 때 사용된다. 이미지 처리, 암호화, 정규표현식 엔진 같은 CPU 집약적 작업이 대표적이다.

js
// WebAssembly 모듈 로드의 기본 형태
const response = await fetch(
"module.wasm"
)
;
const { instance } = await WebAssembly.instantiateStreaming(response);
instance.exports.someFunction();

문제는 모든 런타임이 WebAssembly를 허용하지는 않는다는 점이다.

Shiki는 왜 WebAssembly를 쓰는가

Shiki는 VS Code와 동일한 TextMate 문법을 사용하는 구문 강조 라이브러리다. TextMate 문법의 정규표현식은 JavaScript의 RegExp로는 처리할 수 없는 Oniguruma 정규표현식 문법을 사용한다.

Oniguruma는 원래 C 라이브러리다. 이걸 브라우저와 Node.js에서 쓰려면 WebAssembly로 컴파일해야 한다. Shiki가 내부적으로 onig.wasm을 로드하는 이유가 이것이다.

shiki/dist/onig.wasm  ← Oniguruma를 WebAssembly로 컴파일한 바이너리

즉, codeToHtml()을 호출하면 내부에서 이런 일이 벌어진다.

codeToHtml() 호출
  → Oniguruma 엔진 초기화
  → WebAssembly.instantiate(onig.wasm)  ← 여기서 터짐
  → TextMate 문법으로 토큰화
  → HTML 생성

문제 상황

블로그의 코드 블록 하이라이팅에 Shiki를 사용하고 있었다. MDX 컴포넌트 매핑에서 pre 태그를 가로채 Shiki로 하이라이팅하는 방식이다.

tsx
// MdxRenderer.tsx — 문제의 코드
import { codeToHtml } from "shiki";

pre: async ({ children, ...props }) => {
  const highlighted = await codeToHtml(codeString, {
    lang: language ?? "text",
    theme: "github-dark",
  });
  return <CodeBlock code={highlighted} language={language} />;
},

로컬 vinext dev에서는 잘 동작했다. 하지만 Cloudflare Workers 환경에서 렌더링하면 이런 에러가 터졌다.

CompileError: WebAssembly.instantiate(): Wasm code generation disallowed by embedder

왜 RSC 환경에서 WebAssembly가 차단되는가

React Server Components(RSC)는 서버에서 컴포넌트를 렌더링하고, 그 결과를 직렬화해서 클라이언트로 스트리밍하는 아키텍처다.

이때 "서버"가 어디서 실행되느냐가 핵심이다.
  • Node.js 서버 — WebAssembly 완전 지원. 문제없다.
  • Cloudflare Workers — V8 isolate 기반. WebAssembly를 제한적으로 지원한다.

Cloudflare Workers에서 WebAssembly를 사용하려면 모듈을 정적으로 import하는 방식만 허용된다.

js
// Workers에서 허용되는 방식
import wasm from "./module.wasm";
const instance = await WebAssembly.instantiate(wasm);

// Workers에서 차단되는 방식 — Shiki가 하는 방식
const response = await fetch("onig.wasm");
await WebAssembly.instantiateStreaming(response);

Shiki는 런타임에 동적으로 WASM 바이너리를 로드하고 인스턴스화한다. 이 방식이 Workers의 보안 정책에 의해 차단되는 것이다.

브라우저도 마찬가지다. Content-Security-Policy에 wasm-unsafe-eval을 추가하면 브라우저에서는 동작하지만, RSC 렌더링은 서버(Workers)에서 일어나므로 CSP 헤더와는 무관하다.

해결: 런타임 하이라이팅에서 빌드 타임 하이라이팅으로

문제의 본질을 정리하면 이렇다.
  • Shiki는 런타임에 WebAssembly로 구문 강조를 수행한다.
  • RSC 렌더링이 일어나는 Workers 환경에서는 이 방식의 WASM 로드가 차단된다.
  • 따라서 구문 강조를 빌드 타임으로 옮기면 런타임에서 WASM이 필요 없어진다.

rehype-prism-plus를 선택했다. 이 라이브러리는 Prism.js 기반의 rehype 플러그인으로, 순수 JavaScript만 사용한다. WASM 의존성이 전혀 없다. 그리고 rehype 플러그인이므로 MDX 컴파일 시점에 동작한다.

1. 의존성 교체

bash
pnpm remove shiki
pnpm add -D rehype-prism-plus

2. MDX 컴파일에 rehype 플러그인 추가

빌드 스크립트의 compile() 호출에 rehype 플러그인을 추가한다. 이제 MDX를 JSX로 컴파일할 때 코드 블록이 자동으로 하이라이팅된다.

ts
import rehypePrismPlus from "rehype-prism-plus";

const compiled = await compile(result.source, {
  outputFormat: "program",
  development: false,
  jsx: true,
  rehypePlugins: [[rehypePrismPlus, { ignoreMissing: true }]],
});

컴파일 결과를 보면 코드 블록의 각 토큰이 <span className="token keyword"> 같은 Prism 클래스로 감싸져 있다. 빌드 시점에 이미 하이라이팅이 완료된 것이다.

3. MdxRenderer에서 Shiki 제거

더 이상 런타임에 하이라이팅할 필요가 없다. pre 컴포넌트에서 Shiki 호출을 제거하고, 빌드 타임에 생성된 React 요소를 그대로 렌더링한다.

tsx
// Before — 런타임 Shiki (WASM 필요)
import { codeToHtml } from "shiki";

pre: async ({ children }) => {
  const highlighted = await codeToHtml(code, { lang, theme: "github-dark" });
  return <CodeBlock code={highlighted} />;
},

// After — 빌드 타임 하이라이팅 (WASM 불필요)
pre: ({ children }) => {
  return (
    pre 

주의할 점이 하나 있었다. MDX의 code 컴포넌트 매핑이 인라인 코드(backtick)와 코드 블록 내부의 <code> 태그에 모두 적용된다. 인라인 코드용 스타일(배경색, 패딩)이 코드 블록 안에서도 적용되면 디자인이 깨진다.

tsx
code: ({ className, ...props }) => {
  // 코드 블록 내부의 code인지 판별
  const isInCodeBlock = className?.split(" ")
    .some(c => c.startsWith("language-") || c === "code-highlight");

  if (isInCodeBlock) {
    return <code className="font-mono" {...props} />;
  }
  // 인라인 코드
  return <code className  

4. Prism 토큰 CSS 추가

rehype-prism-plus가 생성하는 <span className="token keyword"> 같은 요소에 색상을 입혀야 한다. globals.css에 GitHub Dark 스타일의 Prism 토큰 CSS를 추가했다.

css
.token.keyword {
  color: #ff7b72;
}
.token.string {
  color: #a5d6ff;
}
.token.function {
  color: #d2a8ff;
}
.token.comment {
  color: #8b949e;
}
/* ... */

Before / After 비교

[Before] 페이지 요청
  → RSC 렌더링 시작
  → pre 컴포넌트에서 codeToHtml() 호출
  → Shiki가 onig.wasm 로드 시도
  → WebAssembly.instantiate() 차단
  → 에러

[After] 빌드 타임
  → MDX 컴파일 시 rehype-prism-plus가 토큰화
  → .compiled.jsx에 하이라이팅된 span이 포함됨

[After] 페이지 요청
  → RSC 렌더링 시작
  → pre 컴포넌트가 이미 하이라이팅된 children을 그대로 렌더링
  → WASM 불필요 → 에러 없음

핵심 교훈

이 문제는 이전 포스트의 eval 차단 이슈와 같은 뿌리에서 나온다. 런타임에서 할 필요 없는 작업을 런타임에서 하고 있었다는 것이다.

  • gray-matter의 eval() → 빌드 타임 frontmatter 파싱으로 해결
  • next-mdx-remote의 new Function() → 빌드 타임 MDX 컴파일로 해결
  • Shiki의 WebAssembly.instantiate() → 빌드 타임 구문 강조로 해결

패턴이 보인다. Edge 런타임(Cloudflare Workers, Vercel Edge 등)에서는

런타임 코드 생성(eval, new Function)과 WASM 동적 로드가 차단

된다. 이 제약을 만나면 해결책은 항상 같다 — 빌드 타임으로 옮기기.

그리고 WASM 의존성은 라이브러리 내부에 숨어있어서 찾기 어렵다. Shiki가 Oniguruma WASM을 쓴다는 건 직접 에러를 만나기 전엔 모르기 쉽다. Edge 환경에 배포할 때는 의존성 트리에서 .wasm 파일이 있는지 미리 확인해보자.

bash
# 의존성 중 .wasm 파일이 있는지 확인
find node_modules -name "*.wasm" -type f

포스트 목록

/SEOJing
파일 11개, 폴더 1개
Cloudflare Workers에서 fs 모듈이 안 되는 이유와 해결법모바일 웹에서 가로 모드를 강제하는 5가지 방법 — iOS Safari에서도 동작하는 코드 뷰어 만들기블로그 글을 PPT로 만들기 — DOM 클로닝 기반 프레젠테이션 모드100vh가 100%가 아닌 이유 — 모바일 뷰포트 단위 완전 정리Context로 퀴즈 컴포넌트를 만들다 막혀서 React.Children을 공부하게 된 이야기localStorage 읽기에서 하이드레이션 에러가 터지는 이유 useSyncExternalStore로 해결useEffect cleanup과 의존성 배열 — 실전 버그 사례로 이해하는 생애주기vinext + GitHub Actions로 Cloudflare Workers 배포하기vinext 오픈소스 기여기: 한국어 slug가 RSC에서 이슈를 일으킨 이유RSC 환경에서 WebAssembly가 차단되는 이유 — Shiki에서 rehype-prism-plus로vinext는 왜 빠를까? — SSR, Vite, Edge, 그리고 Web Vitals까지
<
className
="..."
>
<code className="font-mono">{codeProps.children}</code>
</pre>
);
},
="rounded bg-gray-100 px-1.5 py-0.5 ..."
{...props}
/>
;
},