4P by GN⁺ | ★ favorite | 댓글 1개
  • Skia Canvas는 Node.js에서 HTML Canvas 드로잉 API를 온스크린·오프스크린 렌더링에 쓰게 해 주며, Google Skia 기반이라 Chrome <canvas>와 매우 비슷한 출력을 냄
  • 결과물은 PDF·SVG 같은 벡터와 JPEG·PNG·WEBP 같은 비트맵으로 내보낼 수 있고, 파일·dataURL·Buffer·Sharp 객체로 연결 가능함
  • 브라우저 Canvas 밖의 영역도 다루며, GUI Window 렌더링, 이벤트 프레임워크, 다중 페이지 Canvas, 경로 연산, 3D 원근 변환, CSS filter, 고급 타이포그래피 제어를 제공함
  • 비동기 렌더링과 파일 I/O는 사용자가 설정하는 워커 풀의 네이티브 스레드에서 처리되어, 서버 사이드 이미지 생성과 serverless 환경에 맞음
  • 벤치마크는 serial과 async 모드를 나눠 측정하며, async에서는 여러 반복 작업을 동시에 시작해 멀티스레딩 효과를 비교함

Node.js에서 쓰는 Canvas 구현

  • Skia Canvas는 Node.js용 HTML Canvas 드로잉 API 구현체로, 온스크린과 오프스크린 렌더링을 모두 지원함
  • Google Skia 그래픽 엔진을 사용해 Chrome <canvas> 요소와 매우 유사한 결과를 냄
  • 브라우저 Canvas가 아직 제공하지 못하는 기능까지 포함함

출력 형식과 내보내기 방식

  • 이미지 출력은 벡터 형식비트맵 형식을 모두 지원함
    • 벡터: PDF, SVG
    • 비트맵: JPEG, PNG, WEBP
  • Canvas 결과를 여러 목적지로 보낼 수 있음
  • 인터랙티브 GUI Window에 직접 그릴 수 있고, 브라우저와 유사한 이벤트 프레임워크를 제공함

비동기 렌더링과 다중 페이지

  • 렌더링과 파일 I/O는 네이티브 스레드를 사용하는 워커 풀에서 비동기로 실행됨
  • 하나의 Canvas에 여러 페이지를 만들 수 있음
    • 단일 다중 페이지 PDF로 출력 가능함
    • 여러 파일로 나뉘는 이미지 시퀀스로도 저장할 수 있음

브라우저 Canvas를 넘어서는 그래픽 기능

  • Bézier 경로는 불리언 연산과 점 단위 보간을 지원함
  • 변환 기능은 기본 Canvas의 스케일·회전·이동에 더해 3D 원근 변환을 제공함
  • 도형 채우기에는 비트맵 기반 Pattern뿐 아니라 벡터 기반 Texture를 사용할 수 있음
  • 선 그리기에는 커스텀 marker를 붙일 수 있음
  • 이미지 처리는 전체 CSS filter 연산자 세트를 지원함

타이포그래피 제어

  • 텍스트 렌더링은 여러 줄과 자동 줄바꿈을 처리함
  • 줄 단위 텍스트 메트릭을 제공함
  • small-caps, ligature 같은 OpenType 기능을 표준 font-variant 문법으로 사용할 수 있음
  • 글자 간격, 단어 간격, leading을 비례적으로 제어 가능함
  • variable fonts를 지원하고 weight 값을 투명하게 매핑함
  • 로컬 파일에서 로드한 비시스템 폰트도 사용할 수 있음

사용 예시

  • 이미지 파일 생성 예시는 Canvas를 만들고 getContext("2d")로 2D 컨텍스트를 얻은 뒤, 원뿔형 그라디언트를 stroke 스타일로 사용해 사각형을 그림
    • saveAs("rainbox.png", {density:2})로 고밀도 이미지를 저장함
    • canvas.pngcanvas.toBuffer("png")의 축약형으로 사용됨
    • toDataURL("png") 결과를 HTML 문자열 안에 넣을 수 있음
    • saveAsSync("rainbox.pdf")로 메인 스레드에서 동기 저장도 가능함
  • 다중 페이지 시퀀스 예시는 canvas.newPage()로 색상이 다른 페이지들을 만들고 저장함
    • all-pages.pdf로 다중 페이지 PDF를 저장함
    • page-{2}.png 패턴으로 page-01.png, page-02.png 같은 여러 파일을 만듦
  • Window 렌더링 예시는 Window(300, 300)을 만들고 draw 이벤트에서 프레임 값에 따라 선 두께가 바뀌는 원을 그림
  • Sharp.js 통합 예시는 Canvas 결과를 Sharp 객체로 변환해 후처리하거나, Sharp로 만든 이미지를 loadImage로 Canvas에 그리는 흐름을 보여줌

