React

재는 행위가 크기를 바꾸면 화면이 폭주한다

  • #AI
  • #Claude Code
  • #React
  • #CSS
  • #성능최적화

문제: 차트가 무한히 커진다

  • 화면 — LCA 결과 요약
  • 증상 — 차트가 계속 커지며 레이아웃이 깨지고 깜빡거림
  • 특징 — 멈추지 않는다. 가만히 둬도 계속 커진다

폭을 재봤더니 약 1,200만 px까지 갔다. 매 사이클 약 3.1배씩 커지고 있었다.

원인: CSS 한 줄이 만든 피드백 루프

탭 패널의 컨테이너가 이랬다.

display: grid;
grid-template-rows: ...;
/* grid-template-columns 없음 */

grid-template-columns를 지정하지 않으면 암묵적 컬럼이 생기고, 그 폭은 auto(= max-content) 가 된다. 즉 “안에 든 내용만큼 넓어져라” 는 뜻이다.

여기에 ECharts가 width: 'auto'로 들어가 있었다. 그러면 이런 순환이 생긴다.

  1. 차트가 컨테이너 폭을 잰다
  2. 그린 SVG의 고유 너비가 생긴다
  3. 컬럼이 max-content그 너비만큼 컬럼 트랙이 밀린다
  4. 컨테이너가 넓어진다
  5. ECharts가 넓어진 컨테이너를 재측정한다
  6. 1번으로 돌아간다

측정 결과가 측정 대상을 바꾸는 구조다. 브라우저와 차트 라이브러리 각각은 정상 동작하는데, 둘을 연결하니 발산한다.

수정은 한 줄이다.

grid-template-columns: minmax(0, 1fr);

1fr만 쓰면 안 된다. grid 아이템의 기본 min-widthauto 라 내용이 크면 트랙이 밀려나기 때문이다. minmax(0, 1fr)로 최솟값을 0으로 못 박아야 “부모가 준 폭 안에서만 살아라”가 된다.

적용하니 1324px에서 안정화됐다.

같은 결함군을 전수 점검

한 곳을 고치고 끝내지 않고, “컨테이너 width 제약 누락” 이라는 결함군으로 보고 정적 분석으로 훑었다. 7곳이 더 나왔다.

대상 조치
탭 패널 2곳 gridTemplateColumns 추가
grid 컬럼 미지정 2곳 minmax(0, 1fr)
flex-column 래퍼 min-width: 0 추가
ECharts 래퍼 4곳 width: 100% 명시

flex 컨테이너에 min-width: 0을 넣는 것도 같은 이유다. flex 아이템의 기본 min-widthauto 라, 내용이 크면 부모 밖으로 밀고 나간다. grid의 minmax(0, ...)와 정확히 같은 문제의 flex 버전이다.

정직하게 남긴 한계도 있다.

eco-view GHG 페이지는 테스트 계정에 데이터가 없어 차트 라이브 재현은 불가했으나 동일 결함군에 표준 패턴을 적용.

라이브로 재현·수정 확인한 곳은 한 곳이고, 나머지는 같은 패턴이라 함께 고친 것이다. 이 둘을 구분해 적어두지 않으면 나중에 “다 검증했다”로 잘못 읽힌다.

같은 함정, 다른 얼굴: ResizeObserver

며칠 뒤 다른 화면에서 같은 성질의 버그를 만났다.

  • 화면 — STEP2 서브탭 바
  • 증상 — 깜빡임
  • 구조 — ResizeObserver가 폭을 재서 좁으면 접고(isNarrow) 넓으면 펼친다

문제는 무엇을 쟀는가였다. 관찰 대상이 서브탭의 “내용 컨테이너” 였다.

  • 펼친 상태 — 탭이 다 보여 폭이 넓다 → 임계값 초과 → ‘접음’
  • 접은 상태 — 드롭다운만 남아 폭이 줄어든다 → 임계값 미만 → ‘펼침’
  • 폭이 임계값 근처면 — 무한 진동

이것도 앞의 차트와 정확히 같은 구조다. 관측 결과가 관측 대상의 크기를 바꾼다.

추적이 까다로웠던 이유가 하나 있다.

redux 액션 없이 순수 setState 루프, 서브탭 바 DOM 이 초당 수백 노드 재생성

상태 관리 라이브러리를 거치지 않는 로컬 setState 루프라, Redux DevTools의 액션 로그에 아무것도 안 남는다. 화면은 미친 듯이 깜빡이는데 개발도구는 조용하다.

수정은 측정 대상을 바꾼 것이다.

  • 이전 — 접힘/펼침에 따라 폭이 변하는 내용 컨테이너
  • 이후 — 접힘/펼침에 흔들리지 않는 외부 래퍼(부모 nav의 가용 폭)

규칙으로 정리하면

두 사건에서 같은 규칙이 나온다.

측정값으로 무언가를 바꿀 거라면, 그 변화에 영향받지 않는 것을 재라.

  • 차트 — 부모가 준 폭을 재야 한다. 자기가 그린 결과가 반영된 폭을 재면 안 된다
  • 서브탭 — 접힘 여부와 무관한 바깥 폭을 재야 한다. 접히면 변하는 안쪽을 재면 안 된다

CSS에서는 이게 “내용 기반 크기(auto / max-content)를 피드백 경로에 두지 말라” 로 나타난다.

상황 위험한 값 안전한 값
grid 트랙 auto (암묵 컬럼 포함) minmax(0, 1fr)
grid/flex 아이템 min-width: auto (기본값) min-width: 0
차트 크기 width: 'auto' width: 100%

auto는 “알아서 해줘”라 편해 보이지만, 누가 누구를 기준으로 삼는지가 애매해진다. 그 애매함이 순환을 만든다.

이런 버그의 성질

두 건 모두 공통점이 있었다.

  • 에러가 안 난다 — 예외도, 콘솔 경고도 없다. 브라우저 입장에선 시킨 대로 한 것뿐이다
  • 개별 코드는 다 정상이다 — CSS도 맞고 차트 설정도 맞다. 연결했을 때만 발산한다
  • 개발도구에 안 잡힐 수 있다 — Redux 액션도, 네트워크 요청도 없는 순수 렌더 루프
  • 임계값 근처에서만 터진다 — 서브탭 건은 폭이 애매할 때만 진동한다. 그래서 “내 화면에선 되는데요”가 나온다

그래서 증상으로 검색하기가 어렵다. “차트가 커져요”로는 안 나온다. “무엇이 무엇을 재고, 그 결과가 재는 대상을 바꾸는가” 를 손으로 그려봐야 보인다. 실제로 두 건 다 원인을 찾은 방법이 코드 검색이 아니라 순환을 종이에 그려본 것이었다.