5분 AI.
TRENDSHIFT #4 딥다이브 · 2026-08-24 분석

ThreeUI 딥다이브
— 유료 제품에서 무료 부분만 자동으로 도려내 공개하는 법

겉보기엔 Three.js·WebGL로 만든 화려한 웹 컴포넌트 모음집입니다. 그런데 진짜 볼거리는 컴포넌트가 아니라 그 뒤에 있습니다 — 비공개 본체에서 무료 부분만 잘라내 공개 저장소로 흘려보내고, 그 경계가 새지 않았는지를 사람이 아니라 테스트가 검사합니다. (저장소: MengTo/threeui · ⭐2.8k · TypeScript/React · MIT · 2026-08-24 기준)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

ThreeUI가 정확히 무엇을 하는 물건인가

ThreeUI Community는 유료 제품 threeui.com무료 부분만 통째로 떼어 공개한 저장소입니다. 안에는 두 가지가 같이 들어 있습니다.

컴포넌트는 웹 배경·히어로 섹션·버튼·텍스트 애니메이션·랜딩페이지 같은 것들이고, 대부분 WebGL 셰이더나 Three.js 3D 장면으로 그려집니다. 만든 사람은 Meng To — 디자인 교육 사이트 Design+Code를 만든 사람입니다.

공개일이 2026년 8월 21일, 이 문서를 쓰는 8월 24일 기준 별 2,819개·포크 283개. 사흘 만입니다.

한 컷 비유

"본점 주방은 비공개, 그런데 무료 메뉴만 담은 분점이 자동으로 복제된다"

본점(비공개 저장소)에는 유료 메뉴와 무료 메뉴가 섞여 있습니다. 손님에게 공개하는 분점(이 저장소)에는 무료 메뉴만 있어야 합니다.

보통은 사람이 옮깁니다. 그러다 유료 레시피 한 줄이 딸려 나오면 끝입니다. ThreeUI는 여기를 기계에 맡겼습니다 — 본점이 바뀔 때마다 자동으로 잘라내 분점으로 보내고, 잘못 딸려 나온 게 없는지 사람 눈이 아니라 실패하는 테스트가 확인합니다. 이 저장소에서 배울 게 가장 많은 부분이 여기입니다.

용어
셰이더 (shader)
GPU가 화면의 픽셀 하나하나에 대해 돌리는 아주 작은 프로그램. "이 위치의 색은 무엇인가"를 계산합니다. 픽셀이 수백만 개라도 GPU가 동시에 처리하기 때문에 CPU로는 불가능한 화면 효과가 60fps로 돕니다. 웹에서는 GLSL이라는 C 비슷한 언어로 씁니다.
용어
오픈코어 (open core)
제품의 핵심 일부는 오픈소스로 공개하고, 나머지는 유료로 파는 사업 방식. 문제는 항상 "경계 관리"입니다 — 유료 코드가 공개 쪽으로 새면 되돌릴 수 없습니다. ThreeUI는 이 경계를 문서가 아니라 빌드 스크립트와 테스트로 지킵니다.

2왜 주목받는가

트렌딩 이유와 경쟁 제품 대비 강점

사흘 만에 2.8k는 코드 품질만으로 나오는 숫자가 아닙니다. 세 가지가 겹쳤습니다.

  1. 결과물이 눈에 보입니다. 대부분의 인기 저장소는 스크린샷으로 매력을 전하기 어렵습니다. WebGL 히어로 배경은 반대입니다 — GIF 한 장이면 설명이 끝납니다.
  2. 데모가 아니라 장사하던 코드입니다. 자습용으로 새로 쓴 예제가 아니라, 돈 받고 파는 사이트의 코드 그대로입니다. 카탈로그 사이트도, 컴포넌트 구현도, 배포 파이프라인도 실물입니다.
  3. MIT입니다. 애플리케이션 코드·커뮤니티 컴포넌트 코드·ThreeUI가 직접 만든 이미지까지 MIT. 상업용으로 그대로 써도 됩니다(단, 폰트와 원격 썸네일은 예외 — 아래 참조).

비슷한 것들과 무엇이 다른가

비교 대상성격ThreeUI와의 차이
shadcn/ui복사·붙여넣기 컴포넌트shadcn은 접근성·구조가 중심인 평면 UI. ThreeUI는 GPU로 그리는 시각 효과가 중심이라 겹치지 않는다
Magic UI · Aceternity UI화려한 모션 컴포넌트가장 가까운 경쟁. 다만 그쪽은 CSS·Framer Motion 위주고 ThreeUI는 Three.js/WebGL 실물 3D가 많다
React Three FiberThree.js의 React 바인딩R3F는 도구, ThreeUI는 완성품. 실제로 이 저장소는 R3F를 안 쓰고 Three.js를 직접 다룬다
Spline · Rive3D/모션 저작 도구(SaaS)그쪽은 에디터에서 만들어 런타임으로 삽입. ThreeUI는 소스 코드가 곧 결과물이라 수정 한계가 없다

다만 개인적으로 가장 값어치 있는 부분은 컴포넌트가 아닙니다. 오픈코어를 코드로 강제하는 방식입니다. 비슷한 처지의 프로젝트(유료 제품 + 오픈소스 판)는 많은데, 그 경계 관리를 이렇게 노골적으로 공개한 사례는 드뭅니다.

이 저장소의 진짜 셀링포인트
경계 관리를 전부 읽을 수 있다

scripts/ 안에 공개 경계 감사·빌드 감사·릴리스 등급 추론·설치 스모크 테스트가 전부 들어 있습니다. 합쳐서 1,639줄이고, 전부 의존성 없는 순수 Node 스크립트입니다. 내 프로젝트로 그대로 베껴 갈 수 있는 크기라는 게 핵심입니다.

