개선점
- 게임을 시작하고 드래그를 진행시 화면의 뷰포트가 드래그 방향으로 움직여 버리는 문제가 발생
- 화면이 자꾸 움직이니 모바일에서는 게임을 진행이 힘들었다.
터치 이벤트 문제발생
event.preventDefault()를 통해 터치 이벤트의 기본 동작을 막아야한다.
하지만 아래와 같이 나는 터치 이벤트 리스너를 등록했기 때문에 핸들러 내에서 event.preventDefault() 를 호출 시 다음과 같은 오류를 만나게 되었다.
Unable to preventDefault inside passive event listener invocation
passive 란?
touch, wheel
등 일부 이벤트에서 동작을 최적화하여 스크롤 퍼포먼스를 대폭 향상시키는 웹 표준 기능
보통의 등록된 리스너는 수신한 이벤트를 Compositor thread
에서 Main thread
에 넘기고
Render tree 를 넘겨받을 때까지 기다리지만
passive 옵션이 켜져있다면 Compositor thread 에서 Main thread 의 동작을 기다리지 않고 자기가 할일 Paint와 Composite 를 즉시 수행함
이벤트를 수신하는 즉시 핸들러를 실행하고, 화면에 바로 반영시킬 수 있게 됨으로써 렌더링 퍼포먼스가 향상되는 원리
문제점 확인
위에서 살펴본대로 passive 옵션이 켜져있는 이벤트리스너는 Main thread 의 동작을 배제하고 있는데 preventDefault() 는 Main thread 에서 돌아간다 그래서 passive 옵션이 켜져있는 이벤트리스너는 preventDefault() 를 호출할 수 없다.
그리고 리액트는 기본적으로 touchmove touchstart등의 이벤트에는 passive 옵션을 true로 설정함.
따라서 컴포넌트에 Symentic event를 등록하는 방식말고 ref를 통해 직접적으로 canvas에 touch이벤트들의 리스너를 등록하고 passive옵션을 false로 설정해 주어야한다.
더 나은방법은 없을까?
직접 ref 통해 명시적으로 이벤트를 등록하는건 쉽지만 useEffect에 등록하는 로직과 clean up 함수까지 작성하게 되면 useEffect 가 지나치게 커지고 보기에 좋지 않다.
나는 react 팀이 passive 이벤트를 끄는 기능을 왜 제공하지 않는지 궁금했고 react 레포지토리의 issue 에서 찾을 수 있었다.
위 글을 요약하자면
- 이전에는 크롬에서 touch event 에서도 preventDefault() 를 수행할 수 있었지만 크롬이 wheel, touch 등의 이벤트에서 passive 옵션을 기본적으로 true로 설정하였다.
- React17 에서는 이벤트가 document 레벨에 위임되지 않고 root 컨테이너에 직접 위임이 되도록 변경되어 크롬의 개입을 무효화 했지만 성능의 관점에서 기본적으로 passive 옵션을 기본적으로 true 유지하도록 함.
React 팀에서도 passive 옵션을 true 로 유지하면 preventDefault() 가 필요한경우 활성 리스너를 의도적으로 부착할 수 있는 React의 네이티브 API를 제공하지 않는다는 문제를 인지하고 있었고 성능과 더 많은 이벤트 처리간의 균형 및 브라우저의 이벤트 처리의 일관성을 생각한 결정임을 알 수 있었다.
결론
- useEffect를 통해서 처음 게임 컴포넌트의 렌더링시 ref 에 직접 등록하고 이벤트의 passive 옵션을 꺼주었고 핸들러에서 event.preventDefault()를 호출할 수 있었다.
리스너의 상태추적 문제발생
- 이벤트를 등록하고 테스트를 하던중 모바일과 데스크탑이 서로다른 상태값을 참조하고 있는걸 발견.
- 데스크탑에서는 게임의 최신상태를 잘 추적 했지만 모바일에서는 처음의 상태값을 유지하고 있었음
문제점 확인
- Synthetic Event 로 등록된 mouse 이벤트는 항상 최신의 React 상태값을 가짐
- 하지만 직접 DOM API 를 사용해서 이벤트를 등록한 touch 이벤트는 초기 등록시점의 클로저 환경을 유지하고 초기 상태를 계속 참조하기 때문에 버그가 발생.
- 해결방법은 간단하게 적절한 시점에 이벤트리스너를 다시 등록해 리스너가 최신의 상태를 가질 수 있도록 하는것
결론
- useEffect 를 통해서 리스너를 다시 등록할 적절한 시점을 알리기 위해 황금사과를 제거한 경우, 점수를 얻은경우에 변경되는 상태값을 디펜던시에 등록하여 이벤트 리스너를 다시 등록하도록 하여 해결 완료.