21.6.1.2 DOM 리렌더링 통제를 위한 React.memo 메모이제이션(Memoization) 렌더링 격리 기법

21.6.1.2 DOM 리렌더링 통제를 위한 React.memo 메모이제이션(Memoization) 렌더링 격리 기법

백엔드에서 Node.js + WebSocket 을 통하여 로봇 1만 대의 스냅샷 데이터를 초당 1회씩 브라우저로 점잖게 스로틀링(Throttling)하여 React Zustand 전역 스토어에 착륙시켰다(21.6.1.1장 참조). 시스템이 완벽해 보이지만, 대시보드 화면(DOM) 단계를 그리는 찰나 다시 한번 프론트엔드의 성능 파멸 나락이 아가리를 벌린다.

1만 대의 로봇 정보가 들어있는 거대한 JSON 덩어리가 Zustand 를 강타할 때마다, 대충 짜인 React 컴포넌트는 “데이터가 바뀌었다” 고 착각하여 수백, 수천 개의 화면상 하위 로봇 아이콘(DOM Node)을 통째로 찢어(Destroy) 발기고 다시 그리기(Re-paint)를 반복한다. 이 미친 불필요한 리렌더링 쇄도(Re-rendering Avalanche) 는 브라우저 프레임 드랍(60FPS \rightarrow 5FPS 추락)과 함께 사용자의 관제 PC 메모리를 OOM으로 폭발시킨다.
본 절에서는 단 하나의 바이트 변경 값만 캐치하여 오직 그 좌표만 살짝 비트는, V8 엔진 통제술의 극의 React.memo 메모이제이션 기반의 정밀 핀포인트 DOM 격리 런북을 갈파한다.

1. 전역 Store 구독(Subscribe)의 함정과 폭포수(Waterfall) 렌더링

초보 개발자의 가장 처참한 관제탑 코드는 컴포넌트 최상단에서 전역 스토어를 통째로 퍼 마시는 행위다.

// [파멸로 직행하는 React.FC 렌더 코딩의 혐오스러운 예시]
const BigMapDashboard = () => {
    // 경악! 로봇 1만 개의 해시맵 덩어리 전체를 통째로 구독하고 서 있다.
    // 1만 대 로봇 중 딱 1대(Id: 7)의 좌표 하나가 바뀌어도 이 컴포넌트 전체가 다시 실행되며 폭주한다!
    const allRobots = useRobotStore(state => state.robots); 

    return (
        <div>
            {Object.entries(allRobots).map(([id, data]) => (
                // 1개가 바뀌어도 1만 개의 RobotIcon 자식(Children) 컴포넌트 전부가 다시 호출된다!
                <RobotIcon key={id} id={id} pose={data.pose} />
            ))}
        </div>
    );
};

1초마다 WebSocket이 들어오면, 저 코드는 1초마다 <div> 안의 1만 개 아이콘 메모리 트리를 허물고 가상 DOM(Virtual DOM) 대조 알고리즘 체계를 폭행하며 화면을 난도질한다. 관제 스크린의 팬(Fan)은 비행기 이륙 소리를 낼 것이다.

2. 정밀 타격(Pinpointing) 구독 스위칭과 메모이제이션 결속

이 폭주를 끊어내는 아키텍처는 극단적인 “부모 무지(Parent Ignorance)”“자식 단독 의존성(Child Isolation)” 체계다.
부모(BigMapDashboard)는 데이터를 전혀 모르고 그냥 빈칸(ID 리스트)만 그려주며, 데이터의 실질적 탈취는 오직 말단 세그먼트(Leaf Node)인 개별 RobotIcon 만이 단독으로 Zustand 에 빨대를 꽂는 구조로 갈아엎어야 한다.

그리고 그 말단 자식 컴포넌트들은 외벽에 철통 같은 방탄조끼(React.memo)를 둘러야 한다.

// [최정예 React.memo 핀포인트 서브스크라이빙 방어 런북]

// 1. 방탄조끼 장착! (React.memo)
// "이보시오 React 엔진, 내 프롭(props)이 같으면 절대로 나를 다시 부르지(Render) 마시오!" 강제 규약.
const RobotIcon = React.memo(({ id }) => {
    // 2. 부모가 데이터를 안 넘겨준다. 내 아이디값을 가지고 나 혼자 전역 스토어(Zustand)에 
    // 나만의 "초정밀 빨대" 를 꽂아 다른 놈들 말고 딱 내 데이터(State.robots[id])만 쳐다본다!
    const myPose = useRobotStore(state => state.robots[id]?.pose);

    // 내 데이터가 바뀌지 않으면 이 줄은 영원히 다시 불리지 않는다!
    return <div className="robot-dot" style={{ left: myPose.x, top: myPose.y }} />;
});

const BigMapDashboard = () => {
    // 3. 부모는 전체 1만 개의 맵 데이터 덩어리에 관심 없다. 
    // 오직 "로봇 아이디 목록(Keys)" 배열만 변했을 때만 렌더링을 허가한다! (Shallow Compare)
    const robotIds = useRobotStore(state => Object.keys(state.robots), shallow);

    return (
        <div>
            {robotIds.map(id => <RobotIcon key={id} id={id} />)}
        </div>
    );
};

3. DOM 불변성의 승리와 프레임 파워 복구

이 설계도면(Architecture)의 반전을 직시하라!
매 1초마다 Node.js WebSocket 이 수만 개의 로봇 좌표를 한꺼번에 Zustand Store에 집어던진다.

  1. 부모 컴포넌트 무자극: 로봇 아이디의 목록 자체는 변하지 않았으므로(새 로봇 추가/삭제 없음) BigMapDashboard 부모는 다시 렌더되지 않고 편안하게 수면 상태를 유지한다. (폭포수 파괴 완료)
  2. 독립 컴포넌트 정밀 핀포인트 렌더: Zustand 전역 엔진이 내부적으로 스토어를 갈아 끼우고, “어? 7번 로봇의 X 좌표가 바뀌었네?” 라며 오직 구독(Subscribe)이 트리거 된 단 1개의 RobotIcon (id=7) 에게만 펄스를 날린다.
  3. 절대 방파제 (Memoization): 1만 개의 RobotIcon 컴포넌트 중, 데이터가 바뀌지 않은 9,999 개의 노드는 React.memo 에 의해 가상 돔 대조 로직조차 컷팅당하며 철저하게 무시(Skip)된다.
    오로지 7번 로봇 요소 단 하나만! left 픽셀을 깔짝 움직이며 화면을 그린다.

이것이야말로 프론트엔드 엔진의 브라우저 레이턴시(React VDOM Diffing Cost)를 O(N) 에서 O(1) 혹은 업데이트된 노드 수(O(K)) 만큼으로 단칼에 후려치는 메모이제이션 지배술의 마스터피스다. 광란의 Zenoh 메쉬 백엔드가 아무리 엄청난 스루풋의 포를 쏘아대도, DOM 메모리 트리 계층에서는 오직 변화의 파동을 맞은 그 점(Node) 하나만 부드럽게 반짝이게 하는 궁극의 분단 렌더링(Segmented Rendering) 시스템이 관제 대시보드 생존의 마지노선인 것이다.