벤치마크 방식과 결과

  • 벤치마크는 Skia Canvas를 두 모드로 나눠 테스트함
    • serial: 각 렌더링 작업을 기다린 뒤 다음 반복으로 진행함
    • async: 모든 테스트 반복을 한 번에 시작하고 멀티스레딩 지원으로 병렬 실행함
  • 전체 결과는 canvas-benchmarks 결과 페이지에서 확인할 수 있음
  • 시작 지연 시간 테스트는 100회 반복 기준으로 skia-canvas가 회당 1ms 미만, 총 33ms를 기록함
    • canvaskit-wasm: 회당 25ms, 총 2.47초
    • canvas: 회당 88ms, 총 8.77초
    • @napi-rs/canvas: 회당 69ms, 총 6.87초
  • Bézier curves 테스트는 20회 반복 기준으로 async 모드의 병렬 처리 차이가 드러남
    • skia-canvas serial: 회당 137ms, 총 2.74초
    • skia-canvas async: 회당 28ms, 총 558ms
    • 비교 대상은 canvaskit-wasm 790ms, canvas 486ms, @napi-rs/canvas 230ms였음
  • SVG to PNG 테스트는 100회 반복 기준으로 canvaskit-wasm이 지원하지 않았고, skia-canvas는 serial 회당 58ms·async 회당 11ms를 기록함
    • skia-canvas serial: 총 5.83초
    • skia-canvas async: 총 1.08초
  • 이미지 스케일·회전 테스트 50회 반복 결과
    • skia-canvas serial: 회당 100ms, 총 5.00초
    • skia-canvas async: 회당 19ms, 총 935ms
  • 기본 텍스트 테스트 200회 반복 결과
    • skia-canvas serial: 회당 21ms, 총 4.26초
    • skia-canvas async: 회당 4ms, 총 819ms

댓글과 토론

