TRENDSHIFT DAILY #12 · 2026-07-25 · CANVAS-UI · 살아있는 웹페이지를 GPU 텍스처로 굽는다

DavidHDev/canvas-ui 딥다이브
— 당신의 진짜 DOM을 불·물·유리로 왜곡하는 프레임워크 무관 캔버스 UI 라이브러리

Canvas UI는 캔버스(canvas) 위에 그려지는 창의적 컴포넌트 25종을 모은 오픈소스 라이브러리다. 한 문장으로 말하면 "유동 시뮬레이션·셰이더 이펙트·3D 씬을, 정적인 배경이 아니라 당신의 살아있는(live)·완전히 인터랙티브한 인터페이스 위에서 실행하는" 도구다. 물 이펙트가 당신의 실제 제목 텍스트를 굴절시키고, 불꽃이 당신의 실제 버튼을 일렁이게 만드는데도 — 글자는 여전히 드래그로 선택되고, 링크는 여전히 클릭된다.

이 프로젝트의 급소는 이것이다: 대부분의 "멋진 웹 이펙트"는 페이지 위에 덮은 장식용 유리판일 뿐, 그 아래 콘텐츠를 읽지 못한다. Canvas UI는 실험적 브라우저 API html-in-canvas를 써서 당신의 실제 DOM을 GPU에 텍스처로 밀어 넣는다. 그래서 이펙트가 벽지가 아니라 페이지 자체를 재료로 삼는다. 지원 안 되는 브라우저에서는 자동으로 순수 WebGL 오버레이로 우아하게 강등(graceful degradation)돼, 모든 방문자가 깨지지 않는 페이지를 본다.

(저장소 DavidHDev/canvas-ui · 사이트 canvasui.dev · 컴포넌트 25종 × 6개 프레임워크 = 150 레지스트리 아이템 · 코어 실험적 html-in-canvas + 손으로 짠 WebGL2 GLSL · 3D 3종만 three.js 0.185 · 배포 shadcn 레지스트리(설치 말고 복사) · 문서 사이트 Next.js 16 + Tailwind v4 + Cloudflare Workers · 라이선스 MIT + Commons Clause · 저자 David Haz · shallow clone 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — 페이지 자체가 텍스처가 된다
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 — 3요소 모델과 두 렌더링 패밀리
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 시스템/브라우저 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"페이지 위에 이펙트를 얹는다"가 아니라 "페이지를 이펙트의 재료로 쓴다"

웹에서 "멋진 비주얼 이펙트"를 넣는 흔한 방법은 이렇다. 페이지 맨 위에 투명한 <canvas>를 한 장 덮고, 그 위에 WebGL로 파티클이나 물결을 그린다. 문제는 이 캔버스가 아래에 뭐가 있는지 전혀 모른다는 것이다. 파티클은 당신의 제목 글자를 피해 가지도, 굴절시키지도 못한다. 그냥 무관한 장식이 위에 떠 있을 뿐이다. 게다가 오버레이가 클릭을 가로채지 않도록 pointer-events:none을 걸어야 해서, 이펙트와 콘텐츠는 완전히 남남이다.

한 줄 비유

창문에 "불타는 영상"을 투사하는 것 vs. 창문 자체가 타오르는데도 그대로 열리는 것

기존 이펙트 라이브러리는 "유리창 앞에 불 영상을 트는 프로젝터"다 — 화려하지만 불은 창문과 아무 상관이 없고, 창문 손잡이도 영상에 가려 안 보인다. Canvas UI의 html-in-canvas는 "당신의 진짜 살아있는 페이지를 마법 렌즈 뒤에 놓는 것"이다. 물결이 당신의 실제 헤드라인을 굴절시키고 ASCII 필터가 당신의 실제 문단을 글리프로 다시 그리는데도 — 아래에 있는 게 여전히 당신의 DOM이라서 글자는 선택되고 링크는 눌린다.

Canvas UI가 하는 일은 명확하다. 실험적 브라우저 기능인 html-in-canvas당신의 살아있는 DOM을 매 프레임 비트맵으로 굽고 → 그 비트맵을 GPU 텍스처로 업로드하고프래그먼트 셰이더(GLSL)로 그 텍스처를 왜곡·변형해 → 결과를 출력 캔버스에 그린다. 그 결과가 원본 DOM 위에 정렬돼 얹히므로, 사용자 눈에는 "페이지가 불타고, 얼고, 물결치고, 산산조각 나는" 것처럼 보이지만 실제 상호작용은 그대로 살아있다.

TERM
html-in-canvas
2026년 크롬에 실험 기능으로 들어온 새 브라우저 API(chromestatus feature 5162535032373248). 캔버스 2D 컨텍스트의 drawElementImage(element, x, y)살아있는 DOM 요소를 캔버스에 래스터화(그림으로 구움)할 수 있게 해준다. 여기에 <canvas layoutsubtree> 속성과 requestPaint()가 짝을 이뤄, 그 캔버스가 DOM을 자식으로 품고 내용이 바뀔 때마다 다시 칠하도록 한다. Canvas UI는 이 비트맵을 WebGL 텍스처로 올려 이펙트의 재료로 쓴다. Chrome/Edge 140+에서 플래그를 켜거나 오리진 트라이얼 토큰이 있어야 하며, 그 외 환경에선 오버레이로 폴백한다.

이름은 canvas-ui지만, 진짜 핵심어는 "live DOM as texture(살아있는 DOM을 텍스처로)"다. 스크린샷을 찍어 얹는 것도, 페이지를 통째로 <canvas>로 다시 그리는 것도 아니다. 여전히 진짜 DOM이 살아 숨 쉬는 채로 그 위를 GPU가 실시간으로 굴절시킨다는 점이 이 도구의 정체성이자, 이 문서에서 배울 것을 관통하는 열쇠다.

2왜 주목받는가

새 브라우저 API + shadcn식 배포 + 프레임워크 무관, 세 물결이 겹친 지점

2026년 웹 프론트엔드에는 세 가지 큰 흐름이 있다. ① 크롬이 밀고 있는 html-in-canvas라는 완전히 새로운 그래픽 API, ② shadcn/ui가 대중화한 "설치하지 말고 소스를 복사해 써라"는 배포 방식, ③ AI 코딩 에이전트가 컴포넌트를 자동 설치하는 MCP 워크플로. Canvas UI가 트렌딩에 오른 이유는 이 세 물결이 정확히 겹치는 지점에 서 있기 때문이다. 하나만 잘해도 화제가 될 텐데, 셋을 한 번에 묶었다.

차별점 ① — html-in-canvas 위에 세운 "첫 진지한 컴포넌트 라이브러리"

대부분의 캔버스 이펙트는 페이지와 단절된 오버레이다. Canvas UI의 급소는 이펙트가 당신의 실제 DOM을 읽어(read) 다시 그린다(redraw)는 것이다. 텍스트는 선택 가능한 채로, 링크는 클릭 가능한 채로 남고, 페이지 전체가 하나의 텍스처가 돼 불·물·유리가 실시간으로 왜곡한다. 이건 이론이 아니라 크롬의 신기능을 실제 프로덕션급 라이브러리로 증명한 첫 사례에 가깝고, 그래서 "이게 진짜 되네?"라는 반응이 트렌딩을 만든다.

차별점 ② — "설치하지 말고 복사하라" (shadcn 레지스트리 + MCP)

Canvas UI는 npm 패키지가 아니다. shadcn 호환 레지스트리로 배포돼, npx shadcn@latest add @canvas-ui/liquid-react 한 줄이면 컴포넌트 소스 파일이 당신 프로젝트의 components/canvasui/에 직접 떨어진다. 블랙박스 의존성이 아니라 당신이 열어보고 고칠 수 있는 코드가 손에 들어온다. 게다가 shadcn MCP 서버를 이 레지스트리에 연결하면 "내 히어로 섹션에 liquid 넣어줘"라고 AI에게 말하는 것만으로 설치된다.

TERM
shadcn 레지스트리 (registry) · "복사해서 쓰는" 배포
shadcn/ui가 유행시킨 방식. 라이브러리를 npm install로 숨기는 대신, 컴포넌트의 소스 코드 자체를 사용자 프로젝트에 복사해 넣는다. 각 컴포넌트는 registry-item.json 규격의 JSON으로 서술되고, CLI(npx shadcn add)가 그 소스를 내려받아 지정 폴더에 쓴다. 장점: 의존성이 안 늘고, 소스를 마음대로 고칠 수 있고, 버전 잠김·공급망 문제에서 자유롭다. Canvas UI는 @canvas-ui라는 신뢰 네임스페이스로 이 방식을 그대로 채택했다.

차별점 ③ — 하나의 엔진에서 6개 프레임워크가 나온다

이런 이펙트 라이브러리는 대개 React 전용이다. Canvas UI는 25개 컴포넌트 각각을 React · Solid · Preact · Vue · Svelte · 바닐라 JS 여섯 가지 맛으로 제공한다(25 × 6 = 150 레지스트리 아이템). 비결은 뒤에서 자세히 뜯어볼 "프레임워크 무관 엔진 하나 + 얇은 래퍼 여섯" 구조다. 어떤 스택을 쓰든 같은 이펙트를 붙일 수 있다는 건, 채택 장벽을 극적으로 낮춘다.

차별점 ④ — 어디서나 깨지지 않는다 (우아한 폴백)

html-in-canvas는 아직 Chrome/Edge 140+에서 플래그를 켜야 하는 실험 기능이다. 그럼 사파리·파이어폭스 사용자는? Canvas UI는 지원 여부를 런타임에 감지해, 지원되면 살아있는 DOM 텍스처 모드로, 안 되면 순수 WebGL 오버레이 모드로 자동 전환한다. 에러 없이, 모든 방문자가 작동하는 페이지를 본다. "최신 기능을 쓰되 아무도 배제하지 않는" 프로그레시브 인핸스먼트의 교과서적 사례다.

GOOD
왜 이 조합이 강력한가

신기술의 충격(html-in-canvas로 진짜 DOM을 왜곡), 고칠 수 있는 산출물(복사되는 소스), 넓은 채택성(6개 프레임워크), 무너지지 않는 호환성(오버레이 폴백), 그리고 25개 이펙트 각각에 붙은 미리보기 영상으로 "정말 되네"를 눈으로 보여준다. "실험적 브라우저 API는 장난감"이라는 인식을 "지금 프로덕션에 쓸 수 있다"로 바꾸려는 시도가 트렌딩의 진짜 동력이다.

3기술 스택 전체 지도

세 개의 세계 — "라이브러리"·"문서 사이트"·"배포 파이프라인"

Canvas UI 저장소를 처음 열면 헷갈린다. 이펙트 라이브러리인가, Next.js 웹사이트인가? 답은 "둘 다이되 역할이 다르다"이다. 이 저장소는 하나의 리포에 (A) 배포할 컴포넌트 라이브러리, (B) 그것을 소개하는 문서·마케팅 사이트, (C) 둘을 잇는 레지스트리 빌드 스크립트가 함께 산다. 이 셋을 구분하는 순간 전체 그림이 선명해진다.

세계 A — 컴포넌트 라이브러리 (src/lib/)

진짜 상품이다. 25개 컴포넌트가 각각 폴더 하나로 들어 있고, 렌더링 전략이 두 갈래로 나뉜다. 이 구분을 반드시 기억해야 한다 — "전부 three.js"라고 오해하기 쉽지만 아니다.

계층기술역할
언어TypeScript 5 (strict)엔진·래퍼 전부. @/* → ./src/* 경로 별칭
렌더링 A (22종)raw WebGL2 + 손으로 짠 GLSLthree.js 없이 getContext("webgl2")로 직접. 인라인 #version 300 es 셰이더
렌더링 B (3종)three.js ^0.185.1Object 3종(Dithered/Particle/Glass)만. GLTFLoader·MeshPhysicalMaterial 등
DOM 소스html-in-canvas (drawElementImage)살아있는 DOM을 비트맵으로 → GPU 텍스처
프레임워크 맛React 19·Solid·Preact·Vue 3.5·Svelte 5·바닐라얇은 래퍼 6종이 하나의 *Vanilla.ts 엔진에 위임
런타임 의존성대부분 0three.js를 선언하는 건 Object 3종뿐. 나머지 22종은 의존성 없음
비유

Canvas UI의 22개 이펙트는 맨손으로 GPU에게 직접 말을 거는 쪽이다 — three.js라는 통역사 없이 WebGL2 원어(GLSL)로 셰이더를 쓴다. 반면 3D 물체를 다루는 3종(Object)만은 씬·조명·GLTF 로딩 같은 무거운 살림이 필요해서 three.js라는 통역사를 고용했다. "가벼운 2D 후처리는 맨손, 진짜 3D 씬은 도구" — 이 선택의 경계가 라이브러리 전체를 이해하는 열쇠다.

세계 B — 문서·마케팅 사이트 (src/app/)

라이브러리를 보여주기 위한 canvasui.dev 사이트 그 자체다. 라이브러리 코드와 섞이지 않도록 src/components/(사이트 UI)와 src/lib/(배포 라이브러리)를 철저히 분리해 둔 게 인상적이다.

요소사용 기술쓰임
프레임워크Next.js 16.2.10 (App Router)output:"export" — 정적 사이트로 빌드
런타임React 19.2.4문서 페이지·플레이그라운드·데모 25종
스타일Tailwind CSS v4 (config-less)@tailwindcss/postcss만. components.json=shadcn 설정
배포Cloudflare Workers + Wrangler 4정적 ./out을 엣지에서 서빙. Vite/webpack 커스텀 없음
부가motion·shiki·nuqs·next-themes·base-ui애니메이션·코드 하이라이트·URL 상태·다크모드·헤드리스 UI

세계 C — 배포 파이프라인 (scripts/build-registry.mts)

이 저장소의 숨은 주인공. npm run dev는 먼저 registry 스크립트를 돌려 150개 레지스트리 JSON을 public/r/에 생성한 뒤 Next를 띄운다. 이 스크립트가 node --experimental-strip-types컴파일 없이 TS를 그대로 실행한다는 점(별도 빌드 툴 없음)도, 그리고 뒤에서 볼 makeStandalone()이라는 "엔진을 래퍼에 인라인해 단일 파일로 합치는" 변환도 전부 여기서 일어난다.

TERM
WebGL2 · GLSL · 프래그먼트 셰이더
WebGL2는 브라우저에서 GPU를 직접 쓰는 저수준 그래픽 API. GLSL(OpenGL Shading Language)은 GPU에서 도는 작은 프로그램을 쓰는 언어다. 그중 프래그먼트 셰이더는 "화면의 각 픽셀 색을 어떻게 정할지"를 픽셀 하나하나에 대해 병렬로 계산한다. Canvas UI는 화면을 덮는 사각형(fullscreen quad) 하나를 그리고, 그 위 모든 픽셀에서 프래그먼트 셰이더를 돌려 "이 자리의 DOM 텍스처를 얼마나·어느 방향으로 굴절시킬까"를 계산한다. three.js는 이 과정을 감싸주지만, Canvas UI의 22개 이펙트는 그 껍데기 없이 #version 300 es 원어로 직접 짰다.

4아키텍처 심화 분석

"엔진 하나 + 얇은 래퍼 여섯" — 이 구조가 배울 것의 8할

Canvas UI의 모든 것은 하나의 설계 결정에서 나온다: 렌더링 로직(엔진)은 프레임워크와 완전히 무관한 순수 DOM+WebGL 코드로 짜고, React/Vue/Svelte 같은 프레임워크 래퍼는 최대한 얇게 유지한다. 기여 가이드(CONTRIBUTING.md)가 이걸 못박아 둔다 — "바닐라 엔진이 모든 렌더링 로직을 소유한다. 프레임워크 래퍼는 얇게 유지하라. 모든 컴포넌트는 html-in-canvas(라이브 DOM 텍스처)와 순수 WebGL 오버레이 폴백 양쪽 모두에서 동작해야 한다."

4-1. 파일 6개의 정체 — 폴더 하나를 열어보면

src/lib/ 아래 25개 폴더는 각각 정확히 6개 파일을 담는다. 하나의 엔진과, 그 엔진을 각 프레임워크로 감싼 얇은 래퍼 다섯이다.

src/lib/Liquid/ ├── LiquidVanilla.ts ← ★ 엔진(공유): DOM + WebGL, 프레임워크 의존성 0 ├── Liquid.tsx ← React 19 래퍼 (엔진에 위임) ├── Liquid.solid.tsx ← Solid 래퍼 ├── Liquid.preact.tsx ← Preact 래퍼 ├── Liquid.vue ← Vue 3 래퍼 └── Liquid.svelte ← Svelte 5 래퍼

4-2. 엔진의 공개 API — 팩토리 하나, 인스턴스 셋

모든 엔진은 똑같은 모양을 갖는다: 지원 여부를 확인하는 supports…() 프로브, 요소들과 옵션을 받아 이펙트를 켜는 create…(elements, options) 팩토리, 그리고 팩토리가 돌려주는 인스턴스가 노출하는 세 개의 손잡이setOptions(실시간 옵션 변경)·resize·destroy.

// LiquidVanilla.ts 류의 엔진이 공통으로 노출하는 인터페이스
export interface AsciifyInstance {
  setOptions: (options: AsciifyOptions) => void;  // 옵션을 실시간 갱신
  resize: () => void;                            // 캔버스 크기 다시 읽기
  destroy: () => void;                           // 루프 정지 + GPU 자원 해제
}

// 이 브라우저가 html-in-canvas를 지원하나? (SSR 안전)
export function supportsHtmlInCanvas(): boolean {
  if (typeof document === "undefined") return false;
  const probe = document.createElement("canvas") as PaintableCanvas;
  const ctx = probe.getContext("2d") as ElementImageContext | null;
  return Boolean(
    ctx &&
    typeof ctx.drawElementImage === "function" &&
    typeof probe.requestPaint === "function",
  );
}

4-3. 핵심 메커니즘 — 세 요소(source / content / output)

이펙트의 심장은 세 개의 DOM 요소가 맞물려 도는 방식이다. 이걸 이해하면 Canvas UI의 절반을 이해한 것이다.

① source <canvas layoutsubtree> ← 살아있는 DOM을 자식으로 "품는" 캔버스 └─ ② content <div> ← 실제 당신의 콘텐츠(제목·버튼·텍스트) ③ output <canvas> ← WebGL이 최종 이펙트를 그리는 곳(맨 위, 클릭 통과) [DOM 바뀜] ─▶ requestPaint ─▶ onpaint 콜백 발동 │ ▼ sourceCtx.drawElementImage(content, 0,0) ← 살아있는 DOM을 비트맵으로 구움 │ ▼ gl.texImage2D(... source) ← 그 비트맵을 GPU 텍스처(uContent)로 업로드 │ ▼ 프래그먼트 셰이더가 uContent를 샘플링해 왜곡 ─▶ output 캔버스에 그림 │ └─▶ 아래엔 여전히 진짜 DOM → 글자 선택·링크 클릭 그대로

코드로 보면 이렇다. html-in-canvas가 지원될 때, DOM이 다시 칠해질 때마다 onpaint가 살아있는 content를 source 캔버스에 비트맵으로 굽고, 그 비트맵을 WebGL 텍스처로 업로드한다.

if (htmlInCanvas) {
  paintable.onpaint = () => {
    try {
      sourceCtx!.reset();
      sourceCtx!.drawElementImage!(content, 0, 0);  // 살아있는 DOM → 캔버스 비트맵
      contentDirty = true;
      wake();  // 렌더 루프 깨우기
    } catch {}
  };
}

// 프레임 루프에서: 더러워졌을 때만 GPU로 업로드(낭비 방지)
function uploadContent() {
  if (!htmlInCanvas || !contentDirty) return;
  contentDirty = false;
  gl!.bindTexture(gl!.TEXTURE_2D, contentTexture);
  gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGBA, gl!.RGBA, gl!.UNSIGNED_BYTE, source);
  gl!.generateMipmap(gl!.TEXTURE_2D);
}

4-4. 얇은 래퍼가 하는 일 — 폴백 분기까지

React 래퍼(약 124줄)의 임무는 넷뿐이다: (1) SSR 안전하게 지원 감지, (2) 세 요소 렌더, (3) useEffect로 엔진 연결, (4) prop이 바뀌면 setOptions로 전달. 지원 감지는 useSyncExternalStore로 하는데, 서버에서는 무조건 false(폴백)로 시작해 하이드레이션 불일치를 피한다.

// 지원 감지 — SSR 안전(서버 → false)
const supported = useSyncExternalStore(emptySubscribe, supportsHtmlInCanvas, () => false);
const native = supported && !failed;

useEffect(() => {
  instanceRef.current = createAsciify({ source, content, output }, initialOptions);
  if (native && !instanceRef.current) setFailed(true);  // 엔진이 null → 폴백으로
  return () => { instanceRef.current?.destroy(); };  // 언마운트 시 정리
}, [initialOptions, native]);

useEffect(() => { instanceRef.current?.setOptions(options); });  // prop 변경 실시간 반영

렌더되는 마크업이 폴백의 정체를 드러낸다. 지원되면 당신의 콘텐츠가 <canvas layoutsubtree> 안으로 들어가 라이브 텍스처가 되고, 안 되면 평범한 형제 <div>에 그대로 렌더되며 오직 오버레이 output 캔버스만 돈다. 어느 쪽이든 사용자는 콘텐츠를 본다.

4-5. 두 렌더링 패밀리 — 이걸 뭉뚱그리면 안 된다

중요
"Canvas UI = three.js"는 틀린 이해다

25개 중 22개는 three.js를 전혀 안 쓴다. 순수 WebGL2 컨텍스트를 직접 만들고 #version 300 es GLSL을 손으로 짠다. three.js(^0.185.1)를 쓰는 건 3D 물체를 다루는 Object 3종뿐이다 — DitheredObject · ParticleObject · GlassObject. 이 셋만 GLTFLoader·DRACOLoader·MeshPhysicalMaterial·PMREM 환경맵을 쓰고, dependencies:["three"]를 선언한다. 나머지는 "의존성 없음"이다. 학습할 때 두 패밀리를 반드시 나눠서 봐야 한다.

three.js 쪽은 훨씬 단순한 래퍼(약 58줄)를 갖는다 — source/content 분리 없이 캔버스 하나다. 유리(Glass) 물체는 표준 three.js 투과 재질을 prop으로 구동한다.

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const glass = new THREE.MeshPhysicalMaterial({
  color: 0xffffff, metalness: 0, transmission: 1,  // 완전 투과 유리
  clearcoatRoughness: 0.06, specularIntensity: 1,
});
// prop → 물리 계수(범위 클램프)
glass.ior        = Math.min(Math.max(config.ior, 1), 2.333);   // 굴절률
glass.dispersion = Math.max(config.dispersion, 0);            // 색분산(무지개)
glass.clearcoat  = Math.min(Math.max(config.clearcoat, 0), 1);

4-6. 배포 시점의 마법 — makeStandalone()

여기서 반전이 있다. 저장소 안에서는 엔진(*Vanilla.ts)과 래퍼(*.tsx)가 분리돼 있지만, 사용자에게 배포되는 파일은 엔진이 래퍼 안으로 인라인된 단일 자기완결 파일이다. 빌드 시점에 registry.tsmakeStandalone()import {…} from "./XxxVanilla" 줄을 지우고 그 자리에 엔진 본문을 통째로 이어붙인다. React/Solid/Preact는 import 치환으로, Vue/Svelte는 <script> 블록 주입으로 처리한다.

function makeStandalone(base, wrapper, engine, kind) {
  const body = engine.trimEnd();
  if (kind === "react" || kind === "solid" || kind === "preact") {
    return wrapper.replace(vanillaImport(base), body + "\n\n")     // import 줄 → 엔진 본문
                  .replace(/\nexport type \{[^}]*\};\n?/, "\n");
  }
  const stripped = wrapper.replace(vanillaImport(base), "");
  if (kind === "vue") return `<script lang="ts">\n${body}\n</script>\n\n${stripped}`;
  // svelte: <script module> 안으로 주입
}
설계 패턴 · 핵심 3가지

이 코드에서 배울 만한 설계 결정

① 엔진/래퍼 분리(개발) → 인라인(배포): 개발할 땐 로직을 한 곳(엔진)에 두고 프레임워크별 래퍼를 얇게 유지해 유지보수 비용을 1/6로 줄이고, 배포할 땐 makeStandalone으로 합쳐 사용자에겐 단일 복사 파일을 준다. "개발 편의"와 "사용 편의"를 빌드 단계로 화해시켰다.

② 프로그레시브 인핸스먼트 내장: supports…() 프로브 + useSyncExternalStore(SSR 안전) + 마크업 분기로, 신기능을 쓰되 폴백을 컴포넌트 레벨에 구조화했다. "된다/안 된다"를 사용자가 신경 쓸 필요가 없다.

③ 수요 기반 렌더 루프 + 옵저버 생명주기: RAF 루프가 포인터가 멈추고 콘텐츠가 깨끗하면 스스로 정지하고, ResizeObserver·IntersectionObserver(화면 밖이면 일시정지)·MutationObserver(테마 변경 감지)·matchMedia(모션 축소·다크모드)로 상황에 반응한다. destroy()는 모든 옵저버·GPU 자원을 해제한다. 배터리·성능을 챙기는 실전 라이프사이클의 모범.

5디렉토리 구조 해부

"배포 라이브러리(lib)"와 "문서 사이트(app)"를 칼같이 나눈 345개 파일

구조를 보면 저자의 규율이 보인다. 사용자에게 복사돼 나갈 코드(src/lib/)와, 그것을 소개하는 사이트 코드(src/app/·src/components/)를 철저히 분리했다. 배포되는 것은 오직 src/lib/이고 나머지는 전부 마케팅·문서다.

canvas-ui/ ├── package.json ← name:"canvasui" v0.1.0, private (npm 배포 안 함) ├── next.config.ts ← output:"export" (정적 사이트) ├── components.json ← shadcn 설정(style:"base-nova") ├── wrangler.jsonc ← Cloudflare Workers 배포 설정 ├── postcss.config.mjs ← Tailwind v4 (config-less) │ ├── scripts/ │ └── build-registry.mts ← ★ 150개 레지스트리 JSON 생성 + makeStandalone │ ├── public/ │ ├── llms.txt ← AI가 읽는 사이트맵 │ ├── assets/videos/*.webm ← 컴포넌트별 미리보기 영상 25개 │ ├── assets/models/bolt.glb ← 샘플 3D 모델 │ └── r/ ← (빌드 생성물) 레지스트리 JSON, 커밋 안 됨 │ └── src/ ├── lib/ ← ★★ 배포되는 라이브러리 (25 폴더 × 6 파일) │ ├── Liquid/ Blaze/ Glass/ Shatter/ VHS/ Frost/ ... (25종) │ │ └ <Name>.tsx / .solid.tsx / .preact.tsx / .vue / .svelte / <Name>Vanilla.ts │ ├── registry.ts ← COMPONENTS 레코드 + makeStandalone + getRegistryItem │ ├── utils.ts ← cn() 헬퍼 │ └── github.ts ← REPO = "DavidHDev/canvas-ui" (별 수 런타임 조회) │ ├── app/ ← Next.js 16 문서+랜딩 (App Router) │ ├── (shell)/docs/components/<name>/page.tsx ← 문서 25페이지 │ ├── (shell)/docs/{installation,mcp}/page.tsx │ └── playground/page.tsx ├── components/ ← 사이트 UI (docs/ landing/ playground/ demos/ ui/) ├── demos/ ← 인터랙티브 데모 25개 └── data/components.ts ← 카탈로그 메타데이터(이름·설명·영상)

구조가 말하는 이야기는 분명하다. 진짜 무게중심은 src/lib/의 25개 *Vanilla.ts 엔진에 있다. 엔진 크기는 Glitch 424줄부터 HexFloat 1616줄, Frost 1186줄까지 — 이 손으로 짠 셰이더들이 라이브러리의 실질적 자산이다. 래퍼는 58~148줄로 얇다. "생성은 얇게, 이펙트 엔진은 두껍게"라는 분포가 이 프로젝트의 정체성을 그대로 반영한다.

6학습 포인트

이 레포 하나로 배우는 것 — 최신 브라우저 API + GPU 그래픽스 + 라이브러리 설계

배울 것 ① — html-in-canvas, 가장 새로운 웹 그래픽 API

drawElementImage · requestPaint · layoutsubtree 삼총사를 실제 프로덕션 코드에서 볼 수 있는 드문 기회다. "살아있는 DOM을 GPU 텍스처로 만든다"는 개념과, 지원 감지·오리진 트라이얼·폴백까지 신기능을 안전하게 채택하는 전 과정을 배운다. 아직 문서가 거의 없는 API라 이 레포 자체가 레퍼런스다.

배울 것 ② — 프레임워크 무관 컴포넌트 아키텍처

이 레포에서 가장 재사용 가치가 큰 교훈. "순수 로직 엔진(팩토리 → {setOptions, resize, destroy}) + 얇은 프레임워크 래퍼"라는 패턴은 이펙트 라이브러리뿐 아니라 어떤 크로스 프레임워크 라이브러리에도 적용된다. React useSyncExternalStore로 외부 상태를 SSR 안전하게 구독하는 법까지 실물로 익힌다.

배울 것 ③ — three.js 없이 raw WebGL2 + GLSL

22개 이펙트가 맨손 WebGL2다. 컨텍스트 생성, #version 300 es 셰이더 컴파일, 유니폼 위치 자동 탐색(ACTIVE_UNIFORMS 순회), 화면을 덮는 사각형(TRIANGLE_STRIP 4정점)으로 프래그먼트 셰이더 구동 — WebGL 저수준을 정독할 최고의 교재다. three.js가 감춰주던 것들을 직접 보게 된다.

배울 것 ④ — GPU 유체 시뮬레이션(Navier-Stokes)

LiquidVanilla.ts는 고전적 안정 유체(stable fluids) 솔버다. ADVECT · DIVERGENCE · CURL · VORTICITY · PRESSURE · GRADIENT9개 셰이더 프로그램더블 버퍼링된 부동소수 FBO 위에서 돌린다(simResolution 128, pressureIterations 4). 대학원 그래픽스 과제로 나오는 유체 시뮬을 실전 코드로 뜯어볼 수 있다.

// FRAG_ADVECT — 준-라그랑주 이류(과거로 거슬러 샘플링)
vec2 coord = vUv - uDt * texture(uVelocity, vUv).xy * texelSize;
outColor = uDissipation * texture(uSource, coord);

// FRAG_PRESSURE — 야코비 반복(pressureIterations 번 반복)
float pressure = (L + R + B + T - divergence) * 0.25;

배울 것 ⑤ — 물리 기반 유리·굴절 셰이더

GlassVanilla.ts는 raw WebGL로 둥근 사각형 SDF 렌즈 + 프레넬(Fresnel) + 6개 파장 색수차(chromatic aberration)를 구현해 진짜 분광(dispersion)을 낸다. 페이지 텍스처를 파장별로 굴절해 무지개 테두리를 만드는 이 기법은, 유리 재질 렌더링의 정수를 순수 GLSL로 보여준다.

// 파장별 굴절률 → 6개 파장(611.4~374nm)으로 페이지를 각각 샘플링해 합성
float iorForWavelength (float w) {
  float ab = uAberration * 0.1;
  return mix(uIor + ab, uIor - ab, 1.0 - pow(1.0 - linearStep(450.0, 650.0, w), 4.0));
}
refracted  = sampleRefraction(basePx, rim, normal, iorForWavelength(611.4)) * vec3(1,0,0);
refracted += sampleRefraction(basePx, rim, normal, iorForWavelength(570.5)) * vec3(1,1,0);
// ...4개 파장 더... 그리고 재결합

배울 것 ⑥ — shadcn 레지스트리로 라이브러리 배포하기

npm 없이 컴포넌트를 배포하는 법. registry-item.json 규격, getRegistryItem이 설치 경로(components/canvasui/)를 정하는 법, Solid엔 solid-js를 deps에 추가하는 식의 프레임워크별 처리, 그리고 MCP 서버 연결까지. "복사해서 쓰는" 배포를 직접 만들어보고 싶은 사람에게 완결된 예제다.

주의
만능이 아니다 — 실험 기능의 한계

완전한 html-in-canvas 경험은 아직 Chrome/Edge 140+에서 플래그를 켜거나 프로덕션용 오리진 트라이얼 토큰이 있어야 한다. 사파리·파이어폭스에서는 라이브 DOM 왜곡이 아니라 WebGL 오버레이 폴백으로 동작한다(이펙트는 나오지만 "진짜 DOM을 굴절"하는 핵심 마법은 빠진다). 또한 문서·llms.txt는 "다섯 가지 맛"이라 적지만 실제 빌드는 Preact를 더해 여섯을 낸다(사소한 문서 불일치). 라이선스도 순수 MIT가 아니라 MIT + Commons Clause라, 라이브러리 자체를 "팔거나 재배포"하는 것은 금지(자기 제품에 쓰는 건 자유)라는 점을 유의해야 한다.

7시스템/브라우저 요구사항

"완전한 마법"과 "폴백"을 가르는 선
항목요구사항
완전한 html-in-canvasChrome/Edge 140+ + chrome://flags/#canvas-draw-element 활성화. 프로덕션은 오리진 트라이얼 토큰
폴백(오버레이) 동작WebGL2 지원 브라우저 — Chrome/Edge 111+ · Firefox 111+ · Safari 16.4+
three.js 3종(Object)사용 컴포넌트가 three를 프로젝트에 설치(CLI가 자동 추가)
컴포넌트 소비(사용자)shadcn CLI(npx shadcn) — 빌드 설정 불필요. 소스가 components/canvasui/에 떨어짐
사이트 개발/기여Node 22 · npm · Next.js 16 · Tailwind v4. CI: tsc --noEmit → eslint → next build
사이트 배포Cloudflare Workers + Wrangler 4 (npm run deploy)
라이선스MIT + Commons Clause — 자기 앱/사이트/제품에 사용 자유, 라이브러리 자체 판매·재배포 금지

주목할 점은 "성능 부담의 위치"다. 라이브러리 소비자는 무거운 런타임을 설치하지 않는다(대부분 의존성 0). 진짜 비용은 GPU가 매 프레임 DOM 텍스처를 업로드하고 셰이더를 돌리는 렌더 루프에서 나온다. 그래서 "더러울 때만 업로드(contentDirty)", "포인터 멈추면 루프 정지", "화면 밖이면 IntersectionObserver로 일시정지", "DPR 상한 2" 같은 최적화가 전부 성능·배터리 절감 장치로 맞물려 있다. 아키텍처와 성능 구조가 한 몸인 셈이다.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 읽지만 말고 직접 페이지를 불태워 보기
난이도 ★ · 입문

1. shadcn CLI로 컴포넌트 하나 설치해 히어로에 붙이기

Next.js/React 프로젝트에서 npx shadcn@latest add @canvas-ui/liquid-react를 실행하면 components/canvasui/Liquid.tsx가 생긴다. 히어로 섹션을 <Liquid>…</Liquid>로 감싸고 style={{height:480}}을 줘본다. "설치 없이 소스가 복사되는" shadcn 배포를 몸으로 겪는 게 목표.

난이도 ★★ · 초급

2. 플래그를 껐다 켜며 폴백 관찰하기

Chrome에서 chrome://flags/#canvas-draw-element를 켠 상태와 끈 상태로 canvasui.dev의 같은 데모를 비교한다. 켰을 때는 페이지 텍스트 자체가 왜곡되고, 껐을 때는 오버레이 이펙트만 도는 것을 확인. "라이브 DOM 텍스처 vs 오버레이 폴백"의 차이를 눈으로 각인시킨다.

난이도 ★★★ · 중급

3. Vanilla 엔진 열어 유니폼 하나 바꿔보기

LiquidVanilla.tsGlassVanilla.ts를 열어 옵션(예: curl·pressure·aberration·ior) 하나의 기본값을 바꾸고 setOptions로 실시간 반영해 본다. 유체의 소용돌이 세기나 유리의 색수차가 어떻게 변하는지 관찰. GLSL 유니폼이 시각 결과로 이어지는 고리를 손으로 잡는다.

난이도 ★★★★ · 중상급

4. 같은 컴포넌트를 비-React 프레임워크에 붙여 래퍼 차이 읽기

같은 이펙트의 <Name>.tsx(React)와 <Name>.svelte 또는 <Name>.vue를 나란히 놓고, 공유 엔진은 그대로인데 래퍼만 어떻게 달라지는지 대조한다. React의 useSyncExternalStore가 Svelte에선 무엇으로 대체되는지, 생명주기 훅 매핑을 정리해 본다. 프레임워크 무관 설계의 실체를 이해하는 훈련.

난이도 ★★★★★ · 고급

5. 나만의 최소 html-in-canvas 이펙트 만들기

세 요소(source/content/output) 모델을 그대로 본떠, 페이지를 흑백 반전하거나 그레이스케일로 만드는 간단한 프래그먼트 셰이더 이펙트를 처음부터 짠다. supports…() 프로브와 오버레이 폴백까지 넣어 Chrome/Safari 양쪽에서 깨지지 않게 만든다. 이 레포의 핵심 패턴을 통째로 재현하는, 가장 값진 과제.

9관련 기술 심화 학습 로드맵

Canvas UI를 완전히 소화하기 위한 4주 코스
주차주제학습 내용
1주차WebGL2 기초컨텍스트·셰이더(GLSL 300 es)·프로그램 컴파일 / 유니폼·텍스처 / 화면 덮는 사각형(fullscreen quad)으로 프래그먼트 셰이더 구동. 도구: WebGL2 Fundamentals + *Vanilla.ts 정독
2주차html-in-canvas APIdrawElementImage·requestPaint·layoutsubtree / DOM-as-texture / 오리진 트라이얼 / 프로그레시브 인핸스먼트(지원 감지→폴백). chromestatus 5162535032373248 문서
3주차프레임워크 무관 라이브러리 설계바닐라 엔진 + 얇은 래퍼 / React useSyncExternalStore로 SSR 안전 감지 / ResizeObserver·IntersectionObserver·MutationObserver 생명주기 / shadcn 레지스트리·makeStandalone 빌드
4주차고급 GPU 이펙트유체 시뮬(Navier-Stokes stable fluids, FBO 더블버퍼) / 물리 유리(프레넬·SDF·색수차) / three.js MeshPhysicalMaterial·transmission·PMREM 환경맵. LiquidVanilla·GlassVanilla·GlassObjectVanilla

이 순서를 밟으면 Canvas UI는 단순한 "예쁜 이펙트 모음"이 아니라 최신 브라우저 API·GPU 그래픽스·크로스 프레임워크 라이브러리 설계를 잇는 살아있는 교과서가 된다. 각 주차 개념이 정확히 어느 소스 파일에 구현돼 있어, "이론 → 실제 코드" 왕복 학습이 가능하다.

10핵심 키워드 사전

이 레포를 읽을 때 걸려 넘어지는 용어를 한 번에
용어의미
html-in-canvas살아있는 DOM을 캔버스에 그릴 수 있게 하는 실험적 크롬 API. 이 라이브러리의 정체성
drawElementImage캔버스 2D 컨텍스트 메서드. DOM 요소를 비트맵으로 래스터화해 캔버스에 그림
requestPaint / layoutsubtreeDOM을 품는 캔버스가 내용 변화 시 다시 칠하게 하는 속성·메서드
live DOM as texture스크린샷이 아니라 진짜 살아있는 DOM을 GPU 텍스처로 쓰는 것
progressive enhancement지원되면 신기능, 안 되면 폴백. 아무도 배제하지 않는 점진적 향상
graceful degradation실험 기능이 없을 때 WebGL 오버레이로 우아하게 강등
WebGL2브라우저에서 GPU를 직접 쓰는 저수준 그래픽 API
GLSL / 프래그먼트 셰이더GPU에서 도는 언어 / 픽셀별 색을 병렬 계산하는 셰이더
fullscreen quad화면을 덮는 사각형. 그 위 모든 픽셀에서 셰이더를 돌림
uniform셰이더에 넣는 전역 파라미터(포인터 위치·시간·옵션 값 등)
FBO / 더블 버퍼링프레임버퍼 오브젝트. 유체 시뮬은 읽기/쓰기 텍스처를 교대로 씀
Navier-Stokes / stable fluids유체의 운동 방정식 / 브라우저용 안정 유체 솔버(Liquid)
premultiplied alpha오버레이 합성 규칙. 셰이더는 rgb*alpha로 출력(하드 규칙)
Fresnel / 색수차각도에 따른 반사 / 파장별 굴절로 생기는 무지개 테두리
SDF (부호 거리장)도형까지의 거리를 함수로 표현. 유리 렌즈 모양을 매끄럽게 정의
three.js / MeshPhysicalMaterial3D 라이브러리 / metalness·transmission 등 물리 기반 재질
PMREMthree.js 환경맵 생성기. 유리에 주변 반사를 입힘
shadcn 레지스트리설치 대신 소스를 복사하는 배포. registry-item.json 규격
makeStandalone빌드 시 엔진을 래퍼에 인라인해 단일 파일로 합치는 변환
MCPModel Context Protocol. AI가 레지스트리에서 컴포넌트를 설치하게 함
origin trial실험 API를 플래그 없이 특정 도메인에서 쓰게 하는 토큰
useSyncExternalStoreReact 훅. 외부 상태를 SSR 안전하게 구독(지원 감지에 사용)
framework-agnostic특정 프레임워크에 안 묶임. 하나의 엔진 → 6개 맛

11참고 링크

원문으로 더 파고들기