[React] 리렌더 줄이기 (feat. Zustand)

이성우·2026년 1월 10일

React를 쓰다 보면 "리렌더를 줄여야 된다" 라는 말을 자주 하고 듣게 됩니다.

👀 React 리렌더

React에서 리렌더는

화면을 다시 그린다(x)
함수를 다시 실행한다(o)가 맞습니다.
React 컴포넌트는 함수라서 리렌더가 곧 컴포넌트 함수 다시 호출을 의미하기 때문입니다.

리렌더가 발생하는 상황은 아래와 같습니다.

  • useState setter 호출
  • 부모가 리렌더되어 자식도 같이 리렌더
  • props가 바뀔 때
  • context값이 바뀔 때
  • zustand같은 내가 구독 중인 값이 바뀔 때

여기서 React가 무엇이 바뀌었는지 판단하기 위해 참조라는 개념을 알아야 합니다.

값 vs 참조

Primitive (값 자체로 비교)

number,string,boolean,null,undefined,symbol ...

1 === 1 // true
"hi" === "hi" // true

Object (참조로 비교)

object,array,function,Map,Set ...

{} === {} // false
[] === [] // false
(() => {}) === (() => {}) // false

React와 / Zustand에서 같은지 비교할 때 자주 쓰는 기준이

  • primitive는 값 비교
  • Object는 참조(주소) 비교

React에서 참조를 유지하는 훅

useRef

useRef는 값이 바뀌어도 리렌더를 일으키지 않음

  • 렌더 사이에 값을 유지
  • .current를 바꿔도 리렌더 발생X
const posRef = useRef({x: 0, y: 0});
posRef.current.x = 10; // 리렌더X

useMemo

useMemo는 렌더마다 새 객체를 만들면 매번 참조가 바뀌는 것을 방지 (필요할 때만 새로 만듦)

의존성이 바뀔 때만 새 값을 생성. 그 이외에는 이전 값을 재사용

const config = useMemo(() => ({a,b}),[a,b]);

useCallback

useCallback은 렌더마다 새 함수가 만들어지는 것을 방지

의존성이 바뀔 때만 새 함수를 생성. 그 이외에는 이전 함수를 재사용

const onClick = useCallbak(() => {
  add(value);
},[value]);



👀 Zustand에서의 리렌더

Zustand의 useStore(selector)
1. 내가 고른 값(selector)를 저장
2. store가 업데이트될 때마다 selector를 다시 실행
3. 이전 결과와 현재 결과가 다르면 리렌더
이런 방식으로 동작합니다.

근데 3번의 다르다라는 판단은 Object.is 로 비교합니다.

🔎 Object.is란 두 값이 완전히 같은 값인지를 판단하는 JavaScript의 동등성 비교 함수.(===과는 조금 다름)

그래서 selector가 object/array/function 같은 참조 타입을 매번 새로 만들어 반환하면,
값이 같아 보여도 참조가 달라져서 매번 다르다고 판단되어 리렌더가 발생할 수 있습니다.

selector가 매번 "새 객체" 반환

const { a,b } = useAppStore((s) => ({ a: s.a, b:s.b}));

해결 방법: useShalow | shallow로 얕은 비교

const { a, b } = useAppStore(useShallow((s) => ({ a: s.a, b: s.b })));

const { a,b } = useAppStore((s) => ({a: s.a, b:s.b}),shallow);

구독하지 않고 참조

구독 하지 않고 필요할 때 현재의 값을 읽고 싶을때는 아래와 같은 방법이 있습니다.

현재 상태를 한 번 읽기

const state = useAppStore.getState();
console.log(state.count);
  • 리렌더 발생X
  • 값 자동 갱신X (단발성 값 읽기 용도)

값 변화는 하지만 React의 리렌더는 직접 통제하기

useEffect(() => {
  const unsub = useAppStore.subscribe(
    (s) => s.count,
    (count) => { ... }
  );
  return unsub;
}, []);

보통 UI 리렌더가 아닌 사이드 이펙트 처리에 사용.




💡결론

  • 리렌더 = 함수 재실행(화면 다시 그림 X)
  • primitive는 값 비교, object/array/function은 참조가 같아야 같음
  • useMemo/useCallback은 리렌더를 막는 게 아니라 참조를 안정화하는 것
  • Zustand는 selector 결과를 기본적으로 Object.is로 비교
  • selector에서 객체를 반환해야 하면 shallow/ useShallow 사용
  • getState()는 단발성 값 읽기(리렌더 X, 자동 갱신 X)
  • subscribe()는 리렌더 없이 값 변경 감지 가능하지만 cleanup(unsub)이 필수
profile
안녕하세요!

0개의 댓글