Tips & Tricks (업데이트: 2026. 6. 2.)

Claude Code로 Canvas 개발하기: HiDPI, RAF, Pointer 이벤트, 검증

Claude Code로 안정적인 Canvas UI를 만드는 방법을 HiDPI, RAF, Pointer Events, 상태 관리, Playwright 검증까지 설명합니다.

Claude Code로 Canvas 개발하기: HiDPI, RAF, Pointer 이벤트, 검증

Canvas는 데모보다 검증이 중요하다

Canvas는 브라우저 안에서 선, 이미지, 입자, 차트, 게임 화면을 직접 그리는 저수준 렌더링 표면입니다. 자유도가 높은 만큼, 일반 DOM처럼 레이아웃과 상태가 자동으로 관리되지 않습니다. Claude Code에 “멋진 Canvas 애니메이션을 만들어줘”라고만 요청하면 노트북에서는 좋아 보이지만 스마트폰에서는 흐릿하거나, 터치가 되지 않거나, 가로 스크롤이 생기는 데모가 나오기 쉽습니다.

실무에서 중요한 것은 “그려졌는가”가 아니라 “깨지지 않고 계속 유지되는가”입니다. HiDPI 화면에서 선이 선명한지, requestAnimationFrame 루프가 중복 실행되지 않는지, mouse/touch/pen 입력을 함께 처리하는지, 모바일에서 CTA와 광고 영역을 밀어내지 않는지 확인해야 합니다.

함께 보면 좋은 글은 Claude Code 애니메이션 구현 가이드, Claude Code Three.js 3D 가이드, Claude Code 데이터 시각화 가이드입니다. 공식 문서는 Claude Code Docs, MDN Canvas API, requestAnimationFrame, Pointer Events, Playwright screenshots를 참고하세요.

Claude Code에 줄 프롬프트

HiDPI는 고밀도 디스플레이를 뜻합니다. CSS상으로는 1픽셀이어도 실제 디스플레이에서는 여러 물리 픽셀로 표현될 수 있습니다. Canvas 내부 크기를 CSS 크기와 맞추지 않으면 브라우저가 낮은 해상도 이미지를 확대해 선이 흐려집니다.

Canvas 2D 데모를 구현해 주세요.
조건:
- CSS 픽셀과 Canvas 내부 픽셀을 분리하고 devicePixelRatio를 반영한다
- requestAnimationFrame으로 렌더링하고 dt는 최대값을 제한한다
- Pointer Events로 mouse, touch, pen 입력을 통합한다
- 상태는 하나의 state 객체에 두고 render(ctx)는 그리기만 담당한다
- ResizeObserver로 컨테이너 크기 변경을 따라간다
- 375px 모바일 폭에서 가로 스크롤이 없어야 한다
- Playwright로 표시 여부, 비어 있지 않은 픽셀, 스크린샷, 모바일 폭을 검증한다
- 변경 파일, 위험, 실패 사례, 수동 확인 항목을 정리한다

이 프롬프트의 목적은 화려한 효과 하나가 아니라 재사용 가능한 렌더링 구조를 만드는 것입니다. Claude Code가 기존 CSS, 컴포넌트, 테스트 명령을 함께 보게 하면 불필요한 리디자인도 줄어듭니다.

구조를 먼저 나눈다

Canvas 코드는 입력, 상태 변경, 시간 업데이트, 렌더링, 검증을 분리할수록 안전합니다.

Pointer Events
      |
      v
  input handler  --->  state update  --->  update(dt)
                                         |
ResizeObserver ---> resize(dpr)          v
                                     render(ctx)
                                         |
                                         v
                                Playwright checks

render(ctx) 안에서 이벤트를 등록하거나 DOM을 바꾸거나 새 애니메이션 루프를 시작하면 나중에 추적하기 어렵습니다. 상태를 읽고 그리기만 하도록 제한하면 undo, eraser, WebGL fallback, 스크린샷 테스트를 추가하기 쉽습니다.

실무 유스케이스

