ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Konva] Konva를 이용한 canvas 에디터 구현하기
    Dev 2022. 12. 5. 18:12


    프로젝트 진행 중 React 환경에서 canvas 에디터를 구현해야 하는 상황이 생겨

    konva 라이브러리를 이용해 간단한 에디터를 구현해보았다.

     

    konva를 선택한 이유는 React 환경에서 사용할 수 있는 canvas 라이브러리 중 커스텀이 편리하고, 구현해야 할 기능(동영상, 이미지, 텍스트 크기 조절 및 위치 이동) 적용 예시를 찾아볼 수 있는 라이브러리였기 때문이다.

    또 공식문서도 여러가지 예시와 잘 정리되어 있고, 현재에도 꾸준히 개발되고 있다는 점과 라이브러리 개발자의 피드백이 빠르다는 점도 있었다.

     

    1. 기본 구조

    konva에서 소개하는 기본 구조는 아래와 같다.

    이번 프로젝트에서는 그룹 기능까지는 필요하지 않아서 여러개의 shape 데이터를 매핑하여 stage에 여러개의 layer - shape 구조가 뿌려져있는 형태로 구현하였다.

                  Stage
                    |
             +------+------+
             |             |
           Layer         Layer
             |             |
       +-----+-----+     Shape
       |           |
     Group       Group
       |           |
       +       +---+---+
       |       |       |
    Shape   Group    Shape
               |
               +
               |
             Shape

    간단한 도형이나 텍스트를 만들어주는 것은 라이브러리에서 제공하는 <Text/>, <Rect/> 컴포넌트를 사용하면 아주 간단하게 구현할 수 있다. 하지만 구현해야하는 것은 동영상, 이미지, 텍스트의 분기처리가 필요하고 클릭, 드래그 등의 기능이 필요하므로 이러한 기능들을 수행할 CustomShape 컴포넌트를 만들어 사용하는 방향으로 구현하였다.

    // 매핑되는 캔버스 요소 데이터 타입
    type ShapeDataType = {
        type: string; // video | image | text
        x: number;
        y: number;
        id: string;
        value: string;
        width: number;
        height: number;
        fontSize?: number;
        fill?: string;
        fontFamily?: string;
        fontStyle?: string;
        lineHeight?: number;
        link?: string;
        rotation?: number;
    };
    
    
    // 캔버스 요소를 매핑하는 코드
    <Stage>
        {shapes.map((s) => (
            <Layer key={s.id}>
    	        <CustomShape data={s} />
            </Layer>
        ))}
    </Stage>

    캔버스 요소들이 담겨있는 "shapes" 스테이트는 처음에는 리엑트 스테이트에 저장해서 props로 내려보내 사용하는 형태로 작성하였지만, 자식 컴포넌트들을 잘게 쪼개며 정리할 수록 props drilling이 심해져서 recoil을 이용해 전역으로 상태관리하는 방향으로 수정하였다.

    이 외에도 현재 선택한 요소 데이터를 담고 있는 "selectedShape" 스테이트와 텍스트 수정 상태를 나타내는 "textEditMode" 스테이트 또한 자식 컴포넌트 이곳저곳에서 사용되는 곳이 많아 전역으로 상태 관리 하기로 하였다.

     

    2. CustomShape

    위에서 말한 것과 같이 CustomShape 컴포넌트에서는 data의 type 값에 따라 동영상, 이미지, 텍스트 요소를 랜더하는 역할을 하며,

    Transformer 컴포넌트 또한 랜더하여 드래그 및 크기 조절 기능을 수행한다.

     

    Shape

    data.type에 따라 VideoShape, ImageShape, TextShape 컴포넌트를 랜더하며, 요소 데이터, 클릭, 드래그, 크기 조절 등에 사용되는 함수를 props로 내려보내 요소에 기능을 추가한다. 각 함수들은 대부분 변경된 데이터를 전역 스테이트에 업데이트하는 동일한 흐름으로 작동한다.

     

    Transformer

    처음에 구현할때에는 각 타입별 컴포넌트에 Transformer 컴포넌트를 같이 구현해서 Transformer 컴포넌트와 관련된 함수들이 같은 코드임에도 불구하고 3개의 컴포넌트에 동일하게 작성되는 상황이 발생했다. 

    이러한 중복 코드가 실수를 발생 시키기 쉽고, 추후 확장하는데 걸림돌이 될 수 있다고 판단하여 각 요소 컴포넌트와 분리하여 CustomShape 컴포넌트 안으로 이동시켰다.

     

    3. 각 요소별 컴포넌트

    VideoShape

    비디오 요소 컴포넌트는 react-konva에서 공식 컴포넌트를 제공하고 있지 않아서, 구글링을 통해 구현 예시를 찾아 참고하여 구현하였다.

    대략적인 흐름은 react-konva의 Image 컴포넌트를 사용해서 랜더를 하는데 해당 Image 컴포넌트는 image props으로 dom element 타입의 데이터를 받아서 랜더할 수 있는데, 이러한 특성을 이용한 방법인 것 같았다.

     

    ImageShape

    ImageShape도 VideoShape 컴포넌트와 마찬가지로 konva 자체 Image 컴포넌트를 사용하기 때문에 업로드한 이미지 url을 이용하여 이미지 엘리먼트를 만들어서 image props 값으로 넘겨주는 형태로 구현하였다.

    이미지 요소에서도 처음에 생성되는 최대 크기를 제한하기 위해 비디오 컴포넌트에서 사용한 deCreaseImageWithRatio 함수를 이용하여 초기 생성 사이즈를 제한하였다.

    참고 예시

    TextShape

    TextShape가 가장 간단할것이라고 예상했지만 가장 고생한 컴포넌트이다.

     

    1. 좌표와 rotate 각도가 틀어지는 문제

    텍스트 요소를 두번 클릭했을때 기존의 konva Text 컴포넌트를 보이지 않게 하고 그 위치에 텍스트를 입력할 수 있는 인풋창이 생겨야했다.

    다른 구현 예시를 참고하여 구현하기 시작했는데, 예시와는 달리 내가 구현한 코드에서는 인풋 - 텍스트 컴포넌트간의 상태 변경 시 위치가 이동되거나 rotation 값이 제대로 적용되지 않는 문제가 발생했다.

    이러한 문제는 x, y, rotation 값을 HTML 컴포넌의 divProps에 설정해주는 것으로 해결되었다.

    예상되는 이유는 konva에서 이런 텍스트 수정 기능 등을 구현할때 textarea 등 기존의 html 태그를 캔버스(Stage) 안에서 사용하고 싶다면 HTML 컴포넌트를 사용하도록 안내하고 있으며, 예시에서도 위치값 등의 스타일은 divProps에 넘겨주는 것으로 처리하는 것을 확인 할 수 있는데 이러한 부분을 간과하고 div 태그에 직접적으로 좌표 값이나 transform 값을 넘겼기 때문에 의도한 바와 다르게 작동한 것이라고 생각된다.

    2. 텍스트 입력창의 넓이 조절 문제

    보통의 textarea를 사용하지 않고 div 태그를 사용하고 contentEditable로 처리한 이유가 있다.

    처음에 textarea로 구현 후 테스트 해보았을때 넓이가 일정 크기로 고정되어 있고, 텍스트 입력에 따라 늘어나거나 줄어들지 않았다.

    이러한 문제를 해결하기 위해 textarea auto width 등의 키워드로 찾아보았지만 높이 조절 레퍼런스만 있을 뿐 넓이 조절은 찾을 수 없었다. 그래서 div 태그라면 컨텐츠에 따라 넓이가 조절되지 않을까 싶어서 div로 수정하여 구현해 보았더니 예상대로 입력한 컨텐츠에 맞게 넓이가 줄어들고 늘어나는 것을 확인할 수 있었다. 그래서 div에서 text 변경을 위한 로직을 작성하여 텍스트 수정 기능을 구현하는 것으로 해당 문제를 처리하기로 했다.

     

    +  조건부 랜더링이 아닌 Text 컴포넌트의 opacity로 컴포넌트가 보여지는 여부를 설정한 이유는 텍스트 크기를 조절하는 함수에서 shapeRef를 사용하고 있기 때문에 아예 랜더가 안되게 되면 해당 함수가 작동하지 않게 되므로 opacity로 구현

     

    여차저차 구현하긴 했지만, 뭔가 얼렁뚱땅 구현하고 있는 것 같아 아쉬움이 있다.

    댓글

Designed by Tistory.