데이터타입
자바스크립트에는 원시형과 객체형 이라는 데이터 타입의 두 가지 범주가 존재합니다.
원시 타입
변수 할당 시점에 메모리 영역을 차지하고 저장되기 때문에 불변 형태의 값으로 저장된다.
(boolean, number, string, null, undefined, Symbol, Bigint)
객체 타입
반면 객체는 언제나 프로퍼티를 삭제, 추가, 수정할 수 있기 때문에 원시형과 달리 변경 가능한 형태로 저장되며, 참조 주소값을 변수에 할당받는다. (배열, 함수 , 정규식, 클래스 등.. )
이러한 특성에 의해 객체타입의 경우 내부 프로퍼티가 변경 되더라도 기존의 참조주소를 유지하므로 서로 다른객체로 판단하지 않게됩니다.
그렇다면 자바스크립트는 자료형의 비교를 정확히 어떻게 수행할까요?
JS의 동등성 비교: === 일치 연산자 vs Object.is 메서드
데이터의 값과 자료형이 모두 같은지 파악하는 ‘===’ 일치 연산자는 원시 자료형의 경우 동등함의 판단을 보장합니다.
Object.is도 ===연산자와 쓰임세는 거의 비슷하지만
JAVASCRIPT
-0 === +0 // true
Number.NaN === NaN // false
Object.is(Number.NaN, NaN) // true
NaN === 0 / 0 // false
Object.is(NaN, 0 / 0) // true
와 같은 일부 상황에서 좀 더 까다로운 동등 비교를 수행해준다는 장점이 있죠.
하지만 === 연산자와 Object.is메서드 모두 객체 자료형의 동등성을 판단하는 것에는 한계가 명확한데요,
이것을 극복하기 방법으로는 무엇이 있을까요 ?
리액트에서의 동등비교
React의 데이터의 동등 비교는 내부적으로 구현된 shallowEqual 비교함수를 통해 수행하는데요,
shallowEqual의 기능을 요약하면 다음과 같습니다.
- Object.is 로 동등 비교 수행,
- 객체의 경우 첫 최상위 속성만을 비교하는 Shallow Comparison(얕은비교) 를 수행합니다.
이를 통해 원시형과 객체형 데이터를 효과적으로 비교할 수 있습니다.
React의 shallowEqual 함수 내부 구현 코드
DART
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
} // 두 자료를 Object.is 로 먼저 동등 비교한다 (원시형의 경우 여기서 먼저 걸러지겠죠 ?)
// 객체형의 경우 불변성을 유지시키지 않는다면 true가 반환되어 변경을 감지할 수 없습니다.
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// objA와 objB가 동일한 키를 가지고 있으며, 각 키에 해당하는 값도 동일한지를 검사하여 두 객체가 동일한지를 판단
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;
리액트의 비교는 기본적으로 shallowEqual 함수를 통해 이뤄지는 것을 알아보았습니다.
그렇다면 리액트의 이러한 얕은 비교는 어느 상황에서 수행될까요??
React는 어떤 상황에서 shallow comparison을 사용하는가 ?
- 리렌더 트리거를 위한 props와 state의 변화를 감지하기 위해
- state: 일반적인 경우 업데이트 함수에 state 자체를 할당하지 않고 새로운 데이터를 할당해주어 불변성이 유지되어 re-render를 트리거시킵니다.
- props: 얕은 비교를 수행
- React.memo에서의 props 변화 감지: React.memo는 함수 컴포넌트의 props 변화를 얕은 비교로 확인하여 컴포넌트의 리렌더링 여부를 결정합니다
JAVASCRIPT
// 이전 props
props = { user : { id : 123 } }
// 새로운 props
props = { user : { id : 124 } }
// 위와같은 경우에는 얕은 비교를 통한 props 변경 감지가 불가능하겠죠 ?
따라서 성능 최적화를 위해 React.memo로 컴포넌트를 메모이제이션 해준 경우라도 중첩 객체 props라면 변경을 감지하지 못해 이전 렌더링의 결과를 재사용하게 됩니다.
( * React.memo: 컴포넌트를 고차함수 형태로 감싸 props가 변경된 경우에만 렌더링을 수행하도록 하는 성능 최적화 기법)
요약: 중첩된 형태의 props를 전달할 경우 얕은비교의 한계로, React.memo를 목적대로 사용하기 어렵다.
3. useMemo, useCallback에서의 의존성 배열 비교: 이러한 훅들은 의존성 배열 내부의 값들을 얕은 비교로 확인하여 메모이제이션된 값을 또는 함수를 새로 생성할지를 결정합니다. ( useEffect도 동일하게 얕은 비교를 수행 )
JAVASCRIPT
const NewComponent = () => {
const someFunction = () => {} // 모든 리렌더에서 초기화됩니다.
useEffect(()=>{
console.log('...some action does effect ? ')
},[someFunction]) // dep로 전달한 목적을 상실
return <></>;
}
- cf.) 위 코드의 경우 컴포넌트 내부에서 선언한 함수를 dep로 전달중인데, 모든 리렌더(컴포넌트 재호출) 상황에서 someFunction은 새로운 참조로 초기화되어 dep로 전달한 효용을 잃어버리게 됩니다.
Deep dive +
useEffect, useMemo , useCallback같은 훅의 의존성 배열(dep)에 객체형 데이터를 할당하는 것은 과연 효용이 있을까요?
이미 위에서 언급한 대로 컴포넌트 내부에서 선언한 객체의 경우 리렌더시 매번 새로운 참조로 초기화되어 dep에 할당하는 것이 의미가 없을 수 있습니다.
하지만 부모로부터 전달받게되는 props나 전역상태가 객체형 데이터인 경우 이러한 값은 dep에 전달시 기존의 참조를 유지할 수 있고 얕은 비교를 수행하므로 최적화할 수 있습니다.
@+ 단, Redux Toolkit를 통해 관리되는 전역상태의 경우 내부적으로 immer를 사용하므로 특정 프로퍼티가 변경될 때 그 상위 프로퍼티도 모두 새로운 객체로 초기화되는 structural sharing 방식의 불변성 유지 방식을 수행하므로 dep에 전달시 더 많은 고려가 필요합니다.
<selector함수가 가장 작은 단위를 반환해야하는 이유>
이에 대한 자세한 설명은 Redux와 렌더링 최적화 관련 챕터에서 다루도록 하겠습니다 :)
reference
데이터타입
자바스크립트에는 원시형과 객체형 이라는 데이터 타입의 두 가지 범주가 존재합니다.
원시 타입
변수 할당 시점에 메모리 영역을 차지하고 저장되기 때문에 불변 형태의 값으로 저장된다.
(boolean, number, string, null, undefined, Symbol, Bigint)
객체 타입
반면 객체는 언제나 프로퍼티를 삭제, 추가, 수정할 수 있기 때문에 원시형과 달리 변경 가능한 형태로 저장되며, 참조 주소값을 변수에 할당받는다. (배열, 함수 , 정규식, 클래스 등.. )
이러한 특성에 의해 객체타입의 경우 내부 프로퍼티가 변경 되더라도 기존의 참조주소를 유지하므로 서로 다른객체로 판단하지 않게됩니다.
그렇다면 자바스크립트는 자료형의 비교를 정확히 어떻게 수행할까요?
JS의 동등성 비교: === 일치 연산자 vs Object.is 메서드
데이터의 값과 자료형이 모두 같은지 파악하는 ‘===’ 일치 연산자는 원시 자료형의 경우 동등함의 판단을 보장합니다.
Object.is도 ===연산자와 쓰임세는 거의 비슷하지만
JAVASCRIPT
-0 === +0 // true
Number.NaN === NaN // false
Object.is(Number.NaN, NaN) // true
NaN === 0 / 0 // false
Object.is(NaN, 0 / 0) // true
와 같은 일부 상황에서 좀 더 까다로운 동등 비교를 수행해준다는 장점이 있죠.
하지만 === 연산자와 Object.is메서드 모두 객체 자료형의 동등성을 판단하는 것에는 한계가 명확한데요,
이것을 극복하기 방법으로는 무엇이 있을까요 ?
리액트에서의 동등비교
React의 데이터의 동등 비교는 내부적으로 구현된 shallowEqual 비교함수를 통해 수행하는데요,
shallowEqual의 기능을 요약하면 다음과 같습니다.
- Object.is 로 동등 비교 수행,
- 객체의 경우 첫 최상위 속성만을 비교하는 Shallow Comparison(얕은비교) 를 수행합니다.
이를 통해 원시형과 객체형 데이터를 효과적으로 비교할 수 있습니다.
React의 shallowEqual 함수 내부 구현 코드
DART
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
} // 두 자료를 Object.is 로 먼저 동등 비교한다 (원시형의 경우 여기서 먼저 걸러지겠죠 ?)
// 객체형의 경우 불변성을 유지시키지 않는다면 true가 반환되어 변경을 감지할 수 없습니다.
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// objA와 objB가 동일한 키를 가지고 있으며, 각 키에 해당하는 값도 동일한지를 검사하여 두 객체가 동일한지를 판단
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;
리액트의 비교는 기본적으로 shallowEqual 함수를 통해 이뤄지는 것을 알아보았습니다.
그렇다면 리액트의 이러한 얕은 비교는 어느 상황에서 수행될까요??
React는 어떤 상황에서 shallow comparison을 사용하는가 ?
- 리렌더 트리거를 위한 props와 state의 변화를 감지하기 위해
- state: 일반적인 경우 업데이트 함수에 state 자체를 할당하지 않고 새로운 데이터를 할당해주어 불변성이 유지되어 re-render를 트리거시킵니다.
- props: 얕은 비교를 수행
- React.memo에서의 props 변화 감지: React.memo는 함수 컴포넌트의 props 변화를 얕은 비교로 확인하여 컴포넌트의 리렌더링 여부를 결정합니다
JAVASCRIPT
// 이전 props
props = { user : { id : 123 } }
// 새로운 props
props = { user : { id : 124 } }
// 위와같은 경우에는 얕은 비교를 통한 props 변경 감지가 불가능하겠죠 ?
따라서 성능 최적화를 위해 React.memo로 컴포넌트를 메모이제이션 해준 경우라도 중첩 객체 props라면 변경을 감지하지 못해 이전 렌더링의 결과를 재사용하게 됩니다.
( * React.memo: 컴포넌트를 고차함수 형태로 감싸 props가 변경된 경우에만 렌더링을 수행하도록 하는 성능 최적화 기법)
요약: 중첩된 형태의 props를 전달할 경우 얕은비교의 한계로, React.memo를 목적대로 사용하기 어렵다.
3. useMemo, useCallback에서의 의존성 배열 비교: 이러한 훅들은 의존성 배열 내부의 값들을 얕은 비교로 확인하여 메모이제이션된 값을 또는 함수를 새로 생성할지를 결정합니다. ( useEffect도 동일하게 얕은 비교를 수행 )
JAVASCRIPT
const NewComponent = () => {
const someFunction = () => {} // 모든 리렌더에서 초기화됩니다.
useEffect(()=>{
console.log('...some action does effect ? ')
},[someFunction]) // dep로 전달한 목적을 상실
return <></>;
}
- cf.) 위 코드의 경우 컴포넌트 내부에서 선언한 함수를 dep로 전달중인데, 모든 리렌더(컴포넌트 재호출) 상황에서 someFunction은 새로운 참조로 초기화되어 dep로 전달한 효용을 잃어버리게 됩니다.
Deep dive +
useEffect, useMemo , useCallback같은 훅의 의존성 배열(dep)에 객체형 데이터를 할당하는 것은 과연 효용이 있을까요?
이미 위에서 언급한 대로 컴포넌트 내부에서 선언한 객체의 경우 리렌더시 매번 새로운 참조로 초기화되어 dep에 할당하는 것이 의미가 없을 수 있습니다.
하지만 부모로부터 전달받게되는 props나 전역상태가 객체형 데이터인 경우 이러한 값은 dep에 전달시 기존의 참조를 유지할 수 있고 얕은 비교를 수행하므로 최적화할 수 있습니다.
@+ 단, Redux Toolkit를 통해 관리되는 전역상태의 경우 내부적으로 immer를 사용하므로 특정 프로퍼티가 변경될 때 그 상위 프로퍼티도 모두 새로운 객체로 초기화되는 structural sharing 방식의 불변성 유지 방식을 수행하므로 dep에 전달시 더 많은 고려가 필요합니다.
<selector함수가 가장 작은 단위를 반환해야하는 이유>
이에 대한 자세한 설명은 Redux와 렌더링 최적화 관련 챕터에서 다루도록 하겠습니다 :)