3기술 스택 전체 지도

무엇으로 만들어졌고, 왜 그걸 골랐는가

프런트엔드

항목선택눈여겨볼 점
UIReact 19.2 (개발) / peer >=18 <20패키지 사용자는 React 18이든 19든 된다. 저장소 자체는 19로 개발
언어TypeScript 5.9, strict: truemoduleResolution: "Bundler", isolatedModules. 사이트 빌드는 noEmit, 라이브러리 빌드만 .d.ts를 뽑는다
번들러Vite 7.3설정 파일이 두 개다 — 사이트용·라이브러리용 (아래 4장)
라우팅없음. 직접 만든 src/routes.jsreact-router 같은 의존성 0. history.pushState + popstate로 200줄 안에 끝냈다
스타일순수 CSS 한 덩어리 (src/styles.css 91KB)Tailwind도 CSS-in-JS도 없다. 테마는 <html>data-theme·data-scheme·data-palette 속성으로만 갈린다
상태React 훅 + localStorage전역 상태 라이브러리 없음. 테마·조회수 모두 브라우저에만 저장

3D · 그래픽 — 여기가 특이하다

보통 프로젝트는 Three.js를 한 버전 씁니다. 이 저장소는 세 개가 동시에 살아 있습니다.

// package.json
"dependencies": {
  "three128": "npm:three@0.128.0",   ← 별명 three128 = Three.js 0.128
  "three165": "npm:three@0.165.0"    ← 별명 three165 = Three.js 0.165
},
"peerDependencies": {
  "three": ">=0.149 <1"              ← 쓰는 쪽이 준비하는 '기본' 버전
}

이걸 npm alias라고 부릅니다. 별명: "npm:패키지@버전" 문법으로 같은 패키지의 다른 버전을 다른 이름으로 동시에 설치합니다. 그러면 컴포넌트마다 자기가 검증된 버전을 골라 import할 수 있습니다.

// src/shaders/warp-field/warpFieldRenderer.ts
import * as THREE from "three128";   ← 0.128에서 동작이 검증된 장면
왜 이렇게까지

Three.js는 버전마다 미묘하게 달라지는 것으로 악명이 높습니다. 재질(material)의 기본 색 공간이 바뀌거나, 지오메트리 API 이름이 바뀌거나, 렌더러 옵션이 사라집니다. 몇 년에 걸쳐 만든 시각 효과 100개를 최신 버전 하나로 통일하려면 전부 다시 손봐야 합니다.

그래서 고치는 대신 얼려 버렸습니다. 2021년에 만든 장면은 0.128을 계속 쓰고, 최근 것은 0.165를 씁니다. "언젠가 마이그레이션한다"를 무한정 미루는 대신 미루는 것 자체를 설계로 못 박은 셈입니다.

셰이더 코드(GLSL)는 .glsl 파일이 아니라 TypeScript 템플릿 문자열로 들어 있습니다. 별도 로더 플러그인이 필요 없어지고, 타입스크립트가 그대로 export할 수 있습니다.

// src/shaders/bell-field/bellFieldShaders.ts — 실제 코드 발췌
export const BELL_FIELD_FRAGMENT_SHADER = `
  precision highp float;
  uniform vec2 u_resolution;
  uniform float u_time;
  uniform vec2 u_mouse;

  // 원형 진동 모드의 베셀 포락선을 대신하는 감쇠 코사인
  float bess(float x) { return cos(x - 0.785398) / sqrt(1.0 + abs(x)); }
  ...
`;

인프라 · 배포

항목내용
CIGitHub Actions 3종 — ci.yml(빌드+설치 스모크) · community-sync-pr.yml(동기화 PR 자동 개설) · publish-npm.yml(npm 발행)
Node24 고정 (actions/setup-node@v7)
npm 발행Trusted Publishing + provenance. id-token: write 권한으로 OIDC 토큰을 받아 발행 — npm 토큰을 저장소 시크릿에 두지 않는다
발행 조건package.json·lock·CLI package.json이 바뀐 push만. 게다가 npm view같은 버전이 이미 있으면 건너뛴다
배포 패키지2개 — @designcodeio/threeui(React 컴포넌트) · @designcodeio/threeui-cli(유료 소스 설치기)
분석Google Analytics(gtag)가 index.html에 그대로 박혀 있음 — 포크할 때 지워야 한다
용어
npm provenance (출처 증명)
패키지가 어떤 저장소의 어떤 커밋에서, 어떤 CI 워크플로로 만들어졌는지를 서명해 npm에 함께 올리는 기능. 사용자는 npm 페이지에서 이 증명을 확인할 수 있습니다. 개발자 노트북에서 몰래 올린 게 아니라는 걸 증명하므로, 공급망 공격(패키지 탈취) 대응책으로 씁니다. 함께 쓰는 Trusted Publishing은 아예 npm 토큰 자체를 없애는 방식입니다.

4아키텍처 심화 분석

비공개 본체 → 공개 저장소 → 두 갈래 배포

전체 그림

