React-Konva: 새싹톤 도전과 화이트보드, 그 여정 ...

뮤진·2023년 5월 27일
1
post-thumbnail

Intro

새싹톤에 참여하게 되었다!

인프런에서 새싹톤 팀을 구해 들어가게 되었다.
아무래도 취준생부터 현직자까지 모두 참여하는 해커톤인 만큼 규모도 크고 내가 많이 성장할 수 있을 것이라고 생각했다.

💡 서비스 소개
'우리의 어플은 화상 채팅을 통해 실시간으로 상대방의 운동 동작을 AI 운동 모션 감지 기능을 통해 분석하여 피드백을 제공한다. 원격으로 연결된 운동 파트너와 함께 몸을 움직여 더욱 효과적인 운동을 할 수 있다. 편리한 화상 채팅 인터페이스를 통해 상대방의 모션을 실시간으로 확인하고, 운동 자세의 정확성과 효율성을 개선할 수 있다.

그렇게 합류하게된 우리 팀은, 나 외에는 모두 현직자분들로 이루어져있었고 기획된 서비스 또한 높은 기술력과 좋은 기획을 포함하고 있어 '나만 잘하면 돼..' 겠다는 생각을 했다.

실시간 화상 채팅에 필요한 화이트보드 기능을 React-Konva 라이브러리를 사용해 구현을 하게 되었다.

Konva 공식문서에는 여러가지 Demo가 있어, 이를 기반으로 디벨롭 할 수 있었고
원하는 기능과 비슷한 Free Drawing Demo를 참고하였다.

Konva Demo 분석하기

  • lines 배열은 선 객체가 직접 저장된다.
  • 선이 그려질 때마다 setLines를 호출하여 lines 배열 전체를 갱신한다.
  • 매번 새로운 배열을 생성하여 이전의 lines와 병합한다.
  • 선의 추가 및 갱신 시 -> 배열의 복사와 병합이 필요하여 성능에 영향을 미칠 것이다💩

선의 좌표값들을 실시간으로 전송해야하기 때문에,
성능을 고려해야 했고 이 방법으로 진행하기에는 어렵다고 생각했다.🤔

그래서 그려진 선들이 저장되는 배열,
현재 그려진 선이 저장되는 배열을 나누었다.

Konva로 화이트보드 만들기

우선 Line 컴포넌트를 두개 만들었다.

 <Layer>
         {lines.map((line, index) => (
            <Line
              key={index}
              points={line.flatMap(({ x, y }) => [x, y])}
              stroke={line[0].stroke}
              strokeWidth={line[0].strokeWidth}
              lineCap='round'
              lineJoin='round'
              globalCompositeOperation={mode === 'eraser' ? 'destination-out' : 'source-over'}
            />
          ))}
          {currentLine.length > 0 && (
            <Line
              points={currentLine.flatMap(({ x, y }) => [x, y])}
              stroke={currentLine[0].stroke}
              strokeWidth={currentLine[0].strokeWidth}
              lineCap='round'
              lineJoin='round'
              globalCompositeOperation={mode === 'eraser' ? 'destination-out' : 'source-over'}
            />
          )}
 </Layer>
  • 현재 그려지는 선은 currentLine 배열에 저장되고, 그리기가 완료되면 lines 배열에 추가한다.
  • 선의 추가 및 갱신 시 -> 배열의 복사와 병합이 필요하지 않다.
  • lines 배열에는 선 객체의 참조가 저장되기 때문에 상대적으로 가변성이 낮고, 성능상 이점이 생긴다 ✨

이렇게 그리기 도중인 선과, 그리기를 완료한 선을 명확하게 구분하여 직관적인 코드를 완성하였다. 하지만 이렇게 나눔에 따라 문제가 발생하게 된다 😅

도대체 왜 안지워지는거야? 😩

우선 다음은 위 코드에 대한 내용이다.

  1. 첫번째 Line 컴포넌트는 lines 배열을 map 돌려서 만들어지는 컴포넌트로, 그리기를 완료한 선들을 표시하는 컴포넌트이다.
  2. 두번째 Line 컴포넌트는 currentLength를 그리는 컴포넌트이다.
  3. globalCompositeOperation 속성은 Konva라이브러리에서 제공하는 속성으로 mode에 따라 화면에 그려진다. (brush or eraser)

mode는 사용자가 현재 'brush'툴을 클릭하였는지, 'eraser'툴을 클릭하였는지를 담은 속성이며 mode가 'eraser'일 경우 'destination-out'으로 투명하게 처리되도록 구현하였다.

그런데 문제는.... 마우스를 클릭하고 드래그하는 동안에는 마우스가 지나간 자리가 투명하게 처리가 되는데 (기존에 그려진 선위로 마우스가 지나가면 해당 영역이 투명하게 지워짐) 마우스를 떼는 순간! 마우스가 지나간 자리 그대로 다시 설정된 컬러로 선이 그려진다...^^

