본문 바로가기

Deep Dive

[React] Re-rendering과 함수 참조 관리

 

 

✏️ Re-rendering(리렌더링)이란?

  • React가 컴포넌트를 다시 실행하여 새로운 결과물을 생성하면, 그것을 지켜보고 있던 Virtual DOM이 기존 컴포넌트와 현재 컴포넌트의 차이점(diff)을 발견하여 실제 DOM의 변경 사항만 효율적으로 업데이트하는 것.

 

 

✏️ Re-rendering Trigger (컴포넌트가 리렌더링 되는 조건)

1. 컴포넌트의 props가 변경되거나,

2. 컴포넌트의 state가 업데이트될 때

기본적인 조건은 이 두 가지이지만,

 

3. context가 변경될 때

- Context 값이 변경되면, 해당 Context를 사용하는 모든 컴포넌트가 리렌더링 된다.

 

4. 부모 컴포넌트가 리렌더링 될 때 자식 컴포넌트가 리렌더링 된다.

- 이때 React.memo를 사용하면 불필요한 리렌더링을 방지할 수 있다.

const MemoizedComponent = React.memo(function Component({ onClick }) {
  return <button onClick={onClick}>Hello!</button>;
});

 

이런 추가 조건들도 존재한다.

 

 

✏️ 리렌더링과 함수 참조

  • 컴포넌트가 리렌더링 될 때마다 컴포넌트의 함수 전체가 다시 실행된다.
  • 이때 컴포넌트 내부에 정의된 모든 함수들은 새로운 메모리 주소(참조값)를 할당받게 되는데, 이는 JavaScript의 함수가 참조 타입(Reference Type)이기 때문이다.

 

✏️ 함수 참조 변경으로 발생하는 문제

  • 자식 컴포넌트에 props로 전달된 함수의 참조값이 변경되어 불필요한 리렌더링이 발생할 수 있으며,
  • useEffect의 의존성 배열에 포함된 함수가 새로운 참조값을 가지면서 의도치 않은 side-effect가 실행될 수 있다.

    이는 성능 저하뿐만 아니라, 앞서 말했던 의도치 않은 Side-effect가 발생하면서 예상치 못한 동작을 유발할 수 있다.

 

✏️ 이런 현상이 왜 발생하는 걸까?

  • 원시 타입(Primitive Type)은 값 자체를 복사하기 때문에 값이 변경되어야 새로운 값이라고 인식하지만,
  • 참조 타입(Reference Type)은 참조값(메모리 주소)이 복사되므로 값이 동일해도 메모리 주소(참조값)가 다르면,
    React가 다른(새로운) 함수로 인식하기 때문이다. 
// Primitive
let a = 10;
let b = 10;
a === b; // true (값이 같으면 같은 데이터로 인식)

// Reference
const obj1 = { count: 1 };
const obj2 = { count: 1 };
obj1 === obj2; // false (내용이 같아도 참조 주소가 다르면 다른 데이터로 인식)

// Reference Type에서 동일한 참조
const obj3 = obj1; // obj3는 obj1과 같은 참조를 가리킴
console.log(obj1 === obj3); // true (참조 주소가 같아야 같은 데이터로 인식)

 

 

 

✏️ 함수 참조 변경 문제를 해결하려면?

  • Hook을 통하여 함수 참조를 관리해 주면 되는데, 이때 가장 많이 쓰이는 hook은 useCallback과 useRef이다.
  • 둘의 차이점은 아래와 같으며, 상황에 맞게 사용하면 된다.
    • useCallback : 함수를 memoization 하며, 의존성 배열을 갖고 있다. 의존성 배열이 변경되기 전까지 동일한 함수 참조를 유지한다.
    • useRef : 초기화 시점에 생성된 참조를 컴포넌트의 생명 주기 동안 유지하며, 의존성 배열을 관리하지 않아도 된다.

 

아래는 useCallback과 useRef를 사용하는 예시이다.

 

1. useCallback 사용 예시

import React, { useState, useCallback } from 'react';

// Child Component
const Child = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Child</button>;
});

// Parent Component
const Parent = () => {
  const [count, setCount] = useState(0);

  // handleClick 함수가 계속 실행됨
  const handleClick = () => {
    console.log("isClicked");
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* handleClick이 매번 새롭게 생성되어 Child Component가 리렌더링됨 */}
      <Child onClick={handleClick} />
    </div>
  );
};
  • 위 예시는 Child Component에서 React.memo가 적용된 상태이지만, Parent Component의 내부 함수인 handleClick 함수가 매번 새롭게 생성되어 Child Component가 불필요하게 계속 리렌더링 되는 예시이다.

 

// Parent Component
const Parent = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("isClicked");
  }, []); // 의존성 배열을 비워두면 초기 렌더링 이후 참조값이 유지된다.

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      {/* handleClick의 참조값이 유지되므로 불필요한 리렌더링 방지 */}
      <ChildComponent onClick={handleClick} />
    </div>
  );
};
  • 위 예시처럼 useCallback을 통해 의존성 배열로 참조값을 저장해 놓으면, handleClick의 참조값이 유지되어 Parent Component가 리렌더링 되어도 handleClick 함수가 재실행되지 않는다.
  • 함수 참조로 인해 불필요한 리렌더링을 방지할 때 useCallback을 사용하는 것을 권장한다.

 

 

2. useRef 사용 예시

import React, { useRef, useEffect } from 'react';

const Timer = () => {
  const cbRef = useRef();

  // Timer function
  const startTimer = () => {
    cbRef.current = setInterval(() => {
      console.log("Timer is running");
    }, 1000);
  };

  useEffect(() => {
    startTimer();

    // cleanup 함수로 컴포넌트가 언마운트될 때 타이머 정리
    return () => clearInterval(cbRef.current);
  }, []);
};

export default Timer;
  • 위 예시는 useRef로 cbRef의 current 값을 유지함으로써 컴포넌트가 리렌더링 되더라도 동일한 참조값을 유지하며, useEffect의 cleanup 함수로 컴포넌트가 언마운트될 때 clearInterval을 실행하여 메모리 누수까지 방지해 준다.
  • useRef는 리렌더링과 무관하게 특정 데이터(함수 포함)를 저장하고 재사용할 때 적합하다. 

 

이 밖에도 함수를 값으로 memoization 하는 useMemo라던지, 함수를 컴포넌트 외부에 정의하여 안정적인 참조를 유지하는 useReducer 등의 방법도 있다.

 

다만, useCallback과 useMemo는 React 19에서 'use' hook으로 통합될 예정이라고 하니, 참고하면 좋을 것 같다.