모던 리액트 Deep Dive - Chapter 01-1. 동등 비교

작성일: 2025년 11월 19일 오전 03:12(마지막 수정: 2025년 11월 26일 오후 04:04)
조회수: 75

동등.png

동등 비교

이전 글: chapter 0. 왜 리액트인가?

0. 동등 비교를 왜 알아야 할까?

'의존성 배열(dependencies)'과 'props', 'state' 등에서 '변경'되었는지를 판단하는 방법이기 때문에 알 필요가 있다.

활용 예시: 가상 DOM vs 실제 DOM, 컴포넌트를 렌더링할지 판단, 변수 및 함수의 메모이제이션 등


1. 데이터 타입

이미 다 아는 것들이니 생략(원시, 참조)

  • 주의: typeof null === "object"

2. Object.is(a, b) since ES6

2-1. Object.is란?: 개발자가 예상가능한 '원시 타입' 값 간의 비교가 추가됨

원시 타입에 한해서 살펴보자.

Javascript에서는 먼저 ==가 있다. 하지만 이는 강제로 형변환을 하기에 동등 비교에는 적합하진 않다.

===는 동등 비교에 적합하나 몇 가지 주의할 점이 있다. 이는 개발자가 예상하지 못한 방식의 비교일 수 있다.

js
-0 === +0; // false를 예상하지만 true
NaN === NaN; // true를 예상하지만 false

이를 보완한 동등비교 방식인 Object.is는 ES6부터 등장하였고, React는 내부적으로 동등 비교를 할 때 이를 활용하여 사용하고 있다.

js
Object.is(-0, +0); // false
Object.is(NaN, NaN); // true

물론 객체간의 비교는 ===와 동일하게 동작한다.

2-2. 모든 버전에서 사용 가능한 ObjectIs를 구현한 React

하지만 ES6부터만 호환되는 Object.is이기에, React는 이를 구현한 폴리필(Polyfill)을 함께 사용하여 선언한 objectIs로 이전 버전에서도 사용 가능하도록 하였다.

ts
// 폴리필
function is(x: any, y: any) {
    // -0과 +0일 때, 1/-0은 -Infinity, 1/+0은 +Infinity임으로 이를 활용하여 false 처리
    // NaN은 다른 원시 타입과 다르게 어떤 NaN(심지어 자기 자신)과도 다르므로 이를 활용하여 true 처리
    return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}

// ES6버전의 경우 Object.is는 undefined
const ObjectIs: (x: any, y: any) => boolean =
    typeof Object.is === "function" ? Object.is : is;

export default objectIs;

React 깃허브 코드


3. shallowEqual: objectIs를 포함하여 객체의 얕은 비교(첫 번째 깊이)까지

React에서는 원시 타입의 값 뿐만 아니라, 참조 타입인 객체까지 비교하는 shallowEqual을 사용하고 있다.

객체 간의 비교는 얕은 비교 수준이며, 이를 통해 react는 두 값의 동등 비교를 구현하였다.

참고: mixed 타입은 모든 유형의 슈퍼타입으로, typescript에서는 없는 타입 선언이지만 React에서는 Flow 타입 체커 툴을 사용하고 있다.(Flow에서 mixed 타입)

주의: objA.hasOwnProperty를 사용하지 않고 빌트인 객체의 Object.prototype.hasOwnProperty.call를 사용할까?

ts
import is from "./objectIs";
// 이 코드는 그냥 Object.prototype.hasOwnProperty이다.
import hasOwnProperty from "./hasOwnProperty";

/**
 * @flow
 * // flow 타입체커를 사용할 파일이다 선언!
 */

function shallowEqual(objA: mixed, objectB: mixed) {
    // 원시 타입의 값, 혹은 참조 타입의 참조값이 같나?
    if (is(objA, objB)) {
        return true;
    }
    // 진짜 객체만 걸러내기 - objA 또는 objB가 원시 타입 값인가?: typeof obj가 'object'가 아니거나, 'object'여도 obj === null이면 됨
    if (
        typeof objA !== "object" ||
        objA === null ||
        typeof objB !== "object" ||
        objB === null
    ) {
        // 원시타입임이에도 is를 통과하지 못했으므로 false
        return false;
    }

    // 참조값이 다른 객체간의 비교. 키와 프로퍼티간의 비교
    const keysA = Object.keys(objA);
    const keysB = Object.keys(objB);
    // 키의 길이가 다르면 다르다.
    if (keysA.length !== keysB.length) {
        return false;
    }

    for (let i = 0; i < keysA.length; i++) {
        const currentKey = keysA[i];
        if (
            !hasOwnProperty.call(keysB, currentKey) ||
            !is(objA[currentKey], objB[currentKey]) // 한 단계만
        ) {
            return false;
        }
    }
    return true;
}

React 깃허브 코드


4. 왜 객체의 얕은 비교까지만?

그럼 React는 왜 객체 간의 비교는 얕은 비교, 즉 '첫 번째' 깊이에 존재하는 값끼리만 비교하도록 구현했을까? 이유는 크게 두 가지다.

이유1: JSX props는 기본적으로 객체이며, 이 첫 번쨰 깊이에 있는 값들만 일차적으로 비교하면 되기 때문(props vs props)

-   때문에 props가 깊어지는 경우(props 안에 객체) 예상치 못한 리렌더링이 발생할 수도 있다.

```tsx
// 부모 컴포넌트의 state가 변경되어도 props.counter는 여전히 100이기에 메모이제이션 되어 리렌더링 되지 않는다
const Component = memo((props: Props) => {
    useEffect(() => {
        console.log("컴포넌트의 렌더링이 완료되었음");
    });

    return <h1>{props.counter}</h1>;
});

// 부모 컴포넌트의 state가 변경되면 props.counter는 객체이므로 전후가 다른 참조값이기 때문에 이전 props와 이후 props가 다르게 판단되므로 가지므로 리렌더링된다.
const DeeperComponent = memo((props: DeeperProps) => {
    useEffect(() => {
        console.log("Deeper 컴포넌트의 렌더링이 완료되었음");
    });

    return <h2>{props.counter.counter}</h2>;
});

export default function App() {
    const [, setCounter] = useState(0);

    function handleClick() {
        setCounter((prev) => prev + 1);
    }
}

return (
    <div className="App">
        <Component counter={100} />
        <DeeperComponent counter={{ counter: 100 }} />
        <button onClick={handleClick}>+</button>
    </div>
);
```

이유2: 이를 모두 커버하기 위해 재귀적으로 shallowEqual을 호출하면 객체가 몇 개까지 있을지 알 수 없으므로 성능에 악영향을 미칠 수 있다.

0개의 댓글
💬

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!