리액트에서 동등 비교는 ==나 ===가 아닌 Object.is이다.
Object.is는 ES6에서 제공하는 기능이기 때문에 리액트에서는 이를 구현한 폴리필(Polyfill)을 함께 사용한다.
(폴리필(Polyfill)은 특정 웹 브라우저에서 지원하지 않는 최신 기능이나 API를, 구형 브라우저에서도 사용할 수 있도록 구현한 코드나 라이브러리를 말합니다. 리액트와 관련하여 폴리필은 주로 브라우저 간 호환성을 보장하기 위해 사용됩니다.)
//flow로 구현돼 있어 any가 추가돼 있다. flow에서 any는 타입스크립트와 동일하게 어떠한 값도 받을 수 있는
//타입을 의미한다.
function is(x: any, y:any){
return(
(x === y) && ( x !==0 || 1 / x===1/y)) || (x !== x && y !==y)//eslint-disabled-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
React는 브라우저 호환성을 위해 Object.is를 사용할 수 없는 환경에서도 동일한 기능을 제공하기 위해 폴리필을 포함시킨다.
typeof Object.is === 'function'으로 런타임에서 Object.is 지원 여부를 확인하고, 지원하지 않으면is 함수를 사용한다.
이 방식으로 React는 최신 브라우저와 구형 브라우저 모두에서 일관된 동작을 보장합니다.
리액트에서는 이 objectIs를 기반으로 동등 비교를 하는 shallowEqual이라는 함수를 만들어서 사용한다.
이 shallowEqual은 의존성 비교 등 리액트의 동등 비교가 필요한 다양한 곳에서 사용된다.
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 fasle
}
//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 shllowEqual
리액트에서 비교를 요약하자면 Object.is로 먼저 비교를 수행한다.
그 다음에 Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교를 한 번 더 수행하는 것을 알 수 있다.
객체 간 얕은 비교란 객체의 첫 번째 깊이에 존재하는 값만 비교한다는 것을 의미한다.
//Object.is는 참조가 다른 객체애 대해 비교가 불가능하다.
Object.is({hello:'world'}, {hello: 'world'}) //false
//반면 리액트 팀에서 구현한 shallowEqual은 객체의 1depth까지는 비교가 가능하다.
shallowEqual({hello: 'world'}, {hello: 'world'}) //true
//그러나 2depth까지 가면 이를 비교할 방법이 없으므로 false랄 반환한다.
shallowEqual({hello:{hi:'world'}}, {hello: {hi: 'world'}}) //false
이렇게 객체의 걑은 비교까지만 구현한 이유는 무엇일까?
리액트에서 사용하는 JSX props는 객체이고, 그리고 여기에 있는 props만 일차적으로 비교하면 되기 때문이다.
type Props = {
hello: string
}
function HelloComponent(props:Props){
return <h1>{props.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 defualt 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가 작동하지 않는 모습을 볼 수 있다.
만약 내부에 있는 객체까지 완벽하게 비교하기 위한 재귀문까지 넣었으면 어떻게 됐을까?
객체 안에 객체가 몇 개까지 있을지 알 수 없으므로 이를 재귀적으로 비교하려 할 경우 성능에 악영향을 미칠 것이다.