이미지 에디터 개발 기록 - 필압 처리, iPad에서의 성능 개선
이미지 에디터 개발 기록 - 필압 처리, iPad에서의 성능 개선
기존에 블러만 있던 이미지 에디터를 대대적으로 개편했다. 이제 레이어, 브러시, 색 보정, 필터가 있는 에디터가 되었다. 라이브러리 없이 Canvas와 WebGPU를 사용했다. 이 글은 여러 기능, 특히 브러시와 필압 기능을 추가하면서 겪었던 iPad 성능 저하 문제, 획 사라짐 문제 등의 해결 과정을 기록한 것이다.
브러시, 그리고 필압 기능
브러시 기능을 추가했다. 스타일러스 펜의 필압을 감지하여 획의 굵기가 변경되도록 했다. pointerType이 "pen"일 경우 필압을 그대로 사용하되 하한 값을 설정했다. 아주 약한 터치일 경우 0에 가까운 값이 나와 획이 사라지기 때문이다. 필압을 감지하지 못하는 기기는 브라우저에서 0.5의 필압값을 고정으로 주었다.
export function pointerPressure(pointerType: string, pressure: number) {
return pointerType === "pen" ? Math.max(pressure, MIN_PRESSURE) : 1;
}
따라서 pen이 아닌 경우 필압값을 무시하고 1의 압력을 사용하도록 설정했다.
펜은 화면보다 빠르다
브라우저는 한 프레임(16ms) 안의 입력을 이벤트 하나로 합쳐서 준다. 하지만 펜은 그보다 더 입력을 자주 보낸다. 그래서 빠르게 곡선을 그으면 선이 각지게 된다. 그래서 getCoalescedEvents()로 브라우저가 합치기 전의 입력을 모두 꺼내서 사용하도록 처리했다.
iPad에서 발생한 성능 저하의 해결 과정
데스크탑, 정확히는 macOS의 Chrome이나 Safari에서 테스트할 때는 잔렉 없이 블러나 색 보정 기능들이 매끄럽게 작동했다. 하지만 iPad에서 테스트하니 블러와 색 보정 기능 사용 시 렉이 걸리는 것을 발견했다. 약 900만 픽셀의 큰 이미지에서 테스트하니 펜 입력 한번을 처리하는 데 평균 117ms가 걸리는 것을 확인할 수 있었다.
태블릿의 성능 문제인가 싶어서 WASM을 이용하여 속도 향상을 꾀해야하나 고민했다. 하지만 병목이 일어나는 지점을 테스트해보니 계산 작업이 아닌, 2D 캔버스와 GPU 사이로 픽셀을 옮기는 부분에서 병목 현상이 일어나고 있었다. 따라서 WASM으로 코드를 옮겨 계산 성능을 올리는 것은 해결 방법이 아니었다. 테스트 측정 시 100만 픽셀 당 4ms 정도가 걸렸다. 블러와 색 보정은 이미 GPU에서 계산하고 있었다. 문제는 그 결과를 화면에 그리려고 매번 2D 캔버스로 다시 가져오는 데 있었다. 이를 해결하기 위해 화면 합성 전체를 WebGPU로 옮기고, 그리는 동안에는 점만 GPU에 찍도록 바꿨다. 이렇게 하니 최종적으로 117ms에서 0.1ms로 입력 시간이 줄어들어서 매끄러운 입력을 구현할 수 있었다.
획이 왜 무시되는가

그 다음으로 겪은 일은 획 입력이 사라지는 동작이었다. 글씨를 쓰듯 펜을 빠르게 움직이면 두번째 터치의 입력이 실행되지 않았다. 이 부분도 마찬가지로 빠른 입력으로 인해 병목이 발생하는 줄 착각했다. 하지만 테스트 상에선 페이지에 도착한 입력은 모두 획이 되었고, 빠진 획은 아예 페이지에 도착하지 않았다는 것을 발견했다. 분명 터치를 했는데 왜 획을 감지하지 못할까 생각하면서 계속 입력을 해봤는데, 순간 화면이 텍스트 선택 하듯 하이라이트가 되는 것을 발견했다. 그래서 이건 혹시 짧게 두번 입력하는 것이 텍스트 선택으로 판정되어서 브라우저가 동작을 가로채는게 아닐까 추정하고 작업을 다시 진행했다.
폰이나 태블릿 같은 터치 환경에서는 더블 탭이 부가 기능을 불러오는 동작이다. 글씨를 쓰듯 펜을 빠르게 2번 터치하면 기기에서는 더블 탭인 줄 알고 두번째 입력을 막아버리는 것이다. 이 부분은 CSS 두 줄과 touchstart를 통해서 해결했다.
user-select: none; /* 글자 선택을 끈다 */
-webkit-touch-callout: none; /* 길게 누를 때 뜨는 메뉴(복사, 찾아보기 등)를 끈다. iOS 전용 */
const preventDefault = (event: TouchEvent) => event.preventDefault();
canvas.addEventListener("touchstart", preventDefault, { passive: false });
이렇게 해서 canvas에서 시작한 터치를 브라우저가 자체 동작에 사용하지 못하게 처리했다. React의 onTouchStart로는 기본 동작을 막을 수 없어서 passive: false를 addEventListener로 직접 붙였다.
텍스트 입력이 필요한 부분은 명시적으로 user-select: text로 되돌려두었다.
그럭저럭 나온 만듦새

결과물은 꽤 그럴 듯한 웹 에디터가 나왔다. 만들면서 개발 방향이 정해졌는데, 그것은 바로 웹툰 툴이다. 최근 그림을 그리는 것에 관심이 생겼는데, 웹에서 쉽게 만화를 그릴 수 있도록 하면 좋을 것 같았다. 그래서 앞으로는 내가 직접 사용하면서 겪은 불편함을 해결하는 방향으로 기능을 추가해보려고 한다.