첫 번째는 데이터 시각화입니다. 일반 차트 라이브러리로 표현하기 어려운 실시간 궤적, 밀집 산점도, 오디오 파형, 지도 위 움직임은 Canvas가 잘 맞습니다. 다만 글 본문, 광고, CTA 근처에 들어가는 경우가 많으므로 로딩 상태와 모바일 레이아웃까지 확인해야 합니다.

두 번째는 이미지 주석 도구입니다. 스크린샷 리뷰, 강의 피드백, 디자인 QA에서는 선, 사각형, 화살표, 라벨, undo가 필요합니다. Pointer Events를 쓰면 마우스, 손가락, 스타일러스 입력을 같은 경로로 처리할 수 있고, 지원 기기에서는 pressure로 선 굵기도 조절할 수 있습니다.

세 번째는 교육용 인터랙션과 가벼운 게임입니다. 물리 시뮬레이션, 타이핑 훈련, 단어 카드, 입자 효과는 매 프레임 상태를 업데이트하는 방식이 자연스럽습니다. 하지만 화면을 떠난 뒤에도 requestAnimationFrame이 남아 있으면 배터리와 CPU를 낭비합니다.

네 번째는 제품 페이지의 프리뷰입니다. 색상 비교, 드래그 가능한 설명, 구매 전 시뮬레이션은 전환율에 도움을 줄 수 있습니다. 대신 Canvas가 CTA를 아래로 밀거나 모바일에서 조작을 방해하면 역효과입니다.

실행 가능한 예제

아래 예제는 HTML 파일로 저장해 바로 열 수 있습니다. ctx.setTransform을 사용해 리사이즈 때마다 변환을 덮어쓰는 점이 핵심입니다.

