노트

구조적 공유

Structural Sharing

프론트엔드#react#performance · 연결된 개념 5개

쉽게 말하면

구조적 공유는 지도를 고칠 때 바뀐 동네 한 장만 새로 그리고 나머지 장은 옛 지도책과 함께 쓰는 거예요. 옛 지도책은 그대로 남고, 새 데이터도 안 바뀐 부분은 예전 객체를 써서 화면은 바뀐 곳만 다시 그려요.

비유의 한계 어느 장이 그대로인지 알려면 한 장씩 예전 것과 비교해 봐야 해요. 이 비교도 비용이라 데이터가 아주 크면 오히려 느려지고, JSON으로 표현되는 값에서만 동작해요.

구조적 공유(Structural Sharing)는 새 버전의 데이터를 만들 때 바뀌지 않은 부분은 이전 버전의 객체를 그대로 가리키게 하는 기법이다. 바뀐 노드와 그 노드까지 올라가는 경로만 새로 만들고, 나머지 가지는 두 버전이 함께 쓴다. 불변 데이터를 다루면서도 매번 전체를 복사하지 않게 해 준다.

이전: [A, B, C]      다음: [A, B', C]
next !== prev        (배열 자체는 새 객체)
next[0] === prev[0]  A 재사용
next[1] !== prev[1]  B만 새 객체
next[2] === prev[2]  C 재사용

React에서 왜 중요한가

React는 바뀌었는지를 대개 참조 비교(Object.is)로 판단한다. memo는 props를 하나씩 Object.is로 비교하고, useMemo·useEffect 같은 훅도 의존성을 하나씩 Object.is로 비교한다(memo·useMemo·useCallback). 안 바뀐 항목이 같은 참조로 남아 있으면 <ProductCard product={product} /> 목록에서 바뀐 카드만 다시 렌더된다. 반대로 내용이 같아도 참조가 새로 생기면 메모이제이션이 매번 깨진다.

TanStack Query의 structuralSharing

서버에서 다시 받은 JSON은 파싱 직후 전부 새 객체다. TanStack Query는 이전 데이터와 새 데이터를 깊게 비교해 같은 부분은 이전 참조로 바꿔 넣는다. 아무것도 안 바뀌었으면 data 자체가 이전 객체 그대로다. 그래서 refetch가 일어나도 실제로 바뀐 데이터를 쓰는 컴포넌트만 리렌더된다.

  • 기본값 true. structuralSharing: false로 끄거나 (oldData, newData) => 결과 함수를 넘겨 직접 구현할 수 있다
  • queryFn이 JSON으로 표현되는 값을 반환할 때만 동작한다. 그 밖의 값(Date, Map, 클래스 인스턴스 등)은 내용이 같아도 늘 바뀐 것으로 본다
  • select를 쓰면 원본 결과와 select 결과에 한 번씩, 두 번 적용된다
  • 아주 큰 데이터에서는 비교 자체가 병목이 될 수 있다. 참조 유지가 필요 없으면 끈다

다른 곳의 구조적 공유

  • 상태를 불변으로 갱신하면 같은 효과가 저절로 난다. { ...state, user: { ...state.user, name } }는 바깥 객체와 user만 새로 만들고 다른 필드는 이전 참조를 그대로 둔다(React 상태 갱신)
  • 같은 아이디어가 함수형 언어의 영속 자료구조(Persistent Data Structure)에 있다. 연결 리스트 앞에 원소를 붙이면 꼬리는 복사하지 않고 두 리스트가 공유한다. 트리는 바뀐 노드까지의 경로만 복사한다(path copying). Clojure의 벡터, Immutable.js가 이 방식이다(연결 리스트)

출처: React — memo: Minimizing props changes · TanStack Query — Render Optimizations: structural sharing · TanStack Query — useQuery · TanStack Query — Important Defaults · React — useMemo · TkDodo — React Query Render Optimizations · Wikipedia — Persistent data structure · Immutable.js

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 재조정(Reconciliation)

    재조정(Reconciliation)은 React가 이전 렌더의 엘리먼트 트리와 새 트리를 비교(Diffing)해, 실제 DOM(Document Object Model)에 필요한 최소한의 변경만 반영하는 과정이다. 이 비교를 맡는 재조정자는 react 패키지가 아니라 react-dom 같은 렌더러 번들 안에 들어 있다. 두 트리를 완벽하게 비교하면 O(n³)이라, React는 두 가지 가정을 두어 O(n)으로 줄인다.

  • 상태 내리기와 컴포지션으로 리렌더 줄이기

    상태 내리기(State Colocation)는 state를 실제로 쓰는 컴포넌트 가까이로 옮겨, state가 바뀔 때 다시 렌더되는 범위를 좁히는 기법이다. 메모이제이션(Memoization)보다 먼저 시도할 구조적 최적화다.

  • localStorage와 sessionStorage

    브라우저에 문자열 키-값을 저장하는 Web Storage API의 두 가지. 쿠키와 달리 요청에 자동으로 실리지 않고, 출처 단위로 나뉘며(same-origin-policy), 보통 출처당 5MB 안팎을 쓸 수 있다. 동기 API라 큰 데이터를 자주 읽고 쓰면 메인 스레드를 막는다.

  • 변수·컬렉션 캡슐화

    넓은 범위에서 쓰이는 데이터에 직접 접근하지 못하게 하고, 읽고 쓰는 함수를 통해서만 다루게 하는 리팩터링. 데이터를 옮기거나 바꿀 때 고쳐야 할 곳이 접근 함수 한 곳으로 좁아진다.

  • React.cache

    cache(fn)은 서버 컴포넌트(Server Component)용 React API(Application Programming Interface)로, 함수 결과를 한 번의 서버 요청(렌더) 동안만 기억한다. 같은 요청 안에서 같은 인자로 여러 번 불러도 실제 실행은 한 번이다. React 19 정식 API이며 서버 컴포넌트에서만 쓴다.

보기 옵션