┌──────────────────────────────────────────────────────────────────────┐ │ 비공개 저장소 (ThreeUI 본체) │ │ 컴포넌트 263개 = Community 102 + Pro 57묶음 + Beta 25묶음 │ └───────────────────────────────┬──────────────────────────────────────┘ │ main 브랜치에 push 성공할 때마다 ▼ scripts/sync-community-from-main.mjs (702줄) ┌───────────────────────────────────┐ │ ① Pro·Beta 걸러내기 │ │ ② 소스 문자열 잘라내기(sanitize) │ │ ③ 제한된 폰트 자산 제거 │ │ ④ 무료 메타·옵션은 전부 보존 │ │ ⑤ 실패하면 아무것도 안 쓴다 │ └───────────────┬───────────────────┘ ▼ ┌────────────────────────────────────────────────┐ │ 공개 저장소 (여기) automation/community-sync │ │ · src/data/shaders.tsx 15,661줄 [생성물] │ │ · src/index.ts [생성물] │ │ · src/package-components/* 100개 [생성물] │ │ · public/source-code.json 32.5MB [생성물] │ │ · public/community-sync-report.json [증거] │ └───────────────┬────────────────────────────────┘ │ 자동 PR 1건 → 사람이 리뷰 → merge ▼ npm run build = test → build:site → build:lib │ │ │ │ │ └─ lib-dist/ (npm 패키지) │ └─ dist/ (카탈로그 사이트) └─ 경계 테스트가 하나라도 깨지면 여기서 멈춤 ▼ ┌────────────────────────┐ ┌─────────────────────────────┐ │ 무료 경로 │ │ 유료 경로 │ │ npm i @designcodeio/ │ │ npx @designcodeio/threeui- │ │ threeui │ │ cli add cross-beam │ │ → 소스가 npm에 있다 │ │ → OAuth 로그인 후 서버에서 │ └────────────────────────┘ │ 내려받는다 (npm엔 없음) │ └─────────────────────────────┘

오른쪽 두 갈래가 이 설계의 핵심입니다. 무료 컴포넌트는 npm에 통째로 넣고, 유료 컴포넌트는 npm에 아예 올리지 않습니다. 유료 쪽은 CLI가 브라우저 로그인을 거쳐 요청할 때마다 서버가 권한을 확인하고 내려줍니다. npm에 올려 두고 라이선스 키로 잠그는 방식은 애초에 성립하지 않는다는 판단입니다 — npm에 올라간 코드는 누구나 받을 수 있으니까요.

설계 패턴 ① 생성물 저장소 — "손으로 고치지 마시오"

이 저장소의 알맹이는 사람이 쓴 게 아니라 스크립트가 뱉은 것입니다.

// src/index.ts 첫 줄
// Generated by scripts/generate-library-entry.mjs. Do not edit by hand.

// src/data/mainCatalogSummary.ts
// Updated by scripts/sync-community-from-main.mjs.
export const FULL_THREEUI_COLLECTION_COUNT = 263;

generate-library-entry.mjs가 하는 일이 재밌습니다. 카탈로그 파일 shaders.tsx정규식으로 훑어 lazy(() => import("../shaders/...")) 패턴을 전부 뽑아낸 다음, 컴포넌트 하나당 파일 하나씩 src/package-components/*.ts 100개를 기존 것을 지우고 새로 씁니다. 그리고 이름이 겹치면 그 자리에서 에러를 던집니다.

// scripts/generate-library-entry.mjs 요지
const lazyPattern = /import\("(\.\.\/shaders\/[^"]+)"\)\.then\(...module\.(\w+)...\)/g;
const names = new Set();
for (const item of exports) {
  if (names.has(item.name)) throw new Error(`Duplicate Community export: ${item.name}`);
  names.add(item.name);
}
왜 이렇게

컴포넌트가 100개면 배럴 파일(export를 모아 놓은 index)을 손으로 유지하는 건 지는 싸움입니다. 하나 추가할 때마다 index.ts·subpath export·타입 선언 세 군데를 같이 고쳐야 하고, 언젠가 반드시 하나를 빠뜨립니다.

ThreeUI는 그 세 군데를 카탈로그 파일 하나에서 파생시킵니다. 카탈로그가 진실이고 나머지는 그림자입니다. 그림자를 손으로 고치면 다음 생성 때 조용히 사라집니다.

설계 패턴 ② 경계를 문서가 아니라 테스트로 못 박기

감사 스크립트가 두 겹입니다.

audit-public.mjs — 소스 트리 검사 (32줄). 저장소 전체를 걸어 다니며 다음이 하나라도 있으면 빌드를 실패시킵니다.

audit-build.mjs — 빌드 결과물 검사 (43줄). 이쪽이 더 흥미롭습니다. dist/의 모든 텍스트 파일을 하나로 이어 붙인 뒤 금지 패턴 목록을 돌립니다.

// scripts/audit-build.mjs — 금지 시그니처 (일부)
/@supabase\//        인증 백엔드가 딸려 나왔나
/supabase\.auth/     /createClient\s*\(/
/AuthProvider/       /AccountButton/     /OAuthConsent/
/create-checkout-session/i    /stripe-webhook/i   결제 런타임
/VITE_SUPABASE/      환경변수 이름조차 금지
/threeui\.netlify\.app/       비공개 호스트 주소
/sf-(?:bold|light|medium|regular|semibold)\.woff2/  재배포 불가 폰트
+ 소스맵(.map) 한 개라도 있으면 실패

여기서 감사 대상이 "소스"가 아니라 "빌드 결과물"이라는 게 핵심입니다. 소스만 보면 조건부 import나 동적 참조로 딸려 들어온 코드를 놓칩니다. 번들러가 실제로 묶어 낸 것을 검사하면 그 구멍이 막힙니다.

세 번째 겹은 public-boundary.test.mjs(232줄)입니다. 이건 Vite를 테스트 안에서 직접 띄워(createServer + ssrLoadModule) 카탈로그 모듈을 실제로 불러온 뒤, 동기화 리포트의 숫자와 대조합니다.

// 카탈로그 실물과 리포트가 한 글자도 어긋나면 안 된다
assert.equal(visible.length,   report.communityParents);
assert.equal(allRoutes.length, report.communityRoutes);
assert.ok(report.excludedProParents  > 0);  ← 걸러낸 게 0이면 그것도 이상
assert.ok(report.excludedBetaParents > 0);
유료 표식 필드가 남아 있으면 실패
assert.ok(allRoutes.every(s => s.status === undefined && s.access === undefined));
배울 점
"걸러낸 개수가 0이면 실패" — 반대 방향 검사

보통은 "나오면 안 되는 게 없는지"만 봅니다. 이 테스트는 "걸러낸 게 있기는 한지"도 확인합니다. 필터가 통째로 고장 나서 아무것도 안 걸러도, 앞의 검사만으로는 통과할 수 있기 때문입니다. 필터를 테스트할 땐 필터가 죽은 경우를 반드시 넣어야 합니다.

설계 패턴 ③ "표면 토큰"으로 버전 등급을 자동 추론

prepare-community-release.mjs는 이번 동기화가 patch냐 minor냐 major냐를 사람에게 묻지 않고 계산합니다. 방법은 단순합니다 — 공개된 것들을 문자열 토큰 집합으로 펼친 뒤, 이전 것과 차집합을 냅니다.

// 컴포넌트 하나가 이런 토큰들로 펼쳐진다
component:liquid-metal-button
variant:liquid-metal-button:circle
control:liquid-metal-button:diameter
variant-control:liquid-metal-button:circle:strokeWidth

// 그리고 판정
removed.length ? "major"      뭔가 사라졌다 = 깨는 변경
  : added.length ? "minor"    뭔가 늘었다  = 기능 추가
  : "patch";                  겉모습은 그대로 = 내부 수정

여기에 안전장치가 하나 더 있습니다. 사람이 --type patch추론값보다 낮은 등급을 강제하면 에러를 던집니다. 올려 잡는 건 되고 내려 잡는 건 안 됩니다.

용어
시맨틱 버저닝 (semver) — major / minor / patch
1.4.2 형태에서 앞부터 major·minor·patch. major는 "쓰던 코드가 깨질 수 있음", minor는 "기능이 늘었지만 기존 건 그대로", patch는 "내부만 고침"입니다. 라이브러리 사용자는 이 숫자만 보고 업데이트 위험도를 판단하므로, 잘못 매기면 남의 빌드를 깹니다. ThreeUI는 이 판단을 사람 손에서 뺏어 왔습니다.

설계 패턴 ④ 렌더 경로가 두 개다

컴포넌트를 화면에 그리는 방식이 두 갈래로 갈립니다.

경로 A — React가 직접 그린다 (14개 파일이 ShaderMaterial 사용) ┌─────────────┐ useRef + useEffect ┌──────────────┐ │ React 컴포넌트│ ──────────────────────▶ │ canvas + WebGL│ └─────────────┘ THREE.WebGLRenderer └──────────────┘ · props → uniform 값으로 직접 전달 · 언마운트 시 geometry.dispose() / material.dispose() 경로 B — 완성된 HTML 문서를 통째로 삼킨다 (14개 파일이 srcDoc 사용) ┌──────────────────────────┐ │ import src from │ ← 원본 .html 67개를 문자열로 읽어 들임 │ "./sources/x.html?raw" │ └───────────┬──────────────┘ │ .replace()로 문자열을 갈아끼워 변형(variant)을 만든다 ▼ ┌──────────────────────────┐ postMessage ┌───────────────────┐ │ <iframe srcDoc={source}> │ ◀────────────▶ │ 안에서 도는 원본 │ └──────────────────────────┘ 양방향 브리지 │ 스크립트 │ └───────────────────┘

경로 B는 처음 보면 이상해 보입니다. React 프로젝트에서 왜 통짜 HTML을 iframe에 넣을까요?

왜 iframe인가

랜딩페이지처럼 자기만의 CSS 리셋·전역 스타일·스크롤 로직을 가진 완성품을 React 트리 안에 그냥 넣으면 카탈로그 사이트의 스타일과 서로 잡아먹습니다. 반대로 React용으로 다시 쓰면 "원본 그대로"가 아니게 됩니다.

iframe은 스타일과 스크립트가 격리된 상자입니다. 원본을 한 글자도 안 고치고 넣을 수 있습니다. 대신 바깥에서 값을 바꾸려면 postMessage로 말을 걸어야 하고, ThreeUI는 컴포넌트마다 그 다리(bridge) 스크립트를 문자열로 덧붙여 넣습니다.

// src/shaders/liquid-metal-button/LiquidMetalButton.tsx — 실제 방식
import source from "./liquid-metal-button.html?raw";

// 원본 HTML의 </body> 앞에 통신용 스크립트를 끼워 넣는다
source.replace("</body>", BRIDGE_SCRIPT + "</body>")

// 원형 변형은 CSS만 덧붙이고 body에 표식을 단다
     .replace("<head>", CIRCLE_STYLE + "</head>")
     .replace("<body>", '<body data-shape="circle">')

변형(variant)을 만드는 방법이 "다시 구현"이 아니라 "문자열 치환"입니다. 원본이 언제나 정본으로 남는다는 게 장점이고, 원본의 한 줄만 바뀌어도 치환이 조용히 실패한다는 게 단점입니다.

두 갈래 Vite 빌드

사이트 빌드 (vite.config.js)라이브러리 빌드 (vite.lib.config.js)
목적카탈로그 웹사이트npm 패키지
출력dist/lib-dist/
진입점index.html 1개index.ts + 컴포넌트별 100개
외부 처리없음 (전부 번들)react·react-dom·three·three128·three165 전부 external
모듈 형태청크로 합침preserveModules: true파일 구조를 그대로 유지
소스맵끔 (감사가 .map 존재를 실패로 처리)

preserveModules와 컴포넌트별 진입점이 짝을 이루면 서브패스 import가 가능해집니다.

// 전체를 긁어오는 방식 — 개발 중 import 그래프가 커진다
import { AtTheHorizon } from "@designcodeio/threeui";

// 필요한 하나만 — 이쪽이 훨씬 가볍다
import { AtTheHorizon } from "@designcodeio/threeui/components/AtTheHorizon";

5디렉토리 구조 해부

402개 추적 파일이 어디에 어떻게 놓여 있나
threeui/ ├── index.html 사이트 진입점. 테마 FOUC 방지 인라인 스크립트 + GA + OG 메타 ├── package.json @designcodeio/threeui v1.0.0 · three128/three165 별칭 ├── vite.config.js 사이트 빌드 ├── vite.lib.config.js 라이브러리 빌드 (preserveModules) ├── tsconfig.json strict · noEmit ├── tsconfig.lib.json .d.ts만 뽑는 별도 설정 │ ├── src/ 9.6MB │ ├── App.tsx 13KB. 라우팅·페이지 전환·캡처 모드까지 여기 한 파일 │ ├── routes.js 자체 라우터 (경로 ↔ 컴포넌트/변형 매핑) │ ├── seo.js 10KB. 경로마다 title·description·canonical을 직접 갱신 │ ├── theme.ts 3모드(light/dark/system) × 5팔레트 │ ├── styles.css 91KB 단일 스타일시트 │ ├── shaderMetrics.ts 조회·복사 횟수 (localStorage 전용, 서버 전송 없음) │ │ │ ├── data/ │ │ ├── shaders.tsx 412KB · 15,661줄 ★카탈로그 정본 [생성물] │ │ ├── publicShaders.ts 변형을 접어 '보이는 목록'을 만든다 │ │ ├── catalogResults.ts 검색 매칭 로직 (조사·공백 무시 정규화 포함) │ │ └── mainCatalogSummary.ts FULL_THREEUI_COLLECTION_COUNT = 263 [생성물] │ │ │ ├── shaders/ 45개 디렉토리 8.3MB │ │ ├── bell-field/bellFieldShaders.ts GLSL을 TS 템플릿 문자열로 │ │ ├── warp-field/warpFieldRenderer.ts 22KB. three128 직접 사용 │ │ ├── neuform-isolated/ 58KB+41KB. 가장 큰 묶음 │ │ │ └── sources/*.html 원본 문서 67개 (?raw로 읽힘) │ │ └── community.css 컴포넌트 전용 스타일 │ │ │ ├── components/ 카탈로그 UI (사이드바·검색·문서·FPS 미터 등 19개) │ ├── package-components/ 100개 [전부 생성물] 한 줄짜리 재export 파일 │ └── index.ts 배럴 파일 [생성물] │ ├── packages/cli/ 별도 npm 패키지 (404줄) │ ├── bin/threeui.mjs 8줄 진입점 │ ├── src/oauth.mjs 219줄. PKCE·동적 클라이언트 등록·토큰 갱신 │ ├── src/install.mjs 87줄. 경로 순회 방어 + 덮어쓰기 거부 │ └── test/cli.test.mjs │ ├── scripts/ 1,639줄, 의존성 0의 순수 Node │ ├── sync-community-from-main.mjs 702줄 ★동기화 본체 │ ├── public-boundary.test.mjs 232줄 경계 검증 │ ├── community-sanitizers.mjs 91줄 소스 잘라내기 + 자체 검증 │ ├── prepare-community-release.mjs 100줄 semver 자동 추론 │ ├── audit-public.mjs 32줄 소스 트리 감사 │ ├── audit-build.mjs 43줄 빌드 결과물 감사 │ ├── generate-library-entry.mjs 48줄 배럴·서브패스 생성 │ └── package-install-smoke.mjs 50줄 진짜로 설치되는지 확인 │ ├── public/ 78MB │ ├── source-code.json 32.5MB '소스 보기' 탭 데이터 [생성물] │ ├── community-sync-report.json 20KB 동기화 증거 [생성물] │ └── landing-pages/, sketchbook/ 컴포넌트가 쓰는 실제 이미지·런타임 │ └── .github/workflows/ ci.yml · community-sync-pr.yml · publish-npm.yml

파일 크기가 말해 주는 것

파일크기의미
public/source-code.json32.5MB컴포넌트 소스를 통째로 담은 단일 JSON. 사이트의 '소스 보기' 탭이 이걸 읽는다. 클론 용량의 최대 원인
src/data/shaders.tsx412KB15,661줄. 102개 경로의 메타·컨트롤·변형이 전부 여기 하드코딩
src/styles.css91KBCSS 프레임워크 없이 순수 CSS로 카탈로그 전체를 감당
buildSkillMarkdown.js178KB컴포넌트 설명을 AI 에이전트용 마크다운으로 뽑는 파일. 사이트에 /mcp 문서 페이지가 따로 있다
주의
README의 숫자가 이미 실제와 어긋나 있다

README는 "커뮤니티 부모 컴포넌트 50개, 경로 111개, 무료 변형 141개 + 단독 23개 = 164개"라고 적고 있습니다. 그런데 같은 커밋의 public/community-sync-report.json43 / 102 / 138이라고 말합니다.

리포트가 맞습니다 — 그건 스크립트가 만든 것이고, README는 사람이 쓴 것이기 때문입니다. 실제 카탈로그를 세어 봐도 경로 102개, 그중 variantOf가 붙은 파생 항목이 59개입니다. 이 저장소를 읽을 땐 README보다 생성된 리포트를 믿는 게 안전합니다.

6학습 포인트 (기술별)

이 저장소에서 실제로 가져갈 수 있는 것들
패키지 관리

같은 라이브러리 여러 버전 동거시키기

"three128": "npm:three@0.128.0" 하나면 끝입니다. 마이그레이션이 막힌 레거시 모듈이 있을 때, 전부 멈추고 올리는 대신 옛 버전에 별명을 붙여 격리하는 선택지가 생깁니다.

대가는 번들 크기입니다. Three.js는 압축해도 수백 KB이고, 세 버전을 동시에 쓰는 앱은 세 벌을 받습니다. "쓰는 컴포넌트가 어느 버전을 참조하는지"를 확인하고 고르는 습관이 필요합니다.

빌드 도구

Vite 라이브러리 모드 + 서브패스 export

preserveModules: true + entry에 컴포넌트별 진입점 + package.jsonexports"./components/*" 와일드카드. 이 셋이 한 세트입니다. 컴포넌트가 많은 라이브러리를 만든다면 그대로 베껴 쓸 수 있는 구성입니다.

"sideEffects": ["**/*.css"]도 놓치기 쉬운 부분입니다 — 이게 없으면 번들러가 CSS import를 "안 쓰는 코드"로 보고 지워 버립니다.

