본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.1.4 리액트에서의 동등 비교

by Whiimsy 2024. 3. 29.

1.1.4 리액트에서의 동등 비교

  • 리액트에서 사용하는 동등 비교는 ==나 ===가 아닌 Object.is
  • Object.is는 ES6에서 제공하는 기능이기 때문에 리액트에서는 이를 구현한 폴리필(Polyfill)을 함께 사용(웹 개발에서 기능을 지원하지 않는 웹 브라우저 상의 기능을 구현하는 코드)

objectIs

  • 아래는 리액트에서 실제로 값을 비교할 때 사용하는 코드
    // flow로 구현돼 있어 any가 추가돼 있음. flow에서 any는 타입스크립트와 동일하게 어떠한 값도 받을 수 있는 타입을 의미함
    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
    )
    }
    

/**

  • ChatGPT의 도움!
  • 이 코드는 두 개의 값 xy가 서로 동일한지를 확인하는 함수입니다. 이 함수는 JavaScript의 동등성 비교를 통해 두 값이 같은지 여부를 확인합니다.
  • 여기서 사용된 조건들을 하나씩 살펴보겠습니다:
    1. x === y: xy가 동등한지 비교합니다. 즉, 값 뿐만 아니라 타입도 일치해야 합니다.
    1. (x !== 0 || 1 / x === 1 / y): 만약 x가 0이 아니면, xy가 숫자라면 그들의 역수가 서로 같은지를 비교합니다. 이 부분은 0으로 나누는 경우를 피하기 위한 안전장치입니다.
    1. (x !== x && y !== y): xy가 NaN (Not-a-Number) 인지를 확인합니다. NaN은 자기 자신과도 동일하지 않습니다.
  • 따라서 이 함수는 xy가 동등하면 true를 반환하며, 동등하지 않으면 false를 반환합니다.
  • /

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

export default objectIs


## shallowEqaul
- 리액트에서는 이 objectIs를 기반으로 동등 비교를 하는 shallowEqual 함수를 만들어 사용함
- 이 shallowEqual 함수는 의존성 비교 등 리액트의 동등 비교가 필요한 다양한 곳에서 사용
- ```js
  import is from './objectIs'
  // 다음 코드는 Object.prototype.hasOwnProperty
  // 이는 객체에 특정 프로퍼티가 있는지 확인하는 메서드
  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) ||
        !is(objA[currentKey], objB[currentKey])
      ) {
        return false
      }
    }

    return true
  }

  export default shallowEqual 
  • 리액트에서의 비교를 요약하자면 Object.is로 먼저 비교를 수행한 다음에 Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교(객체의 첫 번째 깊이에 존재하는 값만 비교)를 한 번 더 수행함

  • // Object.is는 참조가 다른 객체에 대해 비교가 불가능
    Object.is({ hello: 'world' }, { hello: 'world' }) // false
    
    // shallowEqual은 객체의 1 depth까지는 비교 가능
    shallowEqual({ hello: 'world' }, { hello: 'world' }) // true
    
    // 그러나 2 depth까지 가면 이를 비교할 방법이 없으므로 false 반환
    shallowEqual({ hello: { hi: 'world' } }, { hello: { hi: 'world' } }) // false
  • 객체의 얕은 비교까지만 구현한 이유? 먼저 리액트에서 사용하는 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 [, 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>
      )
    }
  • 이와 같이 props가 깊어지는 경우, 즉 한 객체 안에 또 다른 객체가 있을 경우 React.memo는 컴포넌트에 실제로 변경된 값이 없음에도 불구하고 메모이제이션된 컴포넌트를 반환하지 못함

  • 즉, Component는 props.counter가 존재하지만, DeeperComponent는 props.counter.counter에 props가 존재함

  • 상위 컴포넌트인 App에서 버튼을 클릭해 강제로 렌더링을 일으킬 경우, shallowEqual을 사용하는 Component 함수는 위 로직에 따라 정확히 객체 간 비교를 수행해 렌더링을 방지해 주지만 DeeperComponent 함수는 제대로 비교하지 못해 memo가 작동하지 않는 모습을 볼 수 있음

  • 만약 내부에 있는 객체까지 완벽하게 비교하기 위한 재귀문까지 넣었으면 객체 안에 객체가 몇 개까지 있는지 알 수 없으므로 이를 재귀적으로 비교하려 할 경우 성능에 악영향 미칠 것