React에서 DOM을 '덜 믿을수록' 빨라지는 이유

React에서 DOM을 '덜 믿을수록' 빨라지는 이유

Canvas API, Uncontrolled Component, react-hook-form이 공통으로 말하는 것—React가 DOM을 놓아줄 때 성능이 열린다.

Canvas API React 성능 최적화 Uncontrolled Component react-hook-form D3.js DOM 렌더링 번들 사이즈 모바일 퍼스트
광고

이거, 단순한 차트 라이브러리 얘기가 아닙니다

최근 velog에 올라온 글 하나가 꽤 오래 머릿속에 남았습니다. 사내 차트 라이브러리를 처음엔 Recharts로, 그다음엔 D3.js로 만들다가 결국 Canvas API로 직접 구현했다는 이야기인데요. 읽으면서 계속 이런 생각이 들었습니다. '이건 차트 얘기가 아니라 React에서 DOM을 어디까지 믿어야 하냐는 얘기잖아.'

그리고 이 질문은 Controlled vs Uncontrolled Component, react-hook-form, 반응형 레이아웃 전략까지 전부 같은 맥락으로 연결됩니다. 오늘은 그 연결고리를 짚어보려고 합니다.

HexaChart 하나가 DOM 노드 65개를 만든다는 사실

D3.js로 레이더 차트(HexaChart)를 구현하면 내부적으로 무슨 일이 벌어질까요? <svg> 루트 하나에 <defs> 안에 gradient, filter, clipPath 정의들이 주렁주렁 달리고, <polygon>, <line>, <circle>, <text> 노드가 줄지어 생성됩니다. 해당 사례에 따르면 HexaChart 하나에 DOM 노드 65개 이상이 생성됩니다.

Canvas 버전은요? <canvas> 엘리먼트 딱 1개입니다. 나머지는 전부 픽셀 연산으로 처리됩니다.

시각적 결과물은 사실상 동일합니다. 글래스모피즘 마커의 glow가 미세하게 다를 뿐, 일반 사용자는 구분하지 못한다고 필자도 인정합니다. 그런데 같은 그림을 그리기 위해 뒤에서 벌어지는 일이 완전히 다른 거예요.

렌더링 속도보다 '구조적 비용'이 문제입니다

단일 렌더링 기준으로는 Canvas가 0.15ms, D3+SVG가 0.28ms로 약 1.9배 차이가 납니다. 둘 다 1ms 미만이니 사용자가 체감할 수준은 아닙니다. 그래서 이 글의 결론이 'D3는 느리다'가 아닌 게 중요합니다.

진짜 문제는 구조적 비용입니다. D3+SVG 방식은 React와 DOM 소유권을 놓고 계속 충돌합니다:

  • React: "내가 Virtual DOM을 Real DOM으로 변환한다."
  • D3: "내가 d3.select → .append → .attr로 직접 DOM을 조작한다."

이 충돌을 해결하는 방법이 세 가지 있는데, 어느 것도 깔끔하지 않습니다. D3에 DOM을 완전히 맡기면 React 최적화를 전혀 못 씁니다. React가 SVG 구조를 그리면 D3의 transition 같은 핵심 기능을 못 씁니다. useMemo로 D3를 계산기로만 쓰면 SVG 애니메이션은 별도 구현이 필요합니다. 피그마 시안에서 설계한 애니메이션을 코드로 옮기다 보면 이 구조적 갈등이 구체적인 타협으로 나타납니다.

반면 Canvas는 <canvas ref={canvasRef}> 하나 잡고 useEffect 안에서 자유롭게 그리면 됩니다. React와 DOM 소유권 싸움이 처음부터 없습니다.

번들 사이즈도 무시 못 합니다

Canvas API는 브라우저 네이티브입니다. 추가 다운로드가 필요 없습니다. D3 버전은 d3-selection, d3-scale, d3-shape, d3-transition, d3-ease를 합치면 minified 기준 약 42KB, gzipped 기준 약 16KB가 추가됩니다.

Lighthouse 점수를 챙기거나 Core Web Vitals에 예민한 분들이라면 이 숫자가 예사롭지 않게 보일 겁니다. 특히 차트가 뷰포트 아래에 있어서 초기 로딩에 직접 기여하지 않는 경우라면, 번들에 16KB를 추가할 명분이 더 흐릿해집니다. 로딩 스켈레톤 넣고 지연 로딩 고민하는 것보다, 애초에 의존성을 줄이는 게 훨씬 근본적인 접근입니다.