프런트엔드

iframe srcDoc + postMessage 샌드박스

외부에서 받은 완성 HTML을 내 스타일과 충돌 없이 화면에 넣어야 할 때 쓰는 방법입니다. 미리보기 기능, 이메일 템플릿 편집기, 사용자 제작 위젯 같은 데 그대로 적용됩니다.

ThreeUI가 잘한 점은 통신 브리지를 원본에 심지 않고 문자열로 덧붙인다는 것입니다. 원본 파일은 계속 "그냥 열면 동작하는 HTML"로 남습니다.

그래픽

GLSL 프래그먼트 셰이더 읽는 법

bellFieldShaders.ts는 입문용으로 좋은 크기입니다. 흐름이 늘 같습니다 — gl_FragCoord(현재 픽셀 좌표)를 화면 크기로 나눠 −1~1 범위로 정규화하고, 화면비를 보정하고, u_time으로 시간에 따라 흔들고, 최종 색을 gl_FragColor에 넣습니다.

주석도 배울 거리입니다 — "원형 진동 모드의 베셀 포락선을 대신하는 감쇠 코사인"처럼 수학적 근사의 의도를 남겨 두는 방식입니다.

그래픽

Three.js 파티클을 새로 만들지 않고 재활용하기

warpFieldRenderer.ts의 워프 효과는 선 수천 개를 매 프레임 새로 만들지 않습니다. Float32Array 하나를 잡아 두고 z좌표만 밀다가, 카메라를 지나치면 뒤쪽으로 되돌려 놓습니다.