가로로 지나가는 선이 지우개 툴을 클릭후 그은 선인데
마우스를 떼면 저렇게 컬러가 있는 선으로 바뀌어 버린다 🤦‍♀️

도대체 왜...?

선이 그려지는 로직을 다시 생각해보았다.

  • 그리고 있는 선은 currentLine에 실시간으로 업데이트되며 두번째 라인 컴포넌트에 의해 렌더링된다.
  • 다 그려진 선은 lines에 합쳐지고 첫번째 라인 컴포넌트에 의해 렌더링된다.

그려지는 동안까지는 eraser기능에 아무 문제가 없다가
마우스를 떼면, 즉 lines 컴포넌트에 합쳐지면 마치 일반 brush 툴처럼 컬러가 생기기 때문에 문제는 lines와 첫번째 Line 컴포넌트에 있을 것이라고 생각했다.

그래서 다음과 같은 방법들을 시도해본다.

👉 Line 컴포넌트 수정해보기

stroke 속성이 현재는 라인배열의 stroke 컬러로 설정이 되어있기때문에,
조건문을 추가해보았다.eraser 도구를 선택하면 컬러가 투명이되도록...

<Line
  key={index}
  points={line.flatMap(({ x, y }) => [x, y])}
  stroke={mode === 'eraser' ? 'rgba(0, 0, 0, 0)' : line[0].stroke}
  strokeWidth={line[0].strokeWidth}
  lineCap="round"
  lineJoin="round"
  globalCompositeOperation={mode === 'eraser' ? 'destination-out' : 'source-over'}
/>

은 실패!

이 외에도 mode가 'eraser'일 경우 currentLine을 빈배열로 두는 등 이상한 여러 시도를 해보았지만

사실 당연히 실패였다.. Konva 속성을 무시한 채 강제적으로 투명색을 지정해주기때문에 만약 해결된다 해도 잘못된 방법이라고 생각이 들었다.

이렇게 Line 컴포넌트의 속성들을 어떻게 해보려는건 잘못된 방법이라고 생각하여,
currentLine에서 lines로 넘겨주는 객체의 정보에 대해 생각해보게되었다.

👉 currentLine, lines 에 들어가는 배열의 정보들을 수정해보기

현재 lines에서는 아래와 같은 모양의 객체배열이 들어온다.

[{ x: pos.x, y: pos.y, stroke: brushColor, strokeWidth: strokeWidth }]

선이 그려지는 좌표와 선의 컬러, 선의 굵기이다.
그래서 이 객체의 key-value로 mode를 key 값으로 추가하고, value가 'eraser'인 경우에만 'destination-out'이 되도록 조건문을 작성해주면 되지 않을까?
하는 생각을 하게되었다.

그래서 코드를 다음과 같이 수정했다.

  {lines.map((line, index) => (
            <Line
              key={index}
              points={line.flatMap(({ x, y }) => [x, y])}
              stroke={line[0].stroke}
              strokeWidth={line[0].strokeWidth}
              lineCap='round'
              lineJoin='round'
              globalCompositeOperation={
                line[0].mode === 'eraser' ? 'destination-out' : 'source-over'
              }
            />
          ))}

결과는?
성공이다 !!!!!

이제 지우개 툴을 선택하고 기존 라인을 지나가면 해당 영역대로 지워지는 것을 확인 할 수 있었다 🥹👍👍👍

혼자 감격하기...

사실 lines 객체에 mode 속성을 추가하는 것은 결국 필수적이었다.
왜냐하면 이 lines 객체는 마우스 클릭을 뗄 때마다 서버에 전송할 예정이다.
상대방도 실시간으로 해당 라인을 확인해야하기때문에,
해당 좌표 값이 brush로 그린 좌표인지 eraser로 그린 좌표인지도 구분되어야 했기 때문이다.

어쩌면 이 eraser가 뜻밖의 제대로 동작했다면 생각해보지 못했을 것 같다.

디버깅 과정은 길고 험난할 수 있지만
역시 해결하고 나면 짜릿하다 😊

나의 바보같은 코드를 더 좋게 만들어 주었고
리액트 동작원리에 대해 다시 한 번 고민해볼 수 있는 경험이 된 것 같다.

Outro

이렇게 npm 배포까지 예정이였던 화이트보드 기능은 새싹톤 아이디어 2차 과정에서 탈락됨에 따라 함께 마무리하게 되었다.

너무나도 아쉽지만 현직자 분들의 열정과, 화이트보드 기능 구현에 대한 피드백으로 자신감도 얻어가고 나에게도 또 하나의 문제를 해결해보는, 좋은 경험치가 되었다고 생각한다.

앞으로도 계속 도전하자!

profile
프론트엔드 공부기록 🫶 기록을 통해 성장하기

0개의 댓글

관련 채용 정보