모던 리액트 Deep Dive

1.1 자바스크립트의 동등 비교 - 리액트에서의 동등 비교

g*g 2024. 2. 6. 23:18

리액트에서 사용하는 동등 비교는 ==나 ===가 아닌 Object.is다.

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}


// 런타임에 Object.is가 있다면 그것을 사용하고, 아니라면 위 함수를 사용한다.
// Object.is는 인터넷 익스플로러 등에 존재하지 않기 때문에 폴리필을 넣어준 것으로 보인다.
const objectIs: (x: any, y: any) => boolean =
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

 
 
리액트에서는 objectIs를 기반으로 동등 비교를 하는 shallowEqual이라는 함수를 만들어 사용한다. 이 shallowEqual은 의존성 비교 등 리액트의 동등 비교가 필요한 다양한 곳에서 사용된다.

import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';

/**
 * 주어진 객체의 키를 순회하면서 두 값이 엄격한 동등성을 가지는지를 확인하고,
 * 다른 값이 있다면 false를 반환한다. 만약 두 객체 간에 모든 키의 값이 동일하다면
 * true를 반환한다.
 */
 
 // 단순히 Object.is를 수행하는 것뿐만 아니라 객체 간의 비교도 추가돼 있다.
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);

  // 배열의 길이가 다르다면 false
  if (keysA.length !== keysB.length) {
    return false;
  }

  // A의 키를 기준으로, B에 같은 키가 있는지, 그리고 그 값이 같은지 확인한다.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      // $FlowFixMe[incompatible-use] lost refinement of `objB`
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

export default shallowEqual;

 
 
리액트에서의 비교는 Object.is로 먼저 비교를 수행한 다음에 Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교를 한 번 더 수행하는 것을 알 수 있다. 객체 간 얕은 비교란 객체의 첫 번째 깊이에 존재하는 값만 비교한다는 것을 의미한다.

// Object.is는 참조가 다른 객체에 대해 비교가 불가능하다.
Object.is({hello:'world'}, {hello: 'word'}) // false

// 반면 리액트에서 구현한 shallowEqual은 객체의 1 depth 까지는 비교가 가능하다.
shallowEqual({hello:'world'}, {hello: 'word'}) // true

// 그러나 2 depth까지 가면 이를 비교할 방법이 없으므로 false를 반환한다.
shallowEqual({hello:{hi: 'world'}}, {hello:{hi: 'world'}}) // false
[출처] 리액트 딥다이브 1.1 동등비교|작성자 sheepdog13

 
 
이렇게 객체의 얕은 비교까지만 구현한 이유는 무엇일까? 먼저 리액트에서 사용하는 JSX props는 객체이고,그리고 여기에 있는 props만 일차적으로 비교하면 되기 때문이다. 다음 코드를 살펴보자.

type Props = {
   hello: string
}

function HelloComponent(props: Props){
   return <h1>{hello}</h1>
}


function App(){
   return <HelloComponent hello="hi" />
}

 
위 코드에서는 props는 객체다. 그리고 기본적으로 리액트는 props에서 꺼내온 값을 기준으로 렌더링을 수행하기 때문에 일반적인 케이스에서는 얕은 비교로 충분할 것이다. 이러한 특성을 안다면 props에 또 다른 객체를 넘겨준다면 리액트 렌더링이 예상치 못하게 작동한다는 것을 알 수 있다.

 

import { memo, useEffect, useState } from 'react'

type Props = {
   counter: number
}

const Component = memo((props: Props) => {
   useEffect(() => {
      console.log('Component has been rendered!')
})
   return <h1>{props.counter}</h1>
})

type DeeperProps = {
   counter: {
      counter: number
   }
}

const DeeperComponent = memo((props: DeeperProps) => {
   useEffect(() => {
      console.log('DeeperComponent has been rendered!')
})
   return <h1>{props.counter.counter}</h1>
})

export default function App(){
   const [counter, setCounter] = useState(0)
   
   function handleClick() {
      setCounter((prev) => prev + 1)
   }
   return (
      <div className="App">
         <Componet counter= {100} />
         <DeeperComponet counter= {{ counter: 100 }} />
         <button onClick={handleClick}>+</button>
      </div>
      )
}

 
이와 같이 props가 깊어지는 경우, 즉 한 객체 안에 또다른 객체가 있을 경우 React.memo는 컴포넌트에 실제로 변경된 값이 없음에도 불구하고 메모이제이션된 컴포넌트를 반환하지 못한다.
 
 
출처 - 모던 리액트 Deep Dive