positions[i*6 + 2] += step;
if (positions[i*6 + 2] > RECYCLE_Z) {        카메라를 지나쳤으면
  positions[i*6 + 2] = RESET_Z;              맨 뒤로 되돌린다
}
attribute.needsUpdate = true;                GPU에 갱신 신호

객체 풀링(object pooling)의 교과서적 사례입니다. 자바스크립트에서 매 프레임 객체를 만들면 가비지 컬렉터가 주기적으로 프레임을 끊습니다.

프런트엔드

테마 깜빡임(FOUC) 없애기

index.html<head> 안, React가 로드되기 전에 인라인 스크립트가 localStorage를 읽어 <html>data-theme을 붙입니다. try/catch로 감싸 스토리지가 막힌 환경(시크릿 모드 등)에서도 기본값으로 떨어집니다.

다크 모드를 넣을 때 "새로고침하면 흰 화면이 번쩍한다"의 정석 해법입니다.

보안

CLI가 남의 파일시스템을 다룰 때의 방어선

install.mjssafeRelativePath()는 서버가 준 파일 경로를 믿지 않습니다. 널 바이트·절대 경로·역슬래시·.·..·빈 세그먼트를 전부 거부하고, 그러고도 resolve() 결과가 목적지 밖을 가리키면 한 번 더 막습니다.

