문제 발생

React의 forwardRef를 활용하여 Rect, Circle, Group 등 다양한 Konva 노드 컴포넌트에 ref를 연결하여 해당 노드의 인스턴스에 접근하고자 했다. 처음에는 간단할 것이라 예상했다. 하지만 예상과는 달리 TypeScript와의 타입 호환성 문제로 인해 적잖은 어려움을 겪었다.

문제 상황: 예상치 못한 타입 오류와 컴포넌트별 ref 충돌

Konva 노드를 forwardRef로 감싸는 것은 일반적인 React 컴포넌트와 크게 다르지 않을 것이라 생각했다. 그래서 아래와 같이 코드를 작성했다.

import Konva from 'konva';
import React, { forwardRef } from 'react';

interface ElementsCanvasElementProps {
  element: {
    // ... element properties
    elementType: string;
    width?: number;
    height?: number;
    fill?: string;
    x: number;
    y: number;
    points?: number[];
  };
  // ... other props
}

// 처음 시도했던 코드
const ElementsCanvasElement = forwardRef<Konva.Node, ElementsCanvasElementProps>(
  ({ element, ...rest }, ref) => {
    if (element.elementType === 'rectangle') {
      return (
        <Rect
          ref={ref} // 여기서 타입 오류 발생!
          x={element.x}
          y={element.y}
          width={element.width}
          height={element.height}
          fill={element.fill}
          draggable
          {...rest}
        />
      );
    }
    // ... 다른 요소 타입들
    return null; // 기본 반환값
  }
);

하지만 이 코드는 다음과 같은 TypeScript 오류를 발생시켰다.

Type 'ForwardedRef<Konva.Node<Konva.NodeConfig>>' is not assignable to type 'LegacyRef<Konva.Rect> | undefined'.
  Type 'Konva.Node<Konva.NodeConfig>' is not assignable to type 'Konva.Rect'.
    Property 'getWidth' is missing in type 'Node<NodeConfig>' but required in type 'Rect'.ts(2322)

오류 메시지의 핵심은 forwardRef에 제네릭으로 넘긴 Konva.Node 타입과 실제 Rect 컴포넌트가 요구하는 Konva.Rect 타입이 일치하지 않는다는 것이었다. Konva.Node는 모든 Konva 노드의 기본 클래스이지만, RectgetWidth, getHeight 등 자신만의 속성과 메서드를 가진 구체적인 클래스이기 때문에 호환되지 않았던 것이다.

심지어 아래와 같이 하나의 컴포넌트 내에서 조건부 렌더링을 통해 GroupRect를 동시에 반환하려고 할 때도 비슷한 문제가 발생했다.

// Group과 Rect를 동시에 처리하려 할 때의 문제 예시
const ElementsCanvasElement = forwardRef<Konva.Node, ElementsCanvasElementProps>(
  ({ element, ...rest }, ref) => {
    if (element.elementType === 'group') {
      return <Group ref={ref} {...rest} />; // ref는 Konva.Group 타입을 기대
    }
    if (element.elementType === 'rectangle') {
      return <Rect ref={ref} {...rest} />; // ref는 Konva.Rect 타입을 기대
    }
    // ...
  }
);

하나의 ref를 전달받아 내부 요소의 타입에 따라 Konva.Group에도 할당하고 Konva.Rect에도 할당해야 하는 상황인데, forwardRef의 제네릭 타입 (Konva.Node)과 실제 할당하려는 요소의 구체적인 타입 (Konva.Group, Konva.Rect) 간의 불일치가 계속해서 문제를 일으켰다**.**

원인 분석: Konva 타입 시스템과 React의 ref 타입 불일치

문제의 근본 원인은 React의 forwardRef 타입 시스템과 react-konva가 내부적으로 사용하는 Konva의 클래스 타입 시스템 간의 불일치였다.

  1. React.forwardRef<T, P>에서 제네릭 T는 전달될 ref의 타입을 의미한다. 우리는 모든 Konva 노드의 기본 타입인 Konva.Node를 여기에 지정했다.
  2. 하지만 react-konvaRect, Circle, Group 등의 컴포넌트는 내부적으로 실제 Konva 클래스(Konva.Rect, Konva.Circle, Konva.Group)의 인스턴스를 생성한다. 이 컴포넌트들의 ref prop은 해당 구체적인 Konva 클래스 타입을 기대한다.
  3. 따라서 Konva.Node 타입으로 정의된 refKonva.RectKonva.Group 타입을 기대하는 곳에 직접 할당하려고 하니 TypeScript 컴파일러가 타입 오류를 발생시킨 것이다.

코드를 간결하게 만들기 위해 공통 속성을 분리하려는 시도 역시 이 문제에 부딪혔다.

// 공통 속성 분리 시도 (실패)
const ElementsCanvasElement = forwardRef<Konva.Node, ElementsCanvasElementProps>(
  ({ element, ...rest }, ref) => {
    const commonProps = {
      ...rest,
      x: element.x,
      y: element.y,
      draggable: true,
      ref: ref, // 이 ref 타입(Konva.Node)이 문제!
    };

    if (element.elementType === 'rectangle') {
      return <Rect {...commonProps} width={element.width} height={element.height} />; // 에러: ref 타입 불일치
    }
    if (element.elementType === 'group') {
      return <Group {...commonProps} />; // 마찬가지로 에러 발생
    }
    // ...
  }
);

ref의 타입을 Konva.Node로 받으면 Group, Rect에 각각 할당할 수 없고, 반대로 Konva.Group으로 받으면 Rect에는 할당할 수 없는, 진퇴양난의 상황이었다.