[React] React에서 Canvas 사용하기(마우스로 그리기)

mokyoungg·2021년 4월 5일
25

Canvas란 무엇이고 어떻게 쓰는가?

canvas란 web에서 그래픽, 애니메이션을 그릴 수 있게 도와주는 태그이다.
canvas는 태그이므로 해당 태그에 접근하여 조작을 하는데
getContext() 메서드를 통해 드로잉 컨텍스트를 받아 그림을 그릴 수 있다.

[JS] JS에서 Canvas 사용하기(마우스로 그리기)
https://velog.io/@mokyoungg/JS-JS%EC%97%90%EC%84%9C-Canvas-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A7%88%EC%9A%B0%EC%8A%A4%EB%A1%9C-%EA%B7%B8%EB%A6%AC%EA%B8%B0




아래의 코드는 React 함수형 컴포넌트로(React Hooks) 작성하였습니다.

React에서 Canvas 태그 접근하기

web에서 마우스로 그림을 그리리면 canvas라는 html 태그를 사용해야 한다.
JS의 경우, getElementById 등의 방법으로 DOM에서 canvas 태그에 접근이 가능하다.

React에선 useRef를 통해 canvas에 접근해야 한다.

useRef()

useRef는 .current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref 객체를 반환합니다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지될 것입니다.<중략>

DOM에 접근하는 방법으로 refs에 친숙할 지도 모르겠습니다. <중략>
ref 속성보다 useRef()가 더 유용합니다. <중략>

useRef()는 순수 자바스크립트 객체를 생성합니다. 이는 매번 렌더링을 할 때 동일한 ref 객체를 제공합니다.

출처 : https://ko.reactjs.org/docs/hooks-reference.html#useref

리액트를 사용하는 프로젝트에서 DOM을 직접 선택해야 하는 상황이 발생할 때도 있습니다. 그럴 땐, 리액트에서 ref 라는 것을 사용합니다. 함수형 컴포넌트에서 ref를 사용할 때에는 useRef 라는 Hook 함수를 사용합니다.

useRef()를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM에 ref 값으로 설정해주어야 합니다. 그러면, Ref 객체의 .current 값은 우리가 원하는 DOM을 가리키게 됩니다.

https://react.vlpt.us/basic/10-useRef.html

ref 접근하기

redner 메서드 안에서 ref가 엘리먼트에게 전달되었을 때, 그 노드를 향한 참조는 ref의 current 어트리뷰트에 담기게 된다.

컴포넌트가 마운트될 때 React는 current 프로퍼티에 DOM 엘리먼트를 대입하고, 컴포넌트의 마운트가 해제될 때 current 프로퍼티를 다시 null로 돌려 놓는다. ref를 수정하는 작업은 componentDidMount 또는 componentDidUpdate 생명주기 메서드가
호출되기 전에 이루어진다.
출처 : https://ko.reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element

React 코드

  1. useRef()를 사용하여 canvasRef라는 Ref 객체를 만들었다.
  2. 해당 객체를 DOM으로 조작하고 싶은 canvas 태그의 Ref 값으로 설정하였다.
  3. useEffect를 통해, 페이지가 렌더가 될때 canvas에 canvasRef라는 ref 객체의 .current라는 값 할당하였다.
    (current 어트리뷰트에 담긴 캔버스 태그의 참조를 canvas에 할당)
  4. 이제 canvas는 조작하려는 canvas 태그를 가리킨다.
  5. canvas 태그의 크기를 설정한다.(브라우저 윈도우의 뷰포트의 크기 대비 설정)

React에서 canvas 조작하기

HTMLCanvasElement.getContext()

캔버스에 그림을 그리기 위해선 getContext() 메서드가 필요하다.
해당 메서드의 파라미터로 2d가 들어감으로써 2d 그림을 그릴 수 있다.
(beginPath(), moveTo(), lineTo(), stroke() 등의 메서드를 사용할 수 있게 됨)

React 코드

  1. useRef()를 사용하여 contextRef라는 Ref 객체를 만들었다.
  2. 해당 객체는 DOM 조작을 위한 객체가 아닌 canvas.getContext('2d')의 반환값인 드로잉 컨텍스트를 참조한다.
  3. contextRef의 current 어트리뷰트에 반환값을 할당하였다.
  4. 이후 그림 그리는 것에 필요한 메서드는 contextRef.current를 기준으로 작동한다.

React와 Canvas 사용을 위해 유튜브를 참고하였는데
해당 영상에서는 useState를 활용하지 않고 useRef를 통해 드로잉 컨텍스트를 계속 참조 하였다.

이것이 원인인지는 모르겠으나, render가 될 때 가끔씩 참조하는 값이 null이라 오류가 발생하였다. 이후 이 오류를 수정하기 위해 useRef를 통한 참조가 아닌 useState를 활용하여 드로잉 컨텍스트를 ctx라는 state값에 할당하여 사용하였다.
(오류가 발생하지 않았다.)

React에서 마우스로 그림 그리기

아래의 코드에서 문제점을 발견하였다.(4월 12일 작성)

https://velog.io/@mokyoungg/JS-JS%EC%97%90%EC%84%9C-Canvas-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A7%88%EC%9A%B0%EC%8A%A4%EB%A1%9C-%EA%B7%B8%EB%A6%AC%EA%B8%B0

