리액트 함수 컴포넌트와 훅을 반복적으로 작성하다 보면 의존성 배열(dependencies array)에 대한 고민이 생김
보통은 리액트에서 제공하는 eslint-react-config에 선언돼 있는 react-hooks/exhaustive-deps의 도움을 받아 해당 배열을 채우곤 함
이것이 어떤 식으로 작동하는지, 또한 왜 이러한 변수들을 넣어야 하는지 이해하지 못하는 경우가 많은데, 리액트 컴포넌트의 렌더링이 일어나는 이유 중 하나가 바로 props의 동등 비교에 따른 결과임
이 props의 동등 비교는 객체의 얕은 비교를 기반으로 이루어짐
원시 타입과 객체 타입의 가장 큰 차이점 = 값을 저장하는 방식 차이
이 값을 저장하는 방식의 차이가 동등 비교를 할 때 차이를 만드는 원인
먼저 원시 타입은 불변 형태의 값으로 저장된다. 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장된다
반면 객체는 프로퍼티를 삭제, 추가, 수정할 수 있으므로 원시 값과 다르게 변경 가능한 형태로 저장되며, 값을 복사할 때도 값이 아닌 참조를 전달하게 됨
객체는 값을 저장하는 게 아닌 참조를 저장하기 때문에 동일하게 선언했던 객체라 하더라도 저장하는 순간 다른 참조를 바라보기 때문에 false를 반환하게 됨
Object.is는 두 개의 인수를 받으며, 이 인수 두 개가 동일한지 확인하고 반환하는 메서드
==와 다른 점: == 비교는 같음을 비교하기 전에 양쪽이 같은 타입이 아니라면 비교할 수 있도록 강제로 형변환을 한 후에 비교함
Object.is는 이러한 형변환 작업을 하지 않음
===와 다른 점: -0 === +0 // true 를 나타낼 때,
Object.is(-0, +0) // false 를 나타냄
즉, 좀 더 개발자가 기대하는 방식으로 정확한 비교
리액트에서 사용하는 동등 비교는 ==, ===가 아닌 Object.is임
ES6에서 제공하는 기능이기 때문에 이를 구현한 Polyfill을 함께 사용
리액트에서는 objectIs를 기반으로 동등 비교를 하는 shallowEqual 이라는 함수를 만들어 사용함
의존성 비교 등 리액트의 동등 비교가 필요한 다양한 곳에서 사용
리액트에서의 비교를 요약하자면 Object.is로 먼저 비교를 수행한 다음에 Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교를 한 번 더 수행하는 것을 알 수 있음
Object.is는 참조가 다른 객체에 대해 비교 불가능
반면 리액트 팀에서 구현한 shallowEqual은 객체의 1 depth까지 비교 가능. 그러나 2 depth까지 가면 이를 비교할 방법이 없으므로 false를 반환
shallowEqual 코드 예시
Object.is({ hello: 'world' }, { hello: 'world' }) // false
shallowEqual({ hello: 'world' }, { hello: 'world' }) // true
shallowEqual({ hello: {hi: 'world'} }, { hello: { hi: 'world' } })
// false
객체의 얕은 비교까지만 구현한 이유 코드 예시
type Props = {
hello: string
}
function Component(props:Props) {
return <h1>{props.hello}</h1>
}
// ...
function App() {
return <Component hello="hi"/>
}
React.memo의 깊은 비교 문제 예시
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가 작동하지 않는 모습을 볼 수 있음
자바스크립트의 특징을 잘 숙지한다면 함수 컴포넌트에서 사용되는 훅의 의존성 배열의 비교, 렌더링 방지를 넘어선 useMemo와 useCallback의 필요성, 렌더링 최적화를 위해서 꼭 필요한 React.memo를 올바르게 작동시키기 위해 고려해야 할 것들을 쉽게 이해할 수 있음