이건 폼(Form)에서도 똑같은 이야기입니다

흥미로운 건 이 구조적 패턴이 차트에만 국한된 게 아니라는 점입니다. React에서 폼을 다루는 방식에서 동일한 갈등이 반복됩니다.

Controlled Component는 타이핑할 때마다 setState가 발생하고 리렌더링이 일어납니다. React가 값의 주인입니다. 상태 추적이 명확하고 값 제어가 직관적이지만, 입력 필드가 많아질수록 렌더링 비용이 빠르게 증가합니다.

Uncontrolled Component는 DOM이 값의 주인입니다. React는 submit이나 validation 같은 필요한 순간에만 값에 접근합니다. 입력 중에는 React 리렌더링이 발생하지 않습니다.

react-hook-form이 Uncontrolled 기반을 채택한 이유가 정확히 여기에 있습니다. register로 input을 연결하면, React는 해당 필드의 입력 과정에서 손을 뗍니다. DOM이 직접 관리하고, React는 submit 시점에만 개입합니다. 불필요한 리렌더를 구조적으로 차단하는 겁니다. Canvas가 DOM 노드 생성 자체를 없앤 것처럼, react-hook-form은 폼 입력의 리렌더링 자체를 없앱니다.

반응형도 결국 같은 질문입니다

미디어 쿼리 실습 사례에서도 비슷한 인사이트가 나옵니다. "PC 버전부터 만들고 모바일은 나중에 바꾸면 되겠지"라는 접근이 왜 틀렸는지를 몸으로 배운 이야기인데요.

모바일 퍼스트로 기본 스타일을 잡고, min-width로 확장하는 방식이 코드를 훨씬 깔끔하게 만듭니다. 원칙은 동일합니다. 기본 상태를 최소한으로 잡고, 필요한 시점에만 개입한다. Uncontrolled Component가 필요한 순간에만 DOM에 접근하는 것과, 모바일 퍼스트가 필요한 브레이크포인트에서만 스타일을 추가하는 것—구조적으로 같은 사고방식입니다.

시사점: React는 '덜 개입할수록' 빨라집니다

세 가지 사례를 꿰뚫는 핵심은 하나입니다.

React가 DOM을 덜 믿고, 덜 개입할수록 성능이 열린다.

  • Canvas API는 DOM 노드 생성을 없앰으로써 React-DOM 충돌 자체를 제거합니다.
  • react-hook-form은 입력 중 리렌더링을 없앰으로써 폼의 렌더링 비용을 구조적으로 낮춥니다.
  • 모바일 퍼스트 미디어 쿼리는 기본 스타일을 최소화하고 필요한 시점에만 규칙을 추가합니다.

물론 이게 항상 정답은 아닙니다. D3는 복잡한 좌표 변환이나 지도 프로젝션 같은 영역에서 Canvas로 직접 구현하면 끔찍할 수 있습니다. Controlled Component는 실시간 값 제어가 필요한 경우 여전히 올바른 선택입니다. 기획자가 요구하는 인터랙션 수준에 따라 전략은 달라집니다.

전망: '구조적 선택'이 점점 더 중요해집니다

React 19의 변화, Server Components의 확산, 그리고 번들 사이즈와 Core Web Vitals에 대한 압박이 높아지면서, 앞으로는 무엇으로 만드냐보다 어떤 구조로 설계하냐가 성능 격차를 더 크게 만들 것입니다.

Canvas API 기반 차트를 직접 만들어버린 그 결정이 단순한 '스불재'로 읽히지 않는 이유가 여기 있습니다. 그건 라이브러리 선택의 문제가 아니라, React에서 DOM 소유권을 어떻게 배분할 것인지에 대한 구조적 판단이었습니다. 그리고 그 판단은—차트든, 폼이든, 레이아웃이든—언제나 유효한 질문으로 남습니다.

기획자가 이걸 의도한 건지 모르겠지만, React가 DOM을 덜 믿을수록 사용자는 더 빠른 화면을 만나게 됩니다.

출처

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