Frontned Development/FE 회고

Konva vs Fabric vs Canvas API 라이브러리 선택과정

데비시 2023. 7. 20. 10:28

Drawing 기능 +@를 구현하기 위해서 canvas 라이브러리를 선택하는 과정에 고민이 생겼다.

이 기능을 제공하는 여러 라이브러리가 있지만, 리액트와 충돌 이슈가 있는 라이브러리를 모두 제외하니 Konva, Fabric, 자체 구현 세 가지 방식이 남았다. 이 중 자체 구현은 기능이 확장 될 때마다 새로 만드는 부분에서 시간적인 효율이 나오지 않는다 생각하여 제외했다.

결론을 먼저 말하면, Konva를 선택하였다.

(이후 나올 모든 그림의 순서는 canvas - konva - fabric 순입니다.)

 

1. 성능 비교

 

lighthouse 렌더링 성능 비교

최적화된 페이지에서 실험한 결과
최적화가 안된 페이지에서 실험

  • lightHouse 성능은 다 고만고만하다. fabric이 미세하게 빠르다.
  • 최적화가 안되어있을 때는 canvas 라이브러리들을 사용하면 SI가 굉장히 느리게 나온다. 특히, Fabric이 느리고 내장 canvas API가 빨랐다.

 

Perfomance 비교

실험과정
performance 성능

  • 모든 과정은 해당 네모 그리기를 하였으며 일부 오차가 있을 수 있지만 테이프로 모니터에 표시하고 동일한 영역을 수행.
  • 퍼센트를 비교했을 때, Fabric이 두 배 가량 빠르지만, 절대값으로 생각하면 모든 라이브러리가 압도적으로 빠르다.

체감 성능

  • 셋 다 비슷하다. fabric.js ≥ konva ≥ canvas API

 

2. 생태계 및 공식문서

npm trends 그래프

  • 1번 그림처럼 Konva의 생태계가 훨씬 크며, fabric을 2년 전부터 압도하였다. 개인적으로 커뮤니티를 돌아다니며 찾은 이유는 아래와 같다.
    • react, svelte, vue에 알맞게 계속 라이브러리 및 문서의 업데이트 (특히, react 생태계가 커짐)
    • 반응형, 이벤트 버블링 등 각종 편의 기능 지원
    • 잘 만들어진 공식 문서와 객체 지향 API
    • 스트레스 부하 테스트를 통한 안정성 검증

konva 공식문서의 내용

  • 물론 Fabric도 공식문서가 잘 되어있으며, react를 사용할 수 있게 업데이트가 진행되고 있다. 단지, Konva가 더 잘 했을 뿐이다.

 

3. 가독성 / 난이도

https://dev.to/lico/react-comparison-of-js-canvas-libraries-konvajs-vs-fabricjs-1dan

이 분이 쓴 글을 보면 더 다양한 비교를 알 수 있는 데, 가독성과 난이도 자체는 fabric.js가 쉽고 좋아보인다.

// konva free drawing

import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import Konva from "konva";

export default function KonvaCanvas() {
  // konva
  const stageRef = useRef<Konva.Stage | null>(null);
  const layerRef = useRef<Konva.Layer | null>(null);
  const lastLineRef = useRef<Konva.Line | null>(null);

  const [isPainting, setIsPainting] = useState(false);
  const [lineColor, setLineColor] = useState<string>("#f80000");
  const [lineWidth, setLineWidth] = useState<number>(10);

  //  function
  const startPaint = () => {
    if (!stageRef.current || !layerRef.current) return;

    setIsPainting(true);
    const pos = stageRef.current.getPointerPosition();
    if (pos) {
      const newLine = new Konva.Line({
        stroke: lineColor,
        strokeWidth: lineWidth,
        globalCompositeOperation: "source-over",
        lineCap: "round",
        lineJoin: "round",
        points: [pos.x, pos.y, pos.x, pos.y],
      });
      layerRef.current.add(newLine);
      lastLineRef.current = newLine;
    }
  };

  const paint = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    if (!isPainting || !stageRef.current || !lastLineRef.current) return;

    e.preventDefault();
    e.stopPropagation();
    const pos = stageRef.current.getPointerPosition();
    if (pos) {
      const newPoints = lastLineRef.current.points().concat([pos.x, pos.y]);
      lastLineRef.current.points(newPoints);
      layerRef.current?.batchDraw();
    }
  };

  const stopPaint = () => {
    setIsPainting(false);
  };

  useEffect(() => {
    const stage = new Konva.Stage({
      container: "mainCanvas",
      width: window.innerWidth,
      height: window.innerHeight,
    });
    const layer = new Konva.Layer();
    stage.add(layer);
    stageRef.current = stage;
    layerRef.current = layer;
  }, []);

  return (
    <Container>
      <div
        id="mainCanvas"
        onMouseDown={() => startPaint()}
        onMouseMove={(e) => paint(e)}
        onMouseUp={() => stopPaint()}
      />
    </Container>
  );
}
// fabric

import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { fabric } from "fabric";

export default function FabricCanvas() {
  const fabricRef = useRef<any>(null);

  const [lineColor, setLineColor] = useState<string>("#f80000");
  const [lineWidth, setLineWidth] = useState<number>(10);

  useEffect(() => {
    const canvas = new fabric.Canvas("c", {
      isDrawingMode: true,
    });
    fabricRef.current = canvas;

    // canvas setting
    canvas.setWidth(window.innerWidth);
    canvas.setHeight(window.innerHeight);
    fabric.Object.prototype.cornerStyle = "circle";
    canvas.freeDrawingBrush = new fabric["PencilBrush"](canvas);

    if (canvas.freeDrawingBrush) {
      const brush = canvas.freeDrawingBrush;
      brush.color = lineColor;
      brush.width = lineWidth;
    }

    return () => {
      canvas.dispose();
    };
  }, []);

  return (
    <Container>
      <div>
        <canvas id="c" ref={fabricRef} />
      </div>
    </Container>
  );
}

 

4. bundle size (konva + react-konva vs fabric)

konva , react-konva, fabric

  • 번들 사이즈는 konva가 이겼다.

 

결론

  • 성능 차이는 절대값에서 크지 않았기 때문에, 성능은 예외로 하였다. (사실, 내가 아직 성능에 대해서 판단할만큼 수준이 높지 않아서도 맞는 거 같다..)
  • 안정성(부하테스트)과 번들 사이즈에서 konva에 가산점을 부여했다.
  • react-konva와 같이 react에 초점을 맞춘 라이브러리가 추가되면서, 장기적인 관점에서 추가 가산점이 들어갔다.
  • 단순히 freedrawing 기능 구현에만 초점을 맞추면 fabric이 쉽고 낫다. 하지만, 기능의 확장, 안정성 등 장기적인 관점에서 konva가 유리할 것으로 생각된다. 우리 프로젝트는 기획이 확장될 가능성들이 있기 때문에 Konva를 채택했다