React + zustand의 메모리와 리랜더링

Jinmin Kim·2025년 4월 18일

📌 1. JavaScript 메모리 모델

  • 스택(Stack)
    • 함수 호출 컨텍스트, 지역 변수(원시값 또는 주소) 보관
  • 힙(Heap)
    • 객체·배열·함수 등의 실제 데이터 저장
  • 원시 타입 vs 참조 타입
    • 원시 타입(숫자·문자열·불리언): 값 자체를 스택에 저장
    • 참조 타입(객체·배열·함수): 힙에 저장된 데이터의 주소(참조) 를 스택에 저장
(Heap)
────────────
0xA1{ test: 'a' }
0xB1{ test: 'b' }

스택(Stack)
────────────
let obj = 0xA1   // obj 변수는 '0xA1' 참조(주소) 보관

📌 2. 얕은(Shallow) vs 깊은(Deep) 비교

  • 얕은 비교 (shallow equality)
    • 최상위 프로퍼티의 참조(주소)를 === 검사
    • 중첩 객체까지 검사하지 않음
    • 예:
const a = { x: 1, y: { z: 2 } };
const b = { x: 1, y: { z: 2 } };
shallowEqual(a, b); // false (y 참조가 다름)
  • 깊은 비교 (deep equality)
    • 중첩된 모든 레벨의 값까지 재귀 검사
    • 비용은 높지만 더 엄밀

📌 3. 얕은 vs 깊은 복사

  • 얕은 복사 (shallow copy)
    • 1단계 프로퍼티만 새 객체/배열 생성
    • 중첩된 객체는 기존 참조 유지
    • 예: const copy = { ...orig };
  • 깊은 복사 (deep copy)
    • 중첩된 모든 객체/배열까지 새 인스턴스 생성
    • 예: const copy = JSON.parse(JSON.stringify(orig)); (한계 존재)

📌 4. Zustand와 shallow 비교

  • Selector + shallow
    const { tableData, ... } = historyStore(
      ( { state, handler } ) => ({
        tableData: state.tableData,      // 또는 state.tableData[index]
        /* ... */
      }),
      shallow
    );
    
    • selector가 반환한 객체의 최상위 프로퍼티얕은 비교
    • 같은 참조면 리렌더링 X, 다른 참조면 리렌더링 ✅

📌 5. Immer의 produce와 참조 변경

Zustand의 setter(예: setTableListData) 내부에서 Immer produce를 쓰면,

어디를 수정하느냐에 따라 새로운 참조가 어디까지 발생하는지가 달라집니다.

수정 형태동작 설명참조 변경 범위shallow 감지
① 최상위 재할당draft.state.tableData = newData;tableData 프로퍼티 전체에 새 배열(또는 객체) 할당최상위 tableData
② 1단계 요소 교체draft.state.tableData[i] = newItem;Immer는 배열 전체를 복사한 뒤, i번째 요소만 바꾼 새 배열 생성최상위 tableData
③ 2단계 이하 중첩 속성 수정draft.state.tableData[i].foo = x;해당 요소 객체만 복사(새 인스턴스), 최상위 배열 참조는 그대로 유지요소 객체만 복사❌ (최상위 같음)

📌 6. state.tableData vs state.tableData[index] 구독 비교

  1. tableData: state.tableData 구독
    • 배열 전체 참조를 구독 → 최상위 참조가 바뀔 때만 리렌더링
  2. tableData: state.tableData[index] 구독
    • 특정 요소의 참조만 구독 → 그 요소의 참조가 바뀔 때만 리렌더링
// A. 배열 전체 재할당
produce(prev, draft ⇒ draft.state.tableData = [...])
// → newData 배열 요소 객체를 어떻게 쓰느냐에 따라
//    same-element-ref → no render  |  new-element-ref → render

// B. 요소 교체 (index 일치 시)
produce(prev, draft ⇒ draft.state.tableData[idx] = newItem)
// → 요소 idx 참조 변경 → render

// C. 요소 내부 속성만 변경
produce(prev, draft ⇒ draft.state.tableData[idx].foo = x)
// → 최상위 참조 유지 → no render (특정 요소 구독 시 render if idx matches)

📌 7. 예제로 보는 참조 변경 & 리렌더링

초기 상태

// 힙                 ┌──────────────────────────┐
// 0x01 → [A, B]      │ A = {test:'a'} @0xA1    │
//         └──────────┤ B = {test:'b'} @0xB1    │
// 스택                └──────────────────────────┘
// let sub0 = state.tableData[0]; // sub0 → 0xA1

Case A: 새 배열만 생성 (요소 재사용)

const newArr = [...state.tableData];
// newArr @0x02, but newArr[0] @0xA1
produce(prev, draft ⇒ { draft.state.tableData = newArr; });
let newSub0 = state.tableData[0]; // 0xA1
// sub0(0xA1) === newSub0(0xA1) → shallow 같음 → no render

Case B: 배열+요소 모두 복제

const newArr = state.tableData.map(item  ({ ...item }));
// newArr @0x03, newArr[0] @0xC1 (새 객체)
produce(prev, draft ⇒ { draft.state.tableData = newArr; });
let newSub0 = state.tableData[0]; // 0xC1
// sub0(0xA1) !== newSub0(0xC1) → shallow 다름 → render

Case C: 요소 내부 속성만 변경

produce(prev, draft ⇒ {
  draft.state.tableData[0].test = 'x';
});
// 임시 객체 A 복제 @0xD1, but array stays @0x01
let newSub0 = state.tableData[0]; // 0xD1
// sub0(0xA1) !== newSub0(0xD1) → 해당 요소 구독 시 render;
// 그러나 배열 전체 구독 시 array ref same → no render

📌 8. 핵심 요약

  • 변수 할당: 참조 타입은 주소를 복사
  • 불변성 유지: Zustand에서는 새로운 참조가 있어야 구독 컴포넌트가 인지
  • Produce 동작: 어디를 쓰느냐(최상위 / 요소 / 중첩)에 따라 참조 생성 범위가 달라짐
  • 구독 범위에 따라 리렌더링 여부 변경
    • 배열 전체 구독 → 최상위 참조 변경 시 인지
    • 요소 단위 구독 → 해당 요소 참조 변경 시 인지

🔥🔥한마디로🔥🔥 ‼️중요‼️

리액트에서 값이 변경되면, 새로운 참조값으로 데이터를 넣어주기때문에
옵저버가 그것이 변경되었다고 인식할수있는것이다.

javscript의 기본형 참조형을 생각하게되었을때
당연히 데이터가 안바뀔거라고 나는 다시 생각했으나,

리액트의 기본 동작원리를 생각하게되면,

const {
        testData,
    } = testStore(
        ({ state, handler }) => ({
            testData: state.testData
        }),
        shallow,
    );

일때에, testData의 데이터가 변경되어서 새로운 값이 되었을때,
testData는 새로운 참조주소를 받게된다.
‼️그래서 랜더가 될지안될지 파악하게된다‼️

profile
Let's do it developer

0개의 댓글