모던 리액트 Deep Dive - Chapter 01-1. 동등 비교

동등 비교
이전 글: chapter 0. 왜 리액트인가?
0. 동등 비교를 왜 알아야 할까?
'의존성 배열(dependencies)'과 'props', 'state' 등에서 '변경'되었는지를 판단하는 방법이기 때문에 알 필요가 있다.
활용 예시: 가상 DOM vs 실제 DOM, 컴포넌트를 렌더링할지 판단, 변수 및 함수의 메모이제이션 등
1. 데이터 타입
이미 다 아는 것들이니 생략(원시, 참조)
- 주의:
typeof null === "object"
2. Object.is(a, b) since ES6
2-1. Object.is란?: 개발자가 예상가능한 '원시 타입' 값 간의 비교가 추가됨
원시 타입에 한해서 살펴보자.
Javascript에서는 먼저 ==가 있다. 하지만 이는 강제로 형변환을 하기에 동등 비교에는 적합하진 않다.
===는 동등 비교에 적합하나 몇 가지 주의할 점이 있다. 이는 개발자가 예상하지 못한 방식의 비교일 수 있다.
js-0 === +0; // false를 예상하지만 true NaN === NaN; // true를 예상하지만 false
이를 보완한 동등비교 방식인 Object.is는 ES6부터 등장하였고, React는 내부적으로 동등 비교를 할 때 이를 활용하여 사용하고 있다.
jsObject.is(-0, +0); // false Object.is(NaN, NaN); // true
물론 객체간의 비교는
===와 동일하게 동작한다.
2-2. 모든 버전에서 사용 가능한 ObjectIs를 구현한 React
하지만 ES6부터만 호환되는 Object.is이기에, React는 이를 구현한 폴리필(Polyfill)을 함께 사용하여 선언한 objectIs로 이전 버전에서도 사용 가능하도록 하였다.
ts// 폴리필 function is(x: any, y: any) { // -0과 +0일 때, 1/-0은 -Infinity, 1/+0은 +Infinity임으로 이를 활용하여 false 처리 // NaN은 다른 원시 타입과 다르게 어떤 NaN(심지어 자기 자신)과도 다르므로 이를 활용하여 true 처리 return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); } // ES6버전의 경우 Object.is는 undefined const ObjectIs: (x: any, y: any) => boolean = typeof Object.is === "function" ? Object.is : is; export default objectIs;
3. shallowEqual: objectIs를 포함하여 객체의 얕은 비교(첫 번째 깊이)까지
React에서는 원시 타입의 값 뿐만 아니라, 참조 타입인 객체까지 비교하는 shallowEqual을 사용하고 있다.
객체 간의 비교는 얕은 비교 수준이며, 이를 통해 react는 두 값의 동등 비교를 구현하였다.
참고: mixed 타입은 모든 유형의 슈퍼타입으로, typescript에서는 없는 타입 선언이지만 React에서는 Flow 타입 체커 툴을 사용하고 있다.(Flow에서 mixed 타입)
주의: 왜
objA.hasOwnProperty를 사용하지 않고 빌트인 객체의Object.prototype.hasOwnProperty.call를 사용할까?
tsimport is from "./objectIs"; // 이 코드는 그냥 Object.prototype.hasOwnProperty이다. import hasOwnProperty from "./hasOwnProperty"; /** * @flow * // flow 타입체커를 사용할 파일이다 선언! */ function shallowEqual(objA: mixed, objectB: mixed) { // 원시 타입의 값, 혹은 참조 타입의 참조값이 같나? if (is(objA, objB)) { return true; } // 진짜 객체만 걸러내기 - objA 또는 objB가 원시 타입 값인가?: typeof obj가 'object'가 아니거나, 'object'여도 obj === null이면 됨 if ( typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null ) { // 원시타입임이에도 is를 통과하지 못했으므로 false return false; } // 참조값이 다른 객체간의 비교. 키와 프로퍼티간의 비교 const keysA = Object.keys(objA); const keysB = Object.keys(objB); // 키의 길이가 다르면 다르다. if (keysA.length !== keysB.length) { return false; } for (let i = 0; i < keysA.length; i++) { const currentKey = keysA[i]; if ( !hasOwnProperty.call(keysB, currentKey) || !is(objA[currentKey], objB[currentKey]) // 한 단계만 ) { return false; } } return true; }
4. 왜 객체의 얕은 비교까지만?
그럼 React는 왜 객체 간의 비교는 얕은 비교, 즉 '첫 번째' 깊이에 존재하는 값끼리만 비교하도록 구현했을까? 이유는 크게 두 가지다.
이유1: JSX props는 기본적으로 객체이며, 이 첫 번쨰 깊이에 있는 값들만 일차적으로 비교하면 되기 때문(props vs props)
- 때문에 props가 깊어지는 경우(props 안에 객체) 예상치 못한 리렌더링이 발생할 수도 있다.
```tsx
// 부모 컴포넌트의 state가 변경되어도 props.counter는 여전히 100이기에 메모이제이션 되어 리렌더링 되지 않는다
const Component = memo((props: Props) => {
useEffect(() => {
console.log("컴포넌트의 렌더링이 완료되었음");
});
return <h1>{props.counter}</h1>;
});
// 부모 컴포넌트의 state가 변경되면 props.counter는 객체이므로 전후가 다른 참조값이기 때문에 이전 props와 이후 props가 다르게 판단되므로 가지므로 리렌더링된다.
const DeeperComponent = memo((props: DeeperProps) => {
useEffect(() => {
console.log("Deeper 컴포넌트의 렌더링이 완료되었음");
});
return <h2>{props.counter.counter}</h2>;
});
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>
);
```
이유2: 이를 모두 커버하기 위해 재귀적으로 shallowEqual을 호출하면 객체가 몇 개까지 있을지 알 수 없으므로 성능에 악영향을 미칠 수 있다.
아직 댓글이 없습니다
첫 번째 댓글을 작성해보세요!