브라우저 API를 제대로 쓴다는 것의 의미

브라우저 API를 제대로 쓴다는 것의 의미

스크롤 이벤트와 WebSocket—둘 다 '동작'하지만, 브라우저가 이미 알고 있는 답을 굳이 다시 계산하고 있다면 그건 설계의 문제다.

IntersectionObserver 스크롤 성능 Core Web Vitals CRDT Yjs 실시간 협업 브라우저 API 프론트엔드 최적화
광고

스크롤 리스너가 달린 코드는 동작한다. getBoundingClientRect()를 호출하고, 엘리먼트가 뷰포트 안에 들어왔는지 계산하고, 이미지를 불러온다. 기능적으로는 완벽하다. 그런데 60fps 디스플레이에서 이 콜백은 초당 60번 실행된다. 매 호출마다 브라우저는 레이아웃을 강제로 재계산한다—개발자가 throttle이나 requestAnimationFrame을 덧붙여도 메인 스레드 위에서 레이아웃 연산이 일어난다는 사실은 바뀌지 않는다.

dev.to에 올라온 Parsa Jiravand의 글은 이 익숙한 패턴에 정확한 진단을 내린다. 브라우저는 이미 어떤 엘리먼트가 뷰포트에 있는지 알고 있다. IntersectionObserver는 그 답을 개발자에게 넘겨줄 뿐이다. 2019년부터 모든 주요 브라우저에서 baseline이 된 API지만, 실제 코드베이스에서는 여전히 스크롤 리스너가 더 많이 보인다. 이유는 단순하다—익숙하기 때문이다.

IntersectionObserver의 핵심은 '언제 물어보느냐'의 차이다. 스크롤 리스너는 픽셀이 움직일 때마다 질문한다. IntersectionObserver는 가시성 상태가 바뀔 때만 콜백을 호출한다. 지오메트리 계산은 메인 스레드 바깥에서 처리된다. threshold 옵션으로 엘리먼트의 몇 퍼센트가 보여야 콜백을 발동할지 제어할 수 있고, rootMargin으로 실제 뷰포트보다 300px 먼저 트리거를 걸어 네트워크 요청을 선제적으로 시작할 수도 있다. lazy loading, 애널리틱스 임프레션 트래킹, 무한 스크롤—이 모든 케이스에서 스크롤 리스너보다 의도가 명확하고, 성능 비용이 낮다.

그런데 이 이야기는 IntersectionObserver 하나로 끝나지 않는다. 실시간 협업 편집 도구를 만들며 비슷한 교훈을 얻은 사례가 있다. Prashant Patil이 dev.to에서 공유한 경험이다. WebSocket으로 모든 편집 이벤트를 브로드캐스트하면 실시간 협업처럼 보인다—두 사용자가 동시에 같은 문서를 편집하기 전까지는. WebSocket은 메시지를 전달한다. 어떤 변경이 '이겨야' 하는지는 알지 못한다. 마지막으로 도착한 업데이트가 이전 작업을 덮어쓰는 Last Write Wins 문제가 발생하고, 한 사용자의 작업이 조용히 사라진다.

해결책은 WebSocket을 버리는 게 아니었다. 책임을 올바르게 분리하는 것이었다. CRDT(Conflict-free Replicated Data Type) 라이브러리인 Yjs를 도입하자 구조가 달라졌다. 전체 문서 상태 대신 개별 연산(6번 위치에 'Amazing' 삽입)을 동기화한다. 각 연산은 고유 식별자를 가지고, 도착 순서에 무관하게 모든 클라이언트가 동일한 최종 상태로 수렴한다. WebSocket은 통신 채널로 남고, Yjs가 동기화와 충돌 해결을 담당한다. 역할이 명확해지자 버그가 사라졌다.

두 사례는 표면적으로 다른 문제처럼 보이지만 같은 지점을 가리킨다. 브라우저(또는 라이브러리)가 이미 풀어놓은 문제를 개발자가 직접 다시 풀고 있지는 않은가. 스크롤 리스너로 뷰포트를 계산하는 것, WebSocket만으로 동시성을 제어하려는 것—둘 다 '동작'하지만 브라우저나 런타임이 제공하는 추상화 계층을 무시하고 아래 레이어에서 직접 싸우는 패턴이다. 그 대가는 성능 손실, 메모리 누수, 예측 불가능한 버그로 돌아온다.

프론트엔드 개발자로서 Core Web Vitals를 고민할 때, 가장 먼저 살펴볼 곳은 번들 사이즈나 이미지 최적화만이 아니다. getBoundingClientRect를 스크롤 핸들러 안에서 호출하는 코드, 전체 상태를 직렬화해서 웹소켓으로 던지는 코드—이런 패턴들이 쌓이면 아무리 프레임워크를 최적화해도 메인 스레드는 조용히 막힌다. Parsa의 표현이 정확하다. "코드베이스에서 getBoundingClientRect가 포함된 스크롤 이벤트 리스너를 찾아라. 그것들 모두가 교체 후보다."

브라우저 API를 제대로 쓴다는 것은 최신 API를 아는 것이 아니다. 어떤 문제를 누가 이미 풀어놨는지를 먼저 파악하고, 그 경계 위에서 설계하는 것이다. IntersectionObserver가 가시성을 관찰하고, Yjs가 동시성 충돌을 해결하는 동안, 개발자가 집중해야 할 것은 그 위에서의 사용자 경험이다. 도구의 책임 범위를 정확히 이해할 때, 비로소 진짜 설계가 시작된다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요