dev.to

Stop Debouncing Resize. The Browser Already Watches It.

resize 디바운싱을 멈추세요. 브라우저가 이미 변화를 감지합니다

창 크기 변경 때마다 실행하는 resize 핸들러 대신 matchMedia()의 change 이벤트를 쓰면, 미디어 쿼리 조건이 바뀌는 순간에만 반응합니다. 화면 너비뿐 아니라 prefers-reduced-motion 같은 설정 변화도 감지할 수 있습니다.

AI 요약

창 너비가 900px 아래로 내려가면 사이드바를 접고 차트를 모바일용으로 바꾸는 화면을 생각해 보세요. JavaScript가 resize 이벤트마다 window.innerWidth를 확인하면 창을 드래그하는 동안 초당 60회 넘게 검사를 반복할 수 있습니다. 디바운싱을 넣어도 검사를 늦출 뿐, CSS 미디어 쿼리와 JavaScript에 중단점을 따로 적어야 하는 문제는 남습니다. 스크롤바가 나타나거나 사라져 너비가 15px 달라지면 경계에서 상태가 잘못 바뀔 수도 있습니다.

너비 조건에는 matchMedia()를 사용합니다

브라우저의 window.matchMedia()는 미디어 쿼리와 현재 일치하는지 확인하고, 조건이 바뀌면 change 이벤트를 보냅니다. 예를 들어 (max-width: 899px) 쿼리의 MediaQueryList를 만든 뒤 matches 값으로 현재 상태를 확인하고, addEventListener("change", ...)에서 차트를 바꿉니다. 창을 경계 너머로 열 번 드래그하면 resize 콜백 수백 번 대신 조건이 바뀔 때마다 이벤트를 받습니다.

이 방식은 너비 임계값을 직접 비교하거나 resize 이벤트를 디바운싱하지 않습니다. CSS와 JavaScript가 같은 미디어 쿼리를 기준으로 움직이므로, CSS 중단점을 바꿀 때 JavaScript의 숫자도 찾아 고쳐야 하는 불일치를 줄입니다.

미디어 쿼리는 화면 크기만 다루지 않습니다

prefers-color-scheme, prefers-reduced-motion, prefers-contrast, hover, pointer도 미디어 기능입니다. 특히 운영체제에서 reduced motion 설정을 바꾸면 열린 탭에서도 change 이벤트가 발생합니다. requestAnimationFrame으로 움직이는 JavaScript 애니메이션은 CSS의 prefers-reduced-motion 규칙만으로 제어하기 어려우므로, 설정을 읽고 변경을 감지해 애니메이션을 조정할 수 있습니다.

MediaQueryList에는 과거에 addListener()와 removeListener()를 썼지만, 두 메서드는 이제 폐기 예정입니다. 앞으로는 다른 DOM 객체와 마찬가지로 addEventListener("change", ...)와 removeEventListener()를 사용합니다. 컴포넌트가 사라질 때 리스너도 제거해야 이미 사라진 DOM을 대상으로 콜백이 계속 실행되는 일을 막습니다.

초기 상태와 프레임워크 처리

댓글 작성자는 matchMedia().matches가 동기적으로 읽힌다는 점을 덧붙입니다. React에서는 lazy useState 초기화 함수에서 처음 상태를 읽으면 첫 화면부터 실제 설정을 반영할 수 있습니다. 기본값으로 렌더링한 뒤 useEffect에서 상태를 고치면 화면이 한 번 잘못 표시됐다가 바뀌는 깜빡임이 생길 수 있습니다.

이 글은 resize에만 한정하지 말고 scroll이나 mousemove에서도 같은 원칙을 살피라고 제안합니다. 자주 발생하는 움직임 이벤트를 매번 확인하면서 몇 가지 상태만 구분한다면, 그 상태 자체의 변화를 알리는 브라우저 이벤트가 있는지 먼저 확인하라는 내용입니다.

dev.to 반응

  • @systemcraftdev — 플랫폼 이벤트의 세분성을 실제로 관심 있는 상태에 맞춘 좋은 예입니다. 프레임워크에서 쓸 때 덧붙일 점이 있습니다. matchMedia().matches는 동기적으로 읽히므로, 첫 페인트 전에 초기 상태를 확인할 수 있습니다. 추측한 기본값을 넣었다가 마운트 뒤 바로잡지 않아도 됩니다. 상태를 바로잡는 순간 깜빡이는 현상은 이 글에서 말한 resize 문제와 똑같지만, 컴포넌트 마운트 단계로 옮겨왔을 뿐입니다. React에서는 useEffect가 아니라 lazy useState 초기화 함수에서 읽으면 됩니다.

원문: dev.to / 번역·요약: Trawling