<style>
  body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #111827; }
  canvas { width: min(100%, 720px); aspect-ratio: 16 / 9; display: block; background: #020617; border: 1px solid #374151; border-radius: 8px; touch-action: none; }
</style>
<canvas id="demo" aria-label="Canvas particle demo"></canvas>
<script type="module">
  const canvas = document.querySelector("#demo");
  const ctx = canvas.getContext("2d");
  const state = { width: 1, height: 1, dpr: 1, last: 0, pointer: { x: 0, y: 0, down: false }, dots: [] };

  function resize() {
    const rect = canvas.getBoundingClientRect();
    state.width = Math.max(1, rect.width);
    state.height = Math.max(1, rect.height);
    state.dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(state.width * state.dpr);
    canvas.height = Math.round(state.height * state.dpr);
    ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
  }

  function point(event) {
    const rect = canvas.getBoundingClientRect();
    return { x: event.clientX - rect.left, y: event.clientY - rect.top, pressure: event.pressure || 0.5 };
  }

  function emit(x, y, pressure = 0.5) {
    for (let i = 0; i < 8; i += 1) {
      const angle = Math.random() * Math.PI * 2;
      const speed = 90 + Math.random() * 180;
      state.dots.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, size: 4 + pressure * 8 });
    }
    state.dots = state.dots.slice(-360);
  }

  canvas.addEventListener("pointerdown", (event) => {
    canvas.setPointerCapture(event.pointerId);
    const p = point(event);
    state.pointer = { x: p.x, y: p.y, down: true };
    emit(p.x, p.y, p.pressure);
  });
  canvas.addEventListener("pointermove", (event) => {
    const events = event.getCoalescedEvents ? event.getCoalescedEvents() : [event];
    for (const item of events) {
      const p = point(item);
      state.pointer.x = p.x;
      state.pointer.y = p.y;
      if (state.pointer.down) emit(p.x, p.y, p.pressure);
    }
  });
  canvas.addEventListener("pointerup", () => (state.pointer.down = false));
  canvas.addEventListener("pointercancel", () => (state.pointer.down = false));

  function frame(now) {
    const dt = state.last ? Math.min((now - state.last) / 1000, 0.033) : 0;
    state.last = now;
    for (const dot of state.dots) {
      dot.vy += 220 * dt;
      dot.x += dot.vx * dt;
      dot.y += dot.vy * dt;
      dot.life -= dt;
    }
    state.dots = state.dots.filter((dot) => dot.life > 0);
    ctx.clearRect(0, 0, state.width, state.height);
    ctx.fillStyle = "#020617";
    ctx.fillRect(0, 0, state.width, state.height);
    for (const dot of state.dots) {
      ctx.fillStyle = `rgba(56,189,248,${dot.life})`;
      ctx.beginPath();
      ctx.arc(dot.x, dot.y, dot.size * dot.life, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.fillStyle = "#e5e7eb";
    ctx.fillText(`dpr ${state.dpr.toFixed(2)} / dots ${state.dots.length}`, 16, 24);
    requestAnimationFrame(frame);
  }

  new ResizeObserver(resize).observe(canvas);
  resize();
  requestAnimationFrame(frame);
</script>

상태 관리와 모바일 붕괴

Canvas에는 DOM 노드처럼 “그려진 선”이 남아 있지 않습니다. 도구, 색상, 선 굵기, 점 목록, undo/redo를 JavaScript 상태로 들고 있어야 합니다. Claude Code에는 상태 업데이트 함수를 작게 만들고, 렌더 함수가 상태를 읽어 그리기만 하게 요청하세요.

모바일에서 자주 깨지는 부분은 고정 폭과 높이 누락입니다. width: 800px는 375px 화면에서 가로 스크롤을 만들고, 높이가 없는 부모 안의 Canvas는 거의 보이지 않을 수 있습니다. 코드 블록, 광고, 관련 글, CTA와 함께 실제 페이지 폭에서 확인해야 합니다.

Playwright 검증

Canvas는 픽셀 결과물이므로 DOM 검사만으로 충분하지 않습니다. 보이는지, 모바일 폭을 넘지 않는지, 실제로 픽셀이 칠해졌는지, 스크린샷이 안정적인지 확인합니다.

import { expect, test } from "@playwright/test";

test("canvas renders on mobile", async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto("/canvas-demo");
  const canvas = page.locator("canvas").first();
  await expect(canvas).toBeVisible();

  const box = await canvas.boundingBox();
  expect(box?.width ?? 0).toBeLessThanOrEqual(390);

  const paintedPixels = await canvas.evaluate((node) => {
    const context = node.getContext("2d");
    if (!context) return 0;
    const data = context.getImageData(0, 0, node.width, node.height).data;
    let painted = 0;
    for (let i = 3; i < data.length; i += 4) if (data[i] > 0) painted += 1;
    return painted;
  });

  expect(paintedPixels).toBeGreaterThan(1000);
  await expect(canvas).toHaveScreenshot("canvas-mobile.png", { maxDiffPixelRatio: 0.03 });
});

흔한 실패와 수익 연결

자주 보는 실패는 CSS 크기만 바꾸는 것, ctx.scale을 누적하는 것, mousemove만 쓰는 것, 애니메이션 루프를 정리하지 않는 것, 고정 폭 Canvas를 글 안에 넣는 것, 검은 배경만 있는 스크린샷을 통과로 보는 것입니다. Claude Code 리뷰 프롬프트에는 이 항목들을 명시적으로 넣어야 합니다.

Canvas는 인터랙티브 설명, 주석 도구, 제품 미리보기, 교육 콘텐츠처럼 정적 이미지보다 더 잘 설명할 때 수익 흐름을 돕습니다. 반대로 본문과 CTA를 가리면 손해입니다. 팀에서 이 과정을 표준화하려면 Claude Code 교육 및 상담에서 프롬프트, 구현 규칙, Playwright 검증을 함께 설계하는 편이 좋습니다.

이 흐름을 실제로 적용해 보니 가장 효과적인 단계는 구현 후 Claude Code에 “실패 사례만 리뷰해 달라”고 요청하는 것이었습니다. 고정 폭, ctx.scale 누적, 터치 입력 누락, 모바일 가로 스크롤이 빠르게 드러났고, 화려하지만 불안한 데모보다 게시 가능한 UI에 가까워졌습니다.

#Claude Code #Canvas #WebGL #그래픽 #TypeScript
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.