덮어쓰기도 기본 거부입니다 — 기존 파일이 있고 내용이 다르면 --force 없이는 진행하지 않습니다. 남의 작업 디렉토리에 파일을 쓰는 도구는 "요청받은 경로"를 절대 그대로 믿으면 안 됩니다.

인증

CLI용 OAuth — PKCE + 로컬 콜백

oauth.mjs는 CLI 로그인의 표준 형태를 그대로 보여 줍니다. 임의 문자열(verifier)을 만들고 그 SHA-256 해시(challenge)를 인증 요청에 실은 뒤, 토큰을 받을 때 원본을 제시해 중간에 인증 코드를 가로챈 사람이 쓰지 못하게 합니다.

세션 파일 저장도 눈여겨볼 만합니다 — 디렉토리는 0700, 파일은 0600, 그리고 임시 파일에 쓴 뒤 rename으로 갈아끼웁니다(원자적 쓰기). 도중에 프로세스가 죽어도 반쯤 쓰인 세션 파일이 남지 않습니다.

7하드웨어 / 시스템 요구사항

돌려 보기 전에 알아 둘 것
항목요구사항
Node.js24 권장 (CI 기준). CLI는 engines: node >=20
디스크클론 141MB (얕은 클론 기준). public/ 78MB + src/ 9.6MB. npm installnode_modules가 더 붙는다
GPUWebGL2를 지원하는 브라우저. 통합 그래픽으로도 대부분 돌지만 파티클 많은 장면은 프레임이 떨어진다
브라우저ES 모듈 + <iframe srcDoc>. 사실상 최신 Chrome/Safari/Firefox/Edge
서버불필요. 정적 사이트라 빌드 후 아무 데나 올리면 된다
모바일돌긴 하는데 발열·배터리를 각오해야 한다. 셰이더 배경은 화면 전체를 매 프레임 다시 칠한다