이 글에서 자바스크립트로 그림을 그리는 방법 2가 문제점을 고칠 수 있는 방법이다.

1번 코드와 아래의 React에서 그림을 그리는 코드는 같은 방식인데 이런식으로 코드를 작성하면 drawing 코드에서(onMouseMove 함수), if(!isDrawing) 코드는 그림을 종료(완성) 이후 마우스를 한번 움직여야 코드가 작동한다는 문제점을 가지고 있다.

React 코드

이후부터는 JS의 그림 그리기 코드와 매우 흡사하다.

그리기에 사용된 메서드(beginPath, lineTo 등)에 대한 설명은 여기서 확인 가능하다.

[JS] JS에서 Canvas 사용하기(마우스로 그리기)
https://velog.io/@mokyoungg/JS-JS%EC%97%90%EC%84%9C-Canvas-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0%EB%A7%88%EC%9A%B0%EC%8A%A4%EB%A1%9C-%EA%B7%B8%EB%A6%AC%EA%B8%B0#htmlcanvaselementgetcontext

nativeEvent 사용

JS 코드와 비슷하지만 다른 점은 마우스를 움직일 때 전달받는 파라미터의 값이 JS에선 event지만 react에선 nativeEvent이다.

왜 사용하는가? event polling이 원인

캔버스 태그 위에서 일어나는 event의 값을 console.log()로 출력하면 다음과 같다.

event의 모든 값들이 null이며 해당 null에 warning이 매우 길게 나온다.

warning의 내용은 다음과 같다.

Warning: This synthetic event is reused for performace reasons.
If you're seeing this, you're accessing the property 'altkey' on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
See http://fb.me/react-eventt-pooling for more infomation.

성능상의 이유로 이벤트가 재사용되며 이 값은 null로 설정된다.
원래 합성 이벤트를 유지하려면 event.persist()를 사용하라.

경고에 나오는 주소를 따라가면 React 공식 페이지의 Event Pooling 설명이 나온다. 여기에선
이벤트 풀링을 'SyntheticEvent(합성이벤트) 객체를 재사용하고 모든 속성이 무효화된다.' 라고 설명한다.

(이벤트 풀링은 React 16버전 및 이하 버전 그리고 리액트네이티브에서만 해당되며 react 17에선 발생하지 않는다.)

The SyntheticEvent objects are pooled.
This means that the SyntheticEvent object will be reused and all properties will be nullified after the event handler has been called.

SyntheticEvent(합성이벤트)

이벤트 핸들러는 모든 브라우저에서 이벤트를 동일하게 처리하기 위한 이벤트 래퍼 SyntheticEvent 객체를 전달 받는다.
브라우저의 고유 이벤트가 필요하다면 nativeEvent 어트리뷰트를 참조하라.
합성 이벤트는 브라우저 고유 이벤트에 직접 대응되지 않으며 다르다.
출처 : https://ko.reactjs.org/docs/events.html#gatsby-focus-wrapper

즉, 리액트에서 발생하는 이벤트는 그대로 전달되는 것이 아니라 합성이벤트로 처리가 된 이후에 전달 받는다.

console에서 확인한 이벤트는 브라우저의 고유 이벤트가 아닌 React가 제공하는 합성이벤트이다.
(브라우저 이벤트 >> 리액트 SyntheticEvent 처리>> 내가 리액트를 통해 확인한 이벤트는 리액트를 통해 가공된 이벤트)

(합성이벤트는 성능상의 이유로 재사용이 되었으며 이때 null로 설정이 된다.)

canvas 태그에서 일어나는 이벤트를 브라우저 고유 이벤트로 여겨 이러한 오류가 발생한 것 같다.

event.persist()

합성 이벤트가 성능상의 이유로 재사용되었고 이를 통해 값이 null로 설정되었다.
리액트 공식페이지에선 원래의 합성 이벤트를 유지하려면 event.persist()를 사용하라고 한다.

event.persist()를 사용하면 합성이벤트가 재사용되고 값이 null로 바뀌기 전에 원래의 모습을 유지할 수 있다.

{nativeEvent}

React 공식페이지에선 브라우저의 고유 이벤트가 필요하다면 nativeEvent 어트리뷰트를 참조하라고 설명되어있다.

해당 이벤트를 참조하기 위해선 그전에 마찬가지로 event.persist() 가 필요하다.

그러나 event.persist()를 하지않고 다음과 같이 nativeEvent를 전달하면 pesist() 없이 바로 사용이 가능하다.

이게 왜 가능한지는 모르겠다.

아무튼 캔버스에서 그림을 그리려면 nativeEvent가 필요하다.
왜냐하면 캔버스에서 마우스로 그림을 그릴 때 필요한 값은 offsetX와 offsetY 값인데 nativeEvent에 있기 때문이다.

offset 값 말고도 client, layer, page 등에서 x와 y값을 제공하지만
offset 값을 사용하는 이유는 offset의 값은 이벤트가 일어나는 대상에서의 x,y값이기 때문이다.





참고

profile
생경하다.

1개의 댓글

comment-user-thumbnail
2021년 7월 27일

좋은글 감사합니다! 많은 도움이 됐어요 ㅎㅎ

답글 달기