← 전체 글로 돌아가기

Next.js

React ResizeObserver를 붙였다가 무한 렌더를 막은 실수 정리

컨테이너 폭을 읽어 카드 개수를 바꾸려다 ResizeObserver와 상태 업데이트가 서로를 다시 부른 문제를 정리했다.

화면 크기를 읽는 코드는 생각보다 상태를 자주 바꾼다

대시보드 카드의 열 수를 컨테이너 폭에 맞추려고 ResizeObserver를 붙였다. 처음에는 창 크기 이벤트보다 정확해서 만족스러웠다. 그런데 좁은 폭과 넓은 폭의 경계에서 화면이 미세하게 흔들리고, 개발자 도구 콘솔에는 관찰 루프 관련 경고가 보였다.

원인은 관찰 콜백에서 매번 setColumns를 호출한 데 있었다. 열 수가 이미 2여도 콜백이 올 때마다 새 렌더를 요청했고, 렌더 뒤 레이아웃이 다시 계산되면서 관찰이 이어졌다. 관찰 값과 UI 상태를 같은 것으로 취급한 것이 헷갈린 지점이었다.

먼저 값이 실제로 달라지는지 막기

상태 갱신 함수에서 이전 값과 비교하면 불필요한 렌더를 줄일 수 있다. 폭 자체를 상태에 넣기보다, 화면에 필요한 열 수만 계산했다.

useEffect(() => {
  const element = ref.current
  if (!element) return

  const observer = new ResizeObserver(([entry]) => {
    const next = entry.contentRect.width >= 900 ? 3 : 2
    setColumns((current) => (current === next ? current : next))
  })

  observer.observe(element)
  return () => observer.disconnect()
}, [])

여기서 기준 폭을 카드 최소 폭과 gap을 더해 정했다. 처음에는 768px 같은 익숙한 브레이크포인트를 썼지만, 실제 카드가 2열에서 잘리는 순간은 812px 근처였다. CSS의 minmax() 값과 자바스크립트 기준을 한 곳에 메모해 두는 편이 낫다.

관찰 콜백에서 레이아웃을 다시 바꾸지 않기

콜백 안에서 요소의 style.width를 바꾸고 다시 getBoundingClientRect()를 읽는 코드는 피했다. 읽기와 쓰기가 섞이면 같은 프레임에 레이아웃 계산이 반복될 수 있다. 표시 방식만 바꿀 일이라면 가능한 한 CSS 컨테이너 쿼리로 넘긴다.

.cards {
  container-type: inline-size;
}

@container (min-width: 900px) {
  .card-list { grid-template-columns: repeat(3, 1fr); }
}

자바스크립트는 차트처럼 실제 폭을 라이브러리에 전달해야 할 때만 남겼다.

적용 뒤 확인한 항목

  • 컴포넌트를 닫을 때 disconnect()가 호출되는가
  • 같은 계산 결과에 상태 업데이트를 하지 않는가
  • 모바일 회전과 사이드바 열기에서도 열 수가 안정적인가
  • Strict Mode 개발 환경에서 observer가 중복 등록되지 않는가

작은 리사이즈 기능도 관찰 대상, 상태, CSS가 서로 영향을 준다. 다음부터는 폭을 받자마자 상태로 저장하기보다, 그 값이 화면에서 정말 다른 결정을 만드는지 먼저 확인하려고 한다.