모던 리액트 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