CustomPaint 위젯의 등장 배경

앞서 배운 RenderObjectWidget은 강력하지만 복잡합니다. 단순히 무언가를 그리고 싶을 때도 RenderObject를 상속하고, createRenderObject를 구현하고, paint 메서드를 오버라이드해야 합니다.

CustomPaint는 이런 복잡성을 해결합니다. 일급 함수로 paint 동작을 전달하면 끝!

왜 CustomPaint인가?

RenderObjectWidget의 복잡성을 피하고 그리기에만 집중할 수 있습니다

❌ RenderObject 방식

1. RenderObjectWidget 상속
2. createRenderObject() 구현
3. RenderBox 클래스 작성
4. paint() 메서드 오버라이드
5. 레이아웃 로직 구현
총 50+ 줄의 보일러플레이트 코드! 😫

✅ CustomPaint 방식

CustomPaint({
painter: (canvas, size) => {
// 그리기 코드
}
})
단 5줄로 끝! 🎉

💡 핵심 아이디어

CustomPaint는 내부적으로 RenderObject를 생성하고 관리합니다. 개발자는 그리기 로직만 함수로 전달하면 되므로, 복잡한 클래스 구조를 이해할 필요가 없습니다.

CustomPaint의 장점

  1. 간단한 API: painter 함수만 전달하면 됨
  2. 빠른 프로토타이핑: 복잡한 클래스 구조 불필요
  3. 재사용성: painter 함수를 쉽게 교체 가능
  4. 성능: 내부적으로 최적화된 RenderObject 사용

CustomPaint 사용하기

CustomPaint 기본 사용법

painter 속성에 함수를 전달하여 커스텀 그리기를 구현합니다

Canvas API 사용법

canvas: {
  paint: (context, size) => {
    const ctx = context.canvas;
    
    // 색상 설정
    ctx.fillStyle = '#3B82F6';
    ctx.strokeStyle = '#FF0000';
    
    // 선 설정
    ctx.lineWidth = 2;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'miter';
    
    // 그리기
    ctx.fillRect(x, y, w, h);
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();
  }
}

최적화 팁

// shouldRepaint 사용
CustomPaint({
  painter: {
    dependencies: myData,
    shouldRepaint: (oldPainter) => {
      // 데이터가 변경되었을 때만 다시 그림
      return oldPainter.dependencies !== myData;
    },
    canvas: {
      paint: (context, size) => {
        // 그리기 로직
      }
    }
  }
})

기본 사용법

// SVG 렌더러용
CustomPaint({
  size: new Size(300, 200),
  painter: {
    svg: {
      createDefaultSvgEl(context) {
        return {
          circle: context.createSvgEl('circle')
        };
      },
      paint(els, size) {
        els.circle.setAttribute('cx', `${size.width / 2}`);
        els.circle.setAttribute('cy', `${size.height / 2}`);
        els.circle.setAttribute('r', '50');
        els.circle.setAttribute('fill', '#3B82F6');
      }
    }
  }
})

// Canvas 렌더러용
CustomPaint({
  size: new Size(300, 200),
  painter: {
    canvas: {
      paint(context, size) {
        const ctx = context.canvas;
        ctx.fillStyle = '#3B82F6';
        ctx.beginPath();
        ctx.arc(size.width / 2, size.height / 2, 50, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }
})

shouldRepaint 최적화

성능을 위해 언제 다시 그려야 하는지 제어할 수 있습니다:

CustomPaint({
  painter: {
    dependencies: myData, // 의존성 데이터
    shouldRepaint: (oldPainter) => {
      // true를 반환하면 다시 그림
      // false를 반환하면 이전 그림 유지
      return oldPainter.dependencies !== myData;
    },
    svg: {
      // SVG 구현
    },
    canvas: {
      // Canvas 구현
    }
  }
})

SVG vs Canvas 렌더링

Flitter의 CustomPaint는 두 가지 렌더링 방식을 지원합니다:

SVG 렌더링

  • 확대/축소 시 품질 유지
  • 벡터 그래픽에 적합
  • createDefaultSvgEl()로 SVG 요소 생성
  • paint()에서 setAttribute로 속성 설정
svg: {
  createDefaultSvgEl(context) {
    return {
      line: context.createSvgEl('path'),
      point: context.createSvgEl('circle')
    };
  },
  paint(els, size) {
    els.line.setAttribute('d', pathData);
    els.line.setAttribute('stroke', '#3B82F6');
    els.point.setAttribute('cx', '50');
    els.point.setAttribute('cy', '50');
  }
}

Canvas 렌더링

  • 복잡한 픽셀 조작에 적합
  • 고성능 애니메이션에 유리
  • 웹 Canvas API와 동일한 방식
canvas: {
  paint(context, size) {
    const ctx = context.canvas;
    ctx.fillStyle = '#3B82F6';
    ctx.fillRect(0, 0, size.width, size.height);
    ctx.strokeStyle = '#FF0000';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(100, 100);
    ctx.stroke();
  }
}

소스 코드 위치

CustomPaint의 구현을 살펴보려면:

  • packages/flitter/src/component/CustomPaint.ts: CustomPaint 위젯
  • packages/flitter/src/engine/canvas/: Canvas 구현
  • packages/flitter/src/engine/paint.ts: Paint 클래스

핵심 정리

  1. CustomPaint는 RenderObject의 복잡성을 숨깁니다
  2. painter 객체에 svg/canvas 구현을 분리하여 제공합니다
  3. dependencies와 shouldRepaint로 성능을 최적화할 수 있습니다
  4. SVG와 Canvas 두 가지 렌더링 방식을 지원합니다
  5. 사용하는 렌더러에 맞는 구현만 제공하면 됩니다
  6. 애니메이션과 쉽게 결합할 수 있습니다

다음 장에서는 지금까지 배운 모든 내용을 활용한 실전 사례들을 살펴보겠습니다.