React 얕은 비교에 대한 얕은 오해

Jinhyuk Choi·2022년 12월 26일
8
post-thumbnail

의문의 시작

리액트 렌더링과 관련된 Velog의 포스팅에서 얕은 비교의 설명을 읽던 중 내가 알던 부분과 조금 다른 내용이 있는 것을 확인하였다.

객체를 비교할 때 얕은 비교의 경우 값을 비교하지 않고 참조(주소값)을 비교하며 깊은 비교의 경우 객체의 속성까지 모두 다 확인을 하는 것으로 알고 있었는데 해당 포스팅에서는 얕은 비교 역시 객체의 모든 필드를 확인한다고 되어있던 것이다.

확인해보기

Javascript 얕은 비교 깊은 비교라는 키워드로 바로 구글링을 해보았고 검색 결과 상단에 등장하는 대부분의 게시글에서도 내가 알고 있던 것과 동일한 방향으로 설명을 해주고 있었다. 하지만 가장 확실한 방법은 공식문서와 코드이니 우선 React에서 설명하고 있는 문서를 찾아보았다.

React 공식문서에서도 얕은 비교에 대해 다음과 같이 설명하고 있었다.

shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.
It does this by iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal.

대충 해석해보면 어쨋든 객체의 모든 Key에 대해 검사를 진행하며 Key의 값이 다를 경우 true를 반환한다고 한다.

그럼 얕은 비교는 어떻게 동작하는거지?

공식문서에서 객체의 모든 Key에 대해 검사를 진행한다고 하니 얕은 비교는 구체적으로 어떤식으로 동작하는지 궁금해졌다.

React Github에서 shallowEqual 코드를 확인해보자.

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

is 함수는 Object.is에 대한 폴리필이므로 Object.is로 이해하면 될 것 같다.
hasOwnProperty 역시 Object.prototype.hasOwnProperty이다.

hasOwnProperty는 prototype에서 가져오는 이유가 궁금하다면 여기서 확인하면 된다.

해당 코드를 해석해보면 당장에 for문을 통해 Object의 key와 value를 비교하는 것이 보인다. 얕은 비교의 경우 객체의 참조(주소값)만을 비교한다는 것은 틀린 사실이라는 것을 확인할 수 있다.

주소값만 비교한다는 말은 왜 나온걸까..?

그럼 객체의 참조(주소값)만을 비교한다는 말은 대체 왜 나온 것일까? 관련된 글을 쓴 사람들의 생각을 읽을 순 없으니 정확한 이유를 알아낼 수는 없지만 코드를 보면서 의심이 가는 부분들을 확인해보았다.

for문 내의 if문의 조건을 확인해보자.

if (
  !hasOwnProperty.call(objB, currentKey) ||
  !is(objA[currentKey], objB[currentKey])
) {
  return false;
}

비교 대상이 되는 객체에 해당 property가 존재하지 않거나 두 값을 비교했을 때 같지 않다면 false를 리턴 하도록 되어 있다.

여기서 주목할 것은 is인데 위에서 말했듯 Object.is와 같은 동작을 수행한다고 보면 된다. 이해를 돕기 위해 해당 함수에 대한 폴리필은 코드를 가져와보았다.

if (!Object.is) {
  Object.is = function(x, y) {
   // SameValue 알고리즘
    if (x === y) { // Steps 1-5, 7-10
     // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
   } else {
     // Step 6.a: NaN == NaN
     return x !== x && y !== y;
   }
  };
}

폴리필 코드를 보면 알겠지만 Object.is의 경우 -0, +0의 비교, Number.NaN, NaN의 비교를 제외하면 ===과 같은 결과값을 가지는데 ===의 경우 객체의 비교에서 참조(주소값)가 다르면 false를 return 한다.

얕은 비교의 경우 깊은 비교와 다르게 객체의 depth와 관계없이 자신과 비교대상의 Property의 값만을 비교하다 보니 주소값이 다르면 false를 return 한다는 설명이 나오게 됐던게 아닌가 예측해본다..!

실제 코드로 검증해보기

문서와 설명을 통해 얕은 비교가 주소값 비교만으로 끝나는게 아니라는 사실을 알았으니 실제로 확인을 해보고 싶어졌다. React에서 제공하고 있는 shallowCompare 함수를 약간 수정하여 브라우저에서 테스트를 해보았다.

TS 코드를 삭제하고 isObject.is로, hasOwnPropertyObject.prototype.hasOwnProperty로 바꾼 정도이니 검증에 큰 문제는 없을 것으로 보인다.

아래는 그 결과이다.

React의 shallowEqual 함수로 다른 주소값을 가진 객체 a, b를 비교하면 true가 나온다.

객체의 property의 값이 원시값이며 값이 같을때 역시 true를 반환한다.

하지만 객체의 property의 값이 객체일 경우 그 주소값이 다르기 때문에 property의 값들이 완전히 같아도 false를 반환하는 것을 볼 수 있다.

결론

막연한 궁금증에서 시작됐다보니 두서가 없는 글이 되어버렸지만 요약하면 다음과 같다.

1. 얕은 비교는 대상을 참조(주소값)으로 비교하지 않는다.
2. 객체일 경우 자신의 Property 전체에 대해 검사를 진행한다.
3. Property의 value에 대해서는 주소값으로 비교하는 것이 맞다. (Object.is 활용)
4. 그럼 뭐가 차이점이냐고 묻는다면..! 깊은 비교의 경우 객체를 만날 경우 재귀적으로 마지막 depth까지 비교를 진행한다.

생각해볼점

React.memo, useMemo, useCallback과 같은 성능 최적화를 위한 방식들에 얕은 비교가 사용되는 것으로 알고 있는데 위에서 본 것처럼 비교 대상의 property의 value에 객체가 존재할 경우 항상 false를 반환하여 의도한대로 동작하지 않게 되는 것일까? 위의 결과로 봤을 때는 오히려 연산을 증가시켜서 성능이 안 좋아질 것 같은데 정상적으로 동작할 때는 어느 정도로 개선이 되며 불필요한 연산이 추가 됐을 때는 어느 정도로 악화가 되는지 실제 코드로 동작시켜 보고 테스트해보고 싶다...! 내년의 나.. 화이팅!

Reference

profile
협업을 사랑하는 개발자

2개의 댓글

comment-user-thumbnail
2023년 6월 30일

리액트에만 해당하는 걸까요??

1개의 답글