05부다브로트 — 발산하는 궤적을 축적해 떠오르는 유령 형상
발산하지 않는 점을 버리고 발산하는 점의 궤적만 캔버스에 쌓으면 명상하는 듯한 대칭 형상이 천천히 밝아오는데, 같은 식에서 어떤 점의 궤적을 남기느냐만 바꿔 전혀 다른 그림이 된다는 점을 보여줍니다.
쓰임아트 히어로, 전시형 배경
createImageDataglobalAlphaMath.random

<main class="stage">
<div class="mb is-demo" id="mbRoot">
<p class="mb__hint">
<span>발산하는 점의 궤적만 캔버스에 쌓으면 명상하는 듯한 대칭 형상이 천천히 떠오릅니다 — 같은 식에서 어떤 점의 궤적을 남기느냐만 바꾼 그림입니다</span>
</p>
<div class="mb__field" role="application" aria-label="부다브로트 실험판" tabindex="0">
<canvas class="mb__cv" id="mbCv"></canvas>
</div>
<div class="mb__bar">
<span class="mb__meta">프레임당 궤적 260개 · 누적 감마 0.94</span>
<span class="mb__meta mb__meta--dim">canvas · Math.random · requestAnimationFrame</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 부다브로트(05). @keyframes 없음 — 캔버스 rAF(발산 궤적 누적 → 감마 감쇠 → 온도곡선 렌더)가 만든다.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-cream;
// ── 편 공통 심야 프랙탈 팔레트 ──
$mb-stage: #1a1726;
@include stage($mb-stage);
$mb-ink: #14111f;
$mb-set: #241d38;
$mb-text: #cfc8e8;
.mb {
--mb-glow: #c9bdf2; // 궤적 빛 — 라벤더
--mb-warm: #efc0b5; // 밝은 중심 — 복숭아
--mb-ink: #{$mb-ink};
display: flex; flex-direction: column; gap: $sp-2;
width: 100%; max-width: 424px;
}
.mb__hint {
display: flex; align-items: center; gap: $sp-1; margin: 0;
font-weight: 700; font-size: clamp(10px, 2.8vw, 12px); line-height: 1.35;
color: $mb-text;
}
.mb__field {
position: relative; height: 180px;
border-radius: $r-card; overflow: hidden;
background: $mb-ink;
box-shadow: inset 0 0 0 1px rgba($mb-text, .18);
}
.mb__field:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
.mb__cv { display: block; width: 100%; height: 100%; }
.mb__bar { display: flex; align-items: baseline; justify-content: space-between; gap: $sp-2; }
.mb__meta { font-size: 10px; font-weight: 500; color: rgba($mb-text, .72); }
.mb__meta--dim { font-size: 10px; }
@media (max-width: 340px) {
.mb { gap: $sp-1; }
.mb__field { height: 100px; }
.mb__meta--dim { display: none; }
}
// reduce — 형상이 떠오른 직후 한 장을 고정.
@media (prefers-reduced-motion: reduce) {
.mb.is-still .mb__field { cursor: default !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #1a1726;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mb {
--mb-glow: #c9bdf2;
--mb-warm: #efc0b5;
--mb-ink: #14111f;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 424px;
}
.mb__hint {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 12px);
line-height: 1.35;
color: #cfc8e8;
}
.mb__field {
position: relative;
height: 180px;
border-radius: 16px;
overflow: hidden;
background: #14111f;
box-shadow: inset 0 0 0 1px rgba(207, 200, 232, 0.18);
}
.mb__field:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.mb__cv {
display: block;
width: 100%;
height: 100%;
}
.mb__bar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.mb__meta {
font-size: 10px;
font-weight: 500;
color: rgba(207, 200, 232, 0.72);
}
.mb__meta--dim {
font-size: 10px;
}
@media (max-width: 340px) {
.mb {
gap: 4px;
}
.mb__field {
height: 100px;
}
.mb__meta--dim {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.mb.is-still .mb__field {
cursor: default !important;
}
}// 부다브로트(05) — 탈출 시간의 거울상.
// 01 은 발산 횟수를 색으로 썼다. 여기선 발산하는 점을 골라 그 궤적이 지나간 모든 픽셀에
// 1 씩 더한다. 발산하지 않는 점(집합 내부)의 궤적은 버린다 — 이 한 줄 차이가 명치 모양을
// 명상 상 인체상으로 뒤집는다. 표준 부다브로트는 90도 회전한 뷰(세로 대칭 인체상)를 쓴다.
// 난수 표본은 시드 고정 LCG — 결정론 렌더(같은 프레임 시각 = 같은 누적 상태).
// 2초 루프: 누적 → 리셋 → 다시 누적. 감마 0.94 감쇠로 오래된 궤적이 서서히 잦아들게 하면
// 형상이 떠오르는 과정 자체가 매 루프 재현된다.
(function () {
// ── 변형 상수 블록 — 아래 값 하나만 바꿔 변형을 만든다 ──
var ORBITS = 260; // 프레임당 새로 뿌리는 궤적 수 — 밝아지는 속도
var MAXIT = 90; // 궤적 추적 반복 — 이 안에 발산해야 궤적을 남긴다
var DECAY = 0.94; // 프레임 감마 감쇠 — 오래된 궤적이 잦아드는 비율
// ────────────────────────────────────────────────────
var root = document.getElementById('mbRoot');
var cv = document.getElementById('mbCv');
var field = cv.parentElement;
var css = getComputedStyle(root);
function tokRGB(name) {
var s = css.getPropertyValue(name).trim();
var hx = s.length === 4
? s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2) + s.charAt(3) + s.charAt(3)
: s.slice(1);
return [parseInt(hx.slice(0, 2), 16), parseInt(hx.slice(2, 4), 16), parseInt(hx.slice(4, 6), 16)];
}
var C_GLOW = tokRGB('--mb-glow'); // 궤적 빛 — 라벤더
var C_WARM = tokRGB('--mb-warm'); // 밝은 중심 — 복숭아
var C_INK = tokRGB('--mb-ink'); // 바닥
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var raf = 0, t0 = -1;
var W = 0, H = 0, IRES = 200;
var ov, octx, ctx;
// 표본 영역 — 만델브로가 살고 있는 c 상자(약 -2.2~1.2 · ±1.4)
var RX0 = -2.2, RX1 = 1.2, RY0 = -1.4, RY1 = 1.4;
var acc; // 픽셀 누적 버퍼(Float32)
var rndState = 20260916;
function rnd() {
// mulberry32 — 시드 고정. Math.random 을 쓰면 렌더마다 다른 그림이 나온다
rndState |= 0; rndState = (rndState + 0x6D2B79F5) | 0;
var t = Math.imul(rndState ^ (rndState >>> 15), 1 | rndState);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
function fit() {
var r = field.getBoundingClientRect();
W = Math.round(r.width); H = Math.round(r.height);
cv.width = W; cv.height = H;
ctx = cv.getContext('2d');
ov = document.createElement('canvas');
ov.width = IRES; ov.height = Math.max(2, Math.round(IRES * H / W));
octx = ov.getContext('2d');
reset();
}
function reset() {
acc = new Float32Array(ov.width * ov.height);
}
// 궤적 하나 시도 — 발산하면 지나간 픽셀에 가중치를 남긴다
function traceOrbit() {
var cr = RX0 + rnd() * (RX1 - RX0);
var ci = RY0 + rnd() * (RY1 - RY0);
var zr = 0, zi = 0, n = 0;
// 먼저 발산 판정(궤적 저장 없이) — 발산하는 점만 다시 걸어 흔적을 남긴다
while (n < MAXIT && zr * zr + zi * zi <= 4) {
var t = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = t; n++;
}
if (n >= MAXIT) return; // 집합 내부 — 궤적을 남기지 않는다(부다브로트의 조건)
zr = 0; zi = 0; // 다시 출발해 흔적을 남긴다
var iw = ov.width, ih = ov.height;
for (var m = 0; m < n; m++) {
// 표준 부다브로트 뷰 — 만델브로를 90도 회전해 세로 대칭 인체상으로
var px = (zi - RX0) / (RX1 - RX0) * iw; // 허수축을 가로로(회전)
var py = (zr - RY0) / (RY1 - RY0) * ih; // 실수축을 세로로
var ix = px | 0, iy = py | 0;
if (ix >= 0 && ix < iw && iy >= 0 && iy < ih) {
acc[iy * iw + ix] += 1; // 방문 가중치(반복이 늦게까지 갈수록 1로 동일 — 긴 궤적이 더 밝다)
}
var t2 = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = t2;
}
}
function render() {
var iw = ov.width, ih = ov.height;
var img = octx.createImageData(iw, ih);
var px = img.data;
var k = 0;
// 누적값 로그 스케일 → 라벤더에서 복숭아로 옮는 온도곡선
for (var q = 0; q < acc.length; q++) {
var v = Math.log(1 + acc[q]) / Math.log(1 + 30); // 0~1(30 방문이면 포화)
v = v > 1 ? 1 : v;
var r = C_INK[0] + (C_WARM[0] - C_GLOW[0]) * v + (C_GLOW[0] - C_INK[0]) * Math.min(1, v * 1.8);
var g = C_INK[1] + (C_WARM[1] - C_GLOW[1]) * v + (C_GLOW[1] - C_INK[1]) * Math.min(1, v * 1.8);
var b = C_INK[2] + (C_WARM[2] - C_GLOW[2]) * v + (C_GLOW[2] - C_INK[2]) * Math.min(1, v * 1.8);
px[k++] = r; px[k++] = g; px[k++] = b; px[k++] = 255;
}
octx.putImageData(img, 0, 0);
// 밝은 궤적 위 얇은 광채 겹 — globalAlpha 로 온도곡선 하이라이트를 살짝 덧칠한다
ctx.globalAlpha = 1;
ctx.drawImage(ov, 0, 0, W, H);
ctx.globalAlpha = 0.14;
ctx.globalCompositeOperation = 'lighter';
ctx.drawImage(ov, 0, 0, W, H); // 같은 프레임 두 번 — 밝은 곳만 더 밝게(가산)
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
}
var steps = 0;
function frame(now) {
if (t0 < 0) t0 = now;
var loop = ((now - t0) / 1000) % 2.0;
var target = Math.floor(loop * 30); // 초당 30 스텝 기준 누적 목표
while (steps < target) {
for (var o = 0; o < ORBITS; o++) traceOrbit();
steps++;
}
if (target < steps - 3) { reset(); steps = 0; } // 루프 리셋 — 되감아 다시 떠오른다
// 감쇠 — 오래된 궤적이 잦아들며 형상이 계속 다듬어진다
if (steps % 4 === 0) for (var q = 0; q < acc.length; q++) acc[q] *= DECAY;
render();
raf = requestAnimationFrame(frame);
}
window.addEventListener('resize', function () { fit(); for (var s = 0; s < 60; s++) for (var o = 0; o < ORBITS; o++) traceOrbit(); render(); });
fit();
if (reduce) {
// reduce — 40 스텝만 쌓은 한 장을 고정(형상이 떠오른 직후)
for (var s = 0; s < 40; s++) for (var o = 0; o < ORBITS; o++) traceOrbit();
render();
root.classList.add('is-still');
} else {
raf = requestAnimationFrame(frame);
}
})();// BuddhabrotGhost — vanilla 판(run/323/demo/05-mb-buddhabrot-ghost/index.html) 의 React 포트.
// 탈출 시간의 거울상. 01 은 발산 횟수를 색으로 썼다. 여기선 발산하는 점을 골라 그 궤적이
// 지나간 모든 픽셀에 1 씩 더한다. 발산하지 않는 점(집합 내부)의 궤적은 버린다 — 이 한 줄
// 차이가 명치 모양을 명상 상 인체상으로 뒤집는다. 표준 부다브로트는 90도 회전한 뷰(세로 대칭
// 인체상)를 쓴다. 난수 표본은 시드 고정 LCG — 결정론 렌더(같은 프레임 시각 = 같은 누적 상태).
// 2초 루프: 누적 → 리셋 → 다시 누적. 감마 0.94 감쇠로 오래된 궤적이 서서히 잦아들게 하면
// 형상이 떠오르는 과정 자체가 매 루프 재현된다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./BuddhabrotGhost.module.scss";
export type BuddhabrotGhostProps = {
duration?: string;
easing?: string;
color?: string;
orbits?: number; // 프레임당 새로 뿌리는 궤적 수 — 변형 "궤적 수"가 260 → 600
};
// ── 변형 상수(기본값) — vanilla 판과 동일 ──
const MAXIT = 90; // 궤적 추적 반복 — 이 안에 발산해야 궤적을 남긴다
const DECAY = 0.94; // 프레임 감마 감쇠 — 오래된 궤적이 잦아드는 비율
const IRES = 200; // 내부 계산 해상도(가로 픽셀)
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Box<T> = { current: T };
type RGB = [number, number, number];
const FIELD_LABEL = "부다브로트 실험판";
const META_LEFT = "프레임당 궤적 260개 · 누적 감마 0.94";
const META_RIGHT = "canvas · Math.random · requestAnimationFrame";
const HINT = "발산하는 점의 궤적만 캔버스에 쌓으면 명상하는 듯한 대칭 형상이 천천히 떠오릅니다 — 같은 식에서 어떤 점의 궤적을 남기느냐만 바꾼 그림입니다";
// 표본 영역 — 만델브로가 살고 있는 c 상자(약 -2.2~1.2 · ±1.4)
const RX0 = -2.2, RX1 = 1.2, RY0 = -1.4, RY1 = 1.4;
// mulberry32 — 시드 고정. Math.random 을 쓰면 렌더마다 다른 그림이 나온다
function mulberry32(seed: number): () => number {
let s = seed | 0;
return function (): number {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export default function BuddhabrotGhost({
duration = "2s",
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#c9bdf2",
orbits = 260
}: BuddhabrotGhostProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const fieldRef = useRef<HTMLDivElement | null>(null);
const cvRef = useRef<HTMLCanvasElement | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const ovRef = useRef<HTMLCanvasElement | null>(null);
const octxRef = useRef<CanvasRenderingContext2D | null>(null);
const colRef = useRef<{ glow: RGB; warm: RGB; ink: RGB } | null>(null);
const accRef = useRef<Float32Array | null>(null);
const rndRef = useRef<() => number>(mulberry32(20260916));
const orbitsRef = useRef(orbits) as Box<number>;
const wRef = useRef(0) as Box<number>;
const hRef = useRef(0) as Box<number>;
const rafRef = useRef(0) as Box<number>;
const stepsRef = useRef(0) as Box<number>;
const [still, setStill] = useState(false);
const vars: Vars = { "--duration": duration, "--easing": easing, "--color": color };
function reduced(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function tokRGB(root: HTMLDivElement, name: string): RGB {
const s = getComputedStyle(root).getPropertyValue(name).trim();
const hx = s.length === 4
? s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2) + s.charAt(3) + s.charAt(3)
: s.slice(1);
return [parseInt(hx.slice(0, 2), 16), parseInt(hx.slice(2, 4), 16), parseInt(hx.slice(4, 6), 16)];
}
function fit(): void {
const field = fieldRef.current;
const cv = cvRef.current;
if (!field || !cv) return;
const r = field.getBoundingClientRect();
const W = Math.round(r.width), H = Math.round(r.height);
wRef.current = W; hRef.current = H;
cv.width = W; cv.height = H;
ctxRef.current = cv.getContext("2d");
const ov = document.createElement("canvas");
ov.width = IRES;
ov.height = Math.max(2, Math.round(IRES * H / W));
ovRef.current = ov;
octxRef.current = ov.getContext("2d");
reset();
}
function reset(): void {
const ov = ovRef.current;
if (!ov) return;
accRef.current = new Float32Array(ov.width * ov.height);
}
// 궤적 하나 시도 — 발산하면 지나간 픽셀에 가중치를 남긴다
function traceOrbit(): void {
const ov = ovRef.current;
const acc = accRef.current;
const rnd = rndRef.current;
if (!ov || !acc || !rnd) return;
const cr = RX0 + rnd() * (RX1 - RX0);
const ci = RY0 + rnd() * (RY1 - RY0);
let zr = 0, zi = 0, n = 0;
// 먼저 발산 판정(궤적 저장 없이) — 발산하는 점만 다시 걸어 흔적을 남긴다
while (n < MAXIT && zr * zr + zi * zi <= 4) {
const t = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = t; n++;
}
if (n >= MAXIT) return; // 집합 내부 — 궤적을 남기지 않는다(부다브로트의 조건)
zr = 0; zi = 0; // 다시 출발해 흔적을 남긴다
const iw = ov.width, ih = ov.height;
for (let m = 0; m < n; m++) {
// 표준 부다브로트 뷰 — 만델브로를 90도 회전해 세로 대칭 인체상으로
const px = (zi - RX0) / (RX1 - RX0) * iw; // 허수축을 가로로(회전)
const py = (zr - RY0) / (RY1 - RY0) * ih; // 실수축을 세로로
const ix = px | 0, iy = py | 0;
if (ix >= 0 && ix < iw && iy >= 0 && iy < ih) {
acc[iy * iw + ix] += 1; // 방문 가중치(긴 궤적이 더 밝다)
}
const t2 = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = t2;
}
}
function render(): void {
const octx = octxRef.current;
const ov = ovRef.current;
const ctx = ctxRef.current;
const acc = accRef.current;
const col = colRef.current;
const W = wRef.current, H = hRef.current;
if (!octx || !ov || !ctx || !acc || !col) return;
const img = octx.createImageData(ov.width, ov.height);
const px = img.data;
let k = 0;
// 누적값 로그 스케일 → 라벤더에서 복숭아로 옮는 온도곡선
for (let q = 0; q < acc.length; q++) {
let v = Math.log(1 + acc[q]) / Math.log(1 + 30); // 0~1(30 방문이면 포화)
if (v > 1) v = 1;
const r = col.ink[0] + (col.warm[0] - col.glow[0]) * v + (col.glow[0] - col.ink[0]) * Math.min(1, v * 1.8);
const g = col.ink[1] + (col.warm[1] - col.glow[1]) * v + (col.glow[1] - col.ink[1]) * Math.min(1, v * 1.8);
const b = col.ink[2] + (col.warm[2] - col.glow[2]) * v + (col.glow[2] - col.ink[2]) * Math.min(1, v * 1.8);
px[k++] = r; px[k++] = g; px[k++] = b; px[k++] = 255;
}
octx.putImageData(img, 0, 0);
// 밝은 궤적 위 얇은 광채 겹 — globalAlpha 로 온도곡선 하이라이트를 살짝 덧칠한다
ctx.globalAlpha = 1;
ctx.drawImage(ov, 0, 0, W, H);
ctx.globalAlpha = 0.14;
ctx.globalCompositeOperation = "lighter";
ctx.drawImage(ov, 0, 0, W, H); // 같은 프레임 두 번 — 밝은 곳만 더 밝게(가산)
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
}
function sprinkle(n: number): void {
for (let s = 0; s < n; s++) {
for (let o = 0; o < orbitsRef.current; o++) traceOrbit();
}
}
let t0 = -1;
function frame(now: number): void {
if (t0 < 0) t0 = now;
const loop = ((now - t0) / 1000) % 2.0;
const target = Math.floor(loop * 30); // 초당 30 스텝 기준 누적 목표
while (stepsRef.current < target) {
sprinkle(1);
stepsRef.current++;
}
if (target < stepsRef.current - 3) { reset(); stepsRef.current = 0; } // 루프 리셋 — 되감아 다시 떠오른다
// 감쇠 — 오래된 궤적이 잦아들며 형상이 계속 다듬어진다
const acc = accRef.current;
if (acc && stepsRef.current % 4 === 0) for (let q = 0; q < acc.length; q++) acc[q] *= DECAY;
render();
rafRef.current = requestAnimationFrame(frame);
}
useEffect(() => {
const root = rootRef.current;
if (!root) return;
orbitsRef.current = orbits;
colRef.current = {
glow: tokRGB(root, "--mb-glow"), // 궤적 빛 — 라벤더
warm: tokRGB(root, "--mb-warm"), // 밝은 중심 — 복숭아
ink: tokRGB(root, "--mb-ink"), // 바닥
};
fit();
stepsRef.current = 0;
rndRef.current = mulberry32(20260916);
function onResize(): void {
fit(); stepsRef.current = 0; rndRef.current = mulberry32(20260916);
sprinkle(60); render();
}
window.addEventListener("resize", onResize);
if (reduced()) {
// reduce — 40 스텝만 쌓은 한 장을 고정(형상이 떠오른 직후)
sprinkle(40);
render();
setStill(true);
} else {
t0 = -1;
rafRef.current = requestAnimationFrame(frame);
}
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(rafRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [orbits]);
const rootCls = still ? `${styles.root} ${styles.isStill}` : styles.root;
return (
<main className={styles.stage}>
<div className={rootCls} ref={rootRef} style={vars}>
<p className={styles.hint}>
<span>{HINT}</span>
</p>
<div
className={styles.field}
ref={fieldRef}
role="application"
aria-label={FIELD_LABEL}
tabIndex={0}
>
<canvas className={styles.cv} ref={cvRef}></canvas>
</div>
<div className={styles.bar}>
<span className={styles.meta}>{META_LEFT}</span>
<span className={styles.metaDim}>{META_RIGHT}</span>
</div>
</div>
</main>
);
}05 부다브로트 — 발산하는 궤적을 축적해 떠오르는 유령 형상 — 측정 누적면적 45.133% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 만델브로 집합 9종 — 무한 확대 줌·줄리아·버닝쉽을 복소평면 한 픽셀씩 계산하는 캔버스
이 데모가 속한 글
만델브로 집합 9종 — 무한 확대 줌·줄리아·버닝쉽을 복소평면 한 픽셀씩 계산하는 캔버스z = z² + c 한 줄을 픽셀마다 반복해 발산 횟수를 세면 명치 끝에서 바깥으로 뻗는 만델브로 집합이 나오는데, 확대를 거듭해도 새로운 미니 만델브로가 끝없이 나오는 무한 줌을 라이브러리 없이 복붙으로 가져갈 수 이 글의 데모 전부 보기


