모바일 환경에서의 클릭, 터치 이벤트 처리

@Ryan· December 31, 2023 · 5 min read

토이프로젝트로 WORDLE 게임을 만들었다.

게임링크, 소스코드링크

데스크탑 브라우저 환경에서는 잘 동작하지만, 내 핸드폰 브라우저 환경(iOS, Safari)에서는 버튼을 빠르게 눌렀을 경우, 예를들어 'P','O'를 빠르게 입력할 경우 'PO' 대신 'PP'가 입력되는 경우가 발생했다.

처음에는 이 오류가 발생하는 원인을 데스크탑 환경과 모바일 환경의 브라우저 렌더링 성능 차이 때문이라고 생각했다. 따라서 상태 업데이트 로직을 살펴보았고 비효율적인 부분의 개선을 시도했다.

1. 상태 업데이트 로직의 비효율성

원래코드:

const [cellValues, setCellValues] = useState<
  { letter: string; color: string }[][]
>(
  Array(6)
    .fill(null)
    .map(() => Array(5).fill({ letter: "", color: "" }))
)

const handleKeyPress = (key: string) => {
  // 기타 코드 생략

  {
    setCellValues(prev => {
      const newValues = prev.map(row => row.map(cell => ({ ...cell })))
      if (currentColumn < 5) {
        newValues[currentRow][currentColumn].letter = key
        setCurrentColumn(Math.min(currentColumn + 1, 5))
        setGuess(guess + key)
      }
      return newValues
    })
  }
}

위 코드를 살펴보면, 게임보드의 그리드를 2차원 배열로 생성하였고, 키가 입력되면 2차원 배열에 key값을 letter에 담아두는 형식으로 상태를 선언했다.

키 값이 입력되었을 때 상태변화를 발생시키는 로직인 handleKeyPress()를 보면, newValues를 깊은 복사를 통해 전체 2차원 배열을 복사하고, 입력된 key값을 담은 후 상태를 업데이트 하고 있다.

하지만 실제로 변경이 필요한 것은 key값이 입력되고 있는 현재 행의 현재 열에 해당하는 셀 뿐이다. 전체 그리드를 복사하는 대신 변경이 필요한 부분만 복사하고 업데이트 하는 것이 효율적이기 때문에 코드를 수정하였다.

수정된 코드:

// 이전 코드 생략
setCellValues((prev) => {
  const newValues = [...prev];
  if (currentColumn < 5) {
    const newRow = [...newValues[currentRow]];
    newRow[currentColumn] = { letter: key, color: '' };
    newValues[currentRow] = newRow;
    setGuess(guess + key);
    setCurrentColumn(Math.min(currentColumn + 1, 5));
  }
  return newValues;

코드를 수정했음에도 여전히 같은 오류가 발생했다.

사실 브라우저와 모바일의 렌더링 성능차이가 난다고 하더라도, 겨우 6*5 배열 수준인데 입력 지연이 발생할 정도로 성능 차이가 난다는 것은 말이 안되긴 했다....

2. iOS환경에서의 클릭 지연

원본기사: https://www.telerik.com/blogs/what-exactly-is.....-the-300ms-click-delay

iOS환경의 브라우저에서는 더블탭, 스와이프를 감지하기 위해 의도적으로 300ms의 클릭 딜레이를 준다는 기사를 찾게 되었다!

버튼 클릭 이벤트에 사용하는 onClick()을 사용할 경우 클릭 딜레이가 발생하게 되는 것이었고, 터치 이벤트를 활용하면 클릭 딜레이가 발생하지 않기 때문에 onTouchEnd()를 활용해 보았다.

const [touchUsed, setTouchUsed] = useState(false)

const handleClick = (key: string) => {
  if (!touchUsed) {
    onKeyPress(key)
  }
  setTouchUsed(false)
}

const handleTouch = (key: string) => {
  setTouchUsed(true)
  onKeyPress(key)
}

// 기타코드 생략
return (
  <button
    key={key}
    onClick={() => handleClick(key)}
    onTouchEnd={() => handleTouch(key)}
    className={className}
  >
    {key}
  </button>
)

데스크탑 환경에서는onClick()을 사용하고 모바일 환경에서는onTouchEnd()를 사용해야 했기 때문에, 터치 상태를 만들고 터치일 경우에만(모바일 브라우저일 경우에만) onTouchEnd()를 사용할 수 있게 구현하였다.

만약 상태를 쓰지 않을 경우 모바일에서 버튼을 누르면 onClick()onTouchEnd()가 연달아 호출되어 key가 두 번 입력되게 된다.

그리고 결과는... 잘 동작한다!

단순히 친구들과 즐기기 위해 만든 토이프로젝트였지만, 예상치 못한 크로스브라우징 이슈를 만나게 되었다. 앞으로의 프로젝트에서는 여러 환경에서의 작동을 고려하여 개발해야겠다.

@Ryan
일단 해보자
© ryanbae.dev, Built with Gatsby