Hacker News 의견들
  • https://windowjs.org도 매우 비슷한 개념임
    Skia를 감싸서 Canvas API로 노출하지만, Node 대신 v8을 내장해 아주 작은 런타임으로 동작함
    약 3년 전에 공개한 첫 오픈소스 프로젝트였고, WebGL·오디오 등을 노출해 데스크톱용 JavaScript 게임 플랫폼으로 만들 계획도 있었지만, 다른 일과 프로젝트 때문에 개발이 중단됨
    브라우저 밖에서도 Canvas를 쓸 수 있게 하는 이런 프로젝트가 나와서 반가움

    • 꽤 멋지지만, 격리가 꼭 필요한 게 아니라면 v8만 있는 환경은 아쉬움
      가끔은 그냥 네트워크 요청을 하고 싶을 때도 있음
    • 멋진 프로젝트임
      Node.js 대신 V8만 내장하면 바이너리 크기가 얼마나 줄어드는지 궁금함
      Node.js도 V8을 쓰는데, Node 바이너리 크기의 대부분이 V8이 아니라 런타임에서 오는 건지도 궁금함
      웹사이트와 GitHub 저장소를 둘러봤지만 Window.js 바이너리가 보통 몇 kB나 MB인지 찾지 못함
  • 호기심인데, 이런 라이브러리는 어디에 쓰는지 궁금함
    데스크톱이라면 도형을 그리는 데 더 나은 네이티브 라이브러리가 있지 않나 싶음

    • Soundslice에서는 프런트엔드 JavaScript/Canvas로 만든 맞춤형 악보 렌더링 그래픽 라이브러리를 사용함
      서버 쪽에서도 벡터 PDF를 생성해야 해서 HTML Canvas API를 쓰고 PDF를 만들 수 있는 Node 라이브러리를 씀
      이렇게 하면 웹 브라우저에서 렌더링된 악보와 결과가 정확히 같아져서 좋음
      결론적으로 이런 라이브러리는 브라우저가 아닌 환경에서도 코드 재사용을 가능하게 해줌
    • 2D 그리기에서 Skia만큼 강력하면서 흔히 “네이티브”라고 부를 만한 것은 사실상 많지 않음
      Skia의 큰 장점은 네이티브 그래픽을 효과적으로 활용하는 데 있고, 애초에 브라우저 엔진에서 쓰이는 이유도 그 때문임
      게다가 목표 API가 하나라는 것도 장점임
    • 이식성이 장점 아닌가? GPU 가속 2D 렌더링 라이브러리 중에 또 무엇이 있는지 궁금함
      한편으로는 이게 더 저수준이지만, 다른 한편으로는 Puppeteer를 2D 그래픽 라이브러리처럼 써본 적도 있음
      Canvas 2D, WebGL, WebGPU, HTML/CSS 전체를 얻고, 텍스트 효과·배경 이미지·CSS 변환도 쓸 수 있음
      이미지와 동영상 로딩도 가능하고, 스크린샷 기능으로 조합한 결과를 이미지로 만들 수 있음
      과하긴 하지만 내 용도에서는 상관없었고, 동작하며 크로스 플랫폼이고 문제를 해결해줌
    • Canvas보다 나은 2D 그래픽 그리기 API를 가진 플랫폼을 꼽을 수 있다면 놀랄 것 같음
      Skia는 Direct2D보다 약간 빠르고 Cairo보다 훨씬 빠름
      고도로 최적화되어 있고 GPU 가속 백엔드도 갖고 있음
    • 같은 코드를 여러 플랫폼에서 실행할 수 있다는 건 큰 보너스임
      Canvas는 매우 흔한 2D 그리기 API이기도 함
      iOS에도 네이티브 버전이 있고 Android에도 네이티브 버전이 있으며, 그리기 연산도 대체로 비슷함
      이를 활용하면 얻는 게 큼
  • Skia에는 Node를 지원하는 WASM 빌드인 CanvasKit이 포함되어 있는데 https://www.npmjs.com/package/canvaskit-wasm, 이 모듈은 Rust 바인딩인 듯함
    두 접근의 장단점이 궁금함

    • “Rust 바인딩”이라기보다는 미리 컴파일된 Node 바이너리에 가까움
      내부적으로 Rust/C++ 혼합 코드베이스에서 생성된 것일 뿐임
      그래서 핵심 차이는 WASM에는 별도 런타임이 필요하지만 Node 바이너리에는 필요 없다는 점임
  • 이건 Rust 크레이트를 감싼 래퍼 같은 건가?
    https://rust-skia.github.io/doc/skia_safe/canvas/struct.Canvas.html

    • Skia Canvas는 바인딩만 있으면 거의 모든 프로그래밍 언어에서 동작할 수 있다고 이해함 https://www.swig.org/
      Node에만 특별한 점은 잘 보이지 않음
    • 맞음, napi-rs로 생성됨
  • 이런 걸 오래 기다려왔음
    이전에 비슷한 일을 하려던 시도들은 Node-Gyp 설치가 필요했는데, 정말 악몽 같았음

  • 단순 렌더링 API 이상이네: “OS 네이티브 그래픽 파이프라인으로 창에 렌더링할 수 있고 브라우저 같은 UI 이벤트 프레임워크를 제공한다”는 내용이 있음
    WebGPU 지원을 위해 wgpu를, WebGL 지원을 위해 ANGLE을 추가할 수도 있겠음

    • Skia는 널리 쓰이는 라이브러리이고, 떠오르는 것만 해도 Chrome, Firefox, LibreOffice가 사용함
      WebGPU와 ANGLE을 포함해 많은 렌더링 대상을 지원함
    • Qt가 이미 제공하는 것과 꽤 비슷함
      QPainter는 원시 이미지와 창을 다루고, 이벤트 지원이 있는 더 정교한 위젯도 있음
      또 한동안 찾고 있던 기능인데, Qt에는 창보다 큰 그래픽 평면에서 수백만 개 객체를 해상도 독립적으로 렌더링하고 이벤트까지 처리할 수 있는 graphicsview가 있음
  • Node 호환 Canvas 구현에 관심 있다면 몇 가지가 있음
    Skia 프로젝트의 canvaskit-wasm은 GPU 가속은 아닌 것 같음: https://github.com/google/skia/tree/main/modules/canvaskit/npm_build
    @napi-rs/canvas는 가장 빠른 바인딩임: https://github.com/Brooooooklyn/canvas?tab=readme-ov-file#performance
    node-canvas는 Skia 대신 Cairo를 사용함: https://github.com/Automattic/node-canvas

  • Advent of Code에서 간단한 그래픽 창이 필요할 때 현재 Deno와 함께 쓰고 있음
    window-example도 있음: https://skia-canvas.org/#rendering-to-a-window
    deno add npm:skia-canvas로 추가하고, deno.json"nodeModulesDir": "auto"를 넣은 뒤 deno install --allow-scripts를 실행하면 됨

  • 이걸 보니 정말 기대됨
    독립 실행도 가능하면서 웹과 호환되는 그래픽을 훨씬 편하게 작성할 수 있게 해줄 것 같음
    첫 번째 스크립트 언어용 라이브러리를 컴파일하기 위해 두 번째 스크립트 언어로 소스에서 빌드하고 코드를 생성할 필요도 없어짐
    VS Code 같은 곳에서 Canvas 코드를 실시간 렌더링하거나, 웹 Canvas에서 돌리기 위한 변환 계층이 있는 코드를 실행하는 데 얼마나 잘 맞을지도 흥미로움