사이트에는 PreviewFpsMeter라는 작은 컴포넌트가 붙어 있어 미리보기의 실제 프레임을 재고 등급을 매깁니다 — 55fps 이상 good · 45 이상 watch · 그 아래 slow. 컴포넌트를 고르기 전에 내 기기에서 몇 fps인지 확인할 수 있다는 뜻입니다.

함정
npm 패키지로 설치할 때 자산이 따라오지 않는다

랜딩페이지처럼 완성된 HTML 문서를 렌더하는 컴포넌트는, 그 안에서 이미지·폰트·Three.js 런타임을 루트 상대 경로로 찾습니다. npm 설치만 하고 쓰면 그 파일들이 없어서 화면이 비거나 깨집니다.

해결은 README에 적힌 대로 node_modules/@designcodeio/threeui/lib-dist/assets/에서 필요한 파일을 내 앱의 public 디렉토리로 복사하거나, 컴포넌트의 sourceUrl·assetBaseUrl prop을 넘겨 경로를 바꾸는 것입니다.

함정
MIT라고 전부 가져다 써도 되는 건 아니다

MIT는 애플리케이션 코드·커뮤니티 컴포넌트 코드·ThreeUI가 직접 만든 이미지까지입니다. 나머지는 다릅니다.

번들된 오픈 폰트는 SIL Open Font License 1.1이고, https://threeui.com에서 불러오는 썸네일·미리보기 이미지는 이 저장소가 재배포하지 않습니다 — 즉 그 이미지는 라이선스가 별개입니다. 상업 프로젝트에 쓸 거면 ASSET-LICENSES.md · FONT-LICENSES.md · THIRD_PARTY_NOTICES.md 세 파일을 먼저 읽으세요.

함정
공개된 건 전체의 일부다

본체 카탈로그는 263개인데 여기 공개된 건 커뮤니티 102개 경로입니다. Pro 57묶음·Beta 25묶음은 코드가 아예 들어 있지 않습니다. 사이트의 Get Pro 링크는 유료 페이지로 나갑니다.

또한 포크해서 유료 부분을 되살릴 수는 없습니다 — 잘려 나간 게 아니라 처음부터 복사되지 않았기 때문입니다.

함정
포크하면 남의 Google Analytics로 데이터가 간다

index.html 맨 위에 gtag 스크립트가 측정 ID G-5WP9FS3ECK와 함께 그대로 들어 있습니다. 지우지 않고 배포하면 내 사이트의 방문 기록이 원저자 계정으로 흘러갑니다. 포크 후 첫 작업으로 지우세요.

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

난이도별 5개 — 위에서 아래로 갈수록 어렵다
과제 1

카탈로그를 띄우고 컨트롤을 만져 본다난이도 ★☆☆☆☆ · 20분

git clone --depth 1 https://github.com/MengTo/threeui.git
cd threeui && npm install && npm run dev

확인할 것 — 컴포넌트 하나를 골라 컨트롤 슬라이더를 움직였을 때 FPS 미터가 어떻게 변하는지, 테마를 5가지 팔레트로 돌렸을 때 <html> 태그의 속성이 어떻게 바뀌는지(개발자 도구 Elements 탭).

배우는 것 — 이 프로젝트가 CSS 변수와 데이터 속성만으로 테마를 갈아끼우는 방식.

과제 2

내 Next.js/Vite 프로젝트에 컴포넌트 하나 붙이기난이도 ★★☆☆☆ · 40분

npm install @designcodeio/threeui three react react-dom
import { BellFieldBackground } from "@designcodeio/threeui/components/BellFieldBackground";
import "@designcodeio/threeui/style.css";

부딪힐 것 — Next.js App Router라면 브라우저 API를 쓰므로 클라이언트 컴포넌트로 만들어야 합니다. 자산이 필요한 컴포넌트를 고르면 7장의 함정을 그대로 만납니다. 일부러 그런 컴포넌트를 하나 골라 깨뜨려 보는 게 이 과제의 목적입니다.

확인할 것 — 전체 import와 서브패스 import로 각각 빌드했을 때 번들 크기 차이.

과제 3

셰이더에 uniform을 하나 더 만들어 새 변형 추가난이도 ★★★☆☆ · 1~2시간

src/shaders/bell-field/bellFieldShaders.tsuniform float u_warp;를 추가하고 좌표 계산에 섞어 넣습니다. 그리고 BellFieldBackground.tsx에서 prop → uniform으로 연결합니다.

주의src/data/shaders.tsx에 컨트롤을 추가하고 싶어질 텐데, 그 파일은 생성물입니다. 로컬 실험은 되지만 업스트림에 올릴 방법은 없습니다. "생성물 저장소에서는 기여 경로가 다르다"를 몸으로 배우는 과제이기도 합니다.

과제 4

내 라이브러리에 "배럴 파일 자동 생성"을 이식한다난이도 ★★★☆☆ · 반나절

scripts/generate-library-entry.mjs(48줄)를 읽고, 내 프로젝트의 컴포넌트 디렉토리를 훑어 index.ts와 컴포넌트별 진입점을 생성하는 스크립트를 씁니다. 여기에 이름 중복 시 에러"Do not edit by hand" 헤더를 반드시 넣으세요.

확인할 것package.jsonexports에 와일드카드 서브패스를 열고, 실제로 import X from "내패키지/components/X"가 타입까지 붙어 동작하는지.

과제 5

"공개 경계 감사"를 내 저장소에 적용한다난이도 ★★★★☆ · 하루

이 저장소에서 가장 값어치 있는 부분을 옮겨 오는 과제입니다. audit-public.mjsaudit-build.mjs를 참고해, 내 프로젝트에서 절대 공개되면 안 되는 것들의 목록을 정규식으로 적고 CI에 겁니다.

핵심 — 소스가 아니라 빌드 결과물을 검사할 것. 그리고 public-boundary.test.mjs처럼 "걸러낸 개수가 0이면 실패"도 함께 넣을 것.

보너스

릴리스 등급 자동 추론기 붙이기난이도 ★★★★☆ · 하루

prepare-community-release.mjsclassifyRelease()를 가져와, 내 라이브러리의 공개 API 표면(export 이름·함수 시그니처·설정 키)을 토큰 집합으로 뽑고 이전 릴리스와 비교합니다. 사라진 게 있으면 major, 늘어난 게 있으면 minor.

"버전을 몇 번째 자리에서 올릴까"를 사람 기억이 아니라 코드가 답하게 만드는 것 — 라이브러리를 오래 유지할수록 이 자동화의 값어치가 커집니다.

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

4주 코스 — 이 저장소를 교재로 삼는다면
주차주제이 저장소에서 읽을 곳바깥 교재
1주차 WebGL·셰이더 기초
좌표 정규화 · uniform · 시간 애니메이션
bell-field/bellFieldShaders.tsdot-matrix/ribbon-field/ 순으로 짧은 것부터 The Book of Shaders (한국어판 있음)
2주차 Three.js 장면 구성
지오메트리 · 재질 · 버퍼 · dispose
warp-field/warpFieldRenderer.ts(22KB, 파티클 재활용) · landscape/ · globe/ Three.js Journey · 공식 매뉴얼의 "Memory 관리" 절
3주차 라이브러리 패키징
Vite lib 모드 · exports 필드 · .d.ts · peerDependencies
vite.lib.config.js · tsconfig.lib.json · package.jsonexports · generate-library-entry.mjs Node.js 공식 Packages 문서의 exports 절
4주차 공급망·릴리스 자동화
경계 감사 · provenance · semver 추론
scripts/audit-*.mjs · public-boundary.test.mjs · prepare-community-release.mjs · .github/workflows/publish-npm.yml npm Provenance 문서 · semver.org 한국어

넷 중 하나만 고른다면 4주차를 권합니다. 1~3주차는 대체 교재가 많지만, "유료 제품에서 무료 판을 안전하게 떼어내는 실물 파이프라인"은 공개된 사례 자체가 드뭅니다.

10핵심 키워드 사전

이 문서에 나온 용어 정리
용어
셰이더 / GLSLGPU가 픽셀마다 돌리는 작은 프로그램과 그 언어. 웹에서는 WebGL을 통해 실행된다
uniform셰이더 프로그램 전체가 공유하는 바깥에서 넣어 주는 값. 시간·해상도·마우스 위치 등이 여기로 들어간다
Three.jsWebGL을 직접 다루기 어려워서 만든 3D 라이브러리. 버전 간 호환성 변화가 잦기로 유명하다
npm alias"별명": "npm:패키지@버전". 같은 패키지의 다른 버전을 다른 이름으로 동시에 설치하는 문법
peerDependencies"이건 내가 설치하지 않을 테니 쓰는 쪽에서 준비해 주세요"라는 선언. React처럼 앱에 하나만 있어야 하는 것에 쓴다
서브패스 export패키지/일부 형태로 필요한 부분만 import하게 열어 주는 package.json 설정
preserveModules번들할 때 파일을 하나로 합치지 않고 원래 파일 구조를 유지하는 Rollup/Vite 옵션. 트리 셰이킹에 유리하다
sideEffects"이 파일들은 import만 해도 의미가 있으니 지우지 마라"고 번들러에 알리는 필드. CSS import가 대표적
srcDociframe에 URL 대신 HTML 문자열을 직접 넣는 속성. 스타일·스크립트가 격리된다
postMessageiframe 안팎처럼 격리된 문맥끼리 메시지를 주고받는 브라우저 API
FOUCFlash of Unstyled Content. 스타일이 적용되기 전 화면이 잠깐 번쩍이는 현상
오픈코어핵심 일부만 오픈소스로 공개하고 나머지는 유료로 파는 사업 방식
semvermajor.minor.patch 세 자리 버전 규칙. major=깨짐, minor=기능 추가, patch=내부 수정
provenance패키지가 어느 저장소·커밋·CI에서 만들어졌는지 서명해 함께 공개하는 npm 기능
Trusted Publishingnpm 토큰 없이 CI의 OIDC 신원만으로 패키지를 발행하는 방식
PKCEProof Key for Code Exchange. 인증 코드를 가로채도 쓸 수 없게 만드는 OAuth 확장. CLI·모바일 앱의 표준
경로 순회 (path traversal)../../etc/passwd처럼 상위 디렉토리로 빠져나가 의도치 않은 파일에 접근하는 공격
객체 풀링객체를 매번 새로 만들지 않고 다 쓴 것을 되돌려 재사용하는 기법. 가비지 컬렉션으로 인한 끊김을 줄인다

11참고 링크

더 파고들 곳

이 프로젝트

저장소 안에서 먼저 읽을 파일

비교해 볼 만한 저장소

배경 지식