React 에서는 동등 비교를 어떤 식으로 진행하는가?
Object.is
메서드로 두 값의 동등 비교를 시행하며, 참조형 타입의 경우 같은 메모리 주소를 참조한다면 같다고 판단한다.JS 의 데이터 타입은 크게 원시 타입과 객체 타입으로 나뉜다.
원시 타입은 객체 타입이 아닌 나머지 타입을 의미한다.
boolean, string, number, null, undefined, BigInt, Symbol 로 이루어져 있다.
typeof null === 'object'
undefined
는 아직 값이 할당되지 않은 상태이며, null
은 명시적으로 값이 비었음을 의미한다. (값은 값이다.)Number vs BigInt
-2^53 - 1 ~ 2^53 - 1
사이를 표현할 수 있다.Object.is
는 ES6 에서 새롭게 정의된 동등 비교 연산이며, 기존의 비교 연산자와의 차이점은 아래와 같다.
==
과 Object.is
의 차이
==
의 경우 양쪽이 동등한 타입이 아니라면 이를 캐스팅한다.Object.is
의 경우 타입이 다르면 false 를 반환한다.===
과 Object.is
의 차이
Object.is
는 +0
과 -0
을 다르다고 판단하지만 ===
는 그렇지 않다.Object.is
는 Number.NaN
(혹은 그 외 NaN 이 나올 만한 케이스) 과 NaN
을 같다고 판단하지만 ===
는 그렇지 않다. (NaN === NaN 은 무조건 false 다.)참조형 타입의 경우 두 객체가 같은 메모리 주소를 참조한다면 true
를 반환한다.
Object.is 조금 더 자세히 살펴보기
function is(a, b) {
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
x === y
가 성립한다면, x !== 0 || 1 / x === 1 / y
식도 성립해야 한다.x !== 0
인 경우x === 0
인 경우 1 / x === 1 / y
이어야 한다.+0
이라면 1 / x 는 Infinity
이고 -0
라면 -Infinity
이다. 따라서 x 와 y 의 부호가 다르면 false 를 반환한다.x !== x && y !== y
인 경우x !== x
를 성립하는 조건은 x 가 NaN 인 경우 외에 없다.x
와 y
가 둘 다 NaN 일 경우 true 를 반환하고, 그 외에는 false 를 반환한다.React 에서는 동등 비교가 필요한 경우 Object.is
에 더해 별도의 추가 작업을 더하여 정의한 shallowEqual 유틸 함수를 사용한다.
shallowEqual 의 경우 object 를 비교할 때 1 Depth 만 체크하므로 복잡한 구조의 Object 를 Props 로 넘길 경우 메모이제이션이 정상적으로 동작하지 않는다.
Source : https://github.com/facebook/react/blob/master/packages/shared/shallowEqual.js
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);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
Object.is
를 사용하여 두 값이 동등한지를 체크한다.function
키워드를 사용하여 함수를 정의한다.function
키워드를 사용하여 정의한 함수를 식별자에 할당한다.// 함수 선언문
function add(a, b) {
return a + b;
}
// 함수 표현식
const add = function (a, b) {
return a + b;
};
undefined
를, const 와 let 은 ReferenceError
를 유발시킨다.new Function()
생성자를 기반으로 함수를 생성하는 방식=>
키워드를 사용한 익명 함수를 생성하고 이를 변수에 할당하는 방식.// Function 생성자
const add = new Function("a", "b", "return a + b");
const subFuncBody = "return a - b";
const sub = new Function("a", "b", subFuncBody); // 런타임 환경에서 Body 를 할당받아 실행이 가능하다.
// Arrow Function
const add = (a, b) => a + b;
arguments
가 없으며 생성자 기반으로 제작이 불가능하다.// IIFE
async (() => {
const slackClient = await slackApp.bootstrap();
slackClient.init();
})
// HOC
const Component = () => (<div> {...} </div>)
const intlComponent = withIntl(Component);
#
을 사용하여 특정 속성을 private 하게 지정할 수 있다.get
을, setter 함수의 경우 set
을 붙인다.class Car {
constructor(name) {
this.name = name;
}
getName() {
return this.name
}
}
const myCar = new Car('레이');
console.log(Object.getPrototypeOf(myCar)) // { constructor: f, getName: ƒ }
Prototype Chaining
toString()
메서드의 경우에도 별도의 정의 없이 어느 객체에서나 사용할 수 있다.'use strict';
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ('value' in descriptor) descriptor.writable = true;
Object.defineProperty(
target,
_toPropertyKey(descriptor.key),
descriptor,
);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, 'prototype', { writable: false });
return Constructor;
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, 'string');
return _typeof(key) === 'symbol' ? key : String(key);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== 'object' || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || 'default');
if (_typeof(res) !== 'object') return res;
throw new TypeError('@@toPrimitive must return a primitive value.');
}
return (hint === 'string' ? String : Number)(input);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function');
}
}
var Cat = /*#__PURE__*/ _createClass(function Cat(name) {
_classCallCheck(this, Cat);
this.name = name;
});
트랜스파일링 된 코드들이 각각 어떤 역할을 하는지 알아보자.
_createClass
함수function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, 'prototype', { writable: false });
return Constructor;
}
_createClass
함수는 첫 번째 인자로 Constructor (생성자) 함수를 받는다. new
키워드로 생성자를 호출하여 prototype 에 적재된 메서드들이 포함된 객체를 반환한다._classCallCheck
함수var Cat = /*#__PURE__*/ _createClass(function Cat(name) {
_classCallCheck(this, Cat);
this.name = name;
});
function _classCallCheck(instance, Constructor) {
// 만약 Cat 함수가 new Cat() 이 아닌 Cat() 으로 호출되었다면 에러 발생.
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function');
}
}
new
키워드를 기반으로 한 생성자 호출로 나뉘는데, 여기서는 생성자를 호출해야 하므로 이를 검사하기 위해 추가된 함수다.instance instanceof Constructor
조건문을 통과할 수 없다._defineProperties
함수function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ('value' in descriptor) descriptor.writable = true;
Object.defineProperty(
target,
_toPropertyKey(descriptor.key),
descriptor,
);
}
}
Object.defineProperties
함수를 래핑한 함수다.value
가 존재하는 property 인 경우 수정이 가능하도록 writable flag 를 true 로 설정한다.function makeFunc() {
// displayName 함수 내부에서 스코프 체이닝으로 인해 해당 변수를 참조하므로, makeFunc 함수가 종료되어 실행 컨텍스트에서 사라져도 변수는 사라지지 않는다.
const name = "Mozilla";
function displayName() {
// name 은 displayName 의 외부 Lexical Environment 에 위치한 name 변수를 참조한다.
console.log(name);
}
return displayName;
}
const myFunc = makeFunc();
myFunc();
실행 컨텍스트 관점에서 해당 클로저가 어떻게 동작하는지를 살펴보자.
makeFunc
함수의 실행 결과가 할당된다. Call Stack 에 myFunc
함수와 관련한 실행 컨텍스트가 적재된다.makeFunc
함수는 내부에서 displayName
함수를 선언한다. Call Stack 에 displayName
함수와 관련한 실행 컨텍스트가 적재된다.displayName
함수가 선언된 환경 (makeFunc 함수) 을 보면 name 변수가 있다.displayName
실행 컨텍스트 내부의 outerEnvironmentReference 포인터에 의해 외부 Lexical Environment 를 참조한다.makeFunc
함수) 내 Environment Record 내부에는 name 식별자가 있고, displayName 함수 내부에서는 이를 사용한다.makeFunc
함수가 종료되고 Call Stack 에 쌓였던 실행 컨텍스트 또한 제거 된다.클로저에 의해 사라지지 않는 변수를 뭐라고 할까?
var global = 'global'
function hello () {
// hello 내부에서 사용되는 global 의 경우에도 Scope Chaining 을 기반으로 전역 컨텍스트에 정의된 global 변수를 참조한다.
console.log(global) // global
}
console.log(global) // global
function a() {
const x = '100';
var y = '200';
console.log(x) // 100
function b() {
var x = '1000';
console.log(x) // 1000
console.log(y) // 200
}
}
블록 스코프
{}
) 를 기준으로 스코프를 할당 받는다. 이를 블록 스코프라고 한다.var a = 100;
{
const a = 1000;
console.log(a); // 1000
}
console.log(a); // 100
// Closure 를 사용하지 않은 경우
const aButton = document.getElementById('a');
function heavyJob() {
const longArr = Array.from({length: 100_000_100}, (_, i) => i);
console.log(longArr);
}
aButton.addEventListener('click', heavyJob)
// Closure 를 사용하는 경우
const bButton = document.getElementById('b');
function heavyJobWithClosure() {
// longArr 배열은 heavyJobWithClosure 함수가 호출되어 내부 함수가 반환될 때 참조되며, 메모리에서 사라지지 않는다.
const longArr = Array.from({length: 100_000_100}, (_, i) => i);
return function () {
// 여기서 longArr 를 사용하고 있기 때문에 longArr 변수는 메모리에서 사라지지 않는다.
console.log(longArr);
}
}
const innerFunc = heavyJobWithClosure();
bButton.addEventListener('click', function () {
innerFunc();
})
heavyJobWithClosure
함수가 실행되어 innerFunc
에 반환된 함수가 적재될 때 생성된다.비동기로 동작한다 VS Non Blocking 하다.
책에서는 브라우저 (Web API) 기반의 이벤트 루프를 위주로 설명했기 때문에, 정리글 또한 whatwg Spec 을 기반으로 작성한다.
이름이 Task Queue 라고 해서 자료 구조가 Queue 인 것은 아니다.
Task queues are sets, not queues, because the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.
libuv
를 기반으로 이벤트 루프를 구현한다.PromiseJobs
이라는 내부 Queue 를 명시하는데, V8 엔진에서는 이를 마이크로 태스크 큐라고 정의한다.MutationObserver
가 변화를 관측할 시 실행하는 Callback 함수 또한 Microtask Queue 로 들어간다.console.log('a');
setTimeout(() => {
console.log('b')
}, 0);
Promise.resolve().then(() => {
console.log('c');
})
window.requestAnimationFrame(() => {
console.log('d')
})
1. console.log('a') 가 Call Stack 에 적재되고, 실행된다.
2. setTimeout 작업이 Task Queue 에 할당된다.
3. Promise.resolve() 작업이 수행되고, then 핸들러 내부의 callback 이 Micro Task Queue 에 할당된다.
4. requestAnimationFrame 가 인자로 받은 callback 을 화면이 렌더링 되는 과정에서 실행하도록 예약 (스케줄링) 한다.
5. Call Stack 이 비었으므로 Micro Task Queue 내부의 작업을 가져와 `console.log('c')` 를 실행한다.
6. Call Stack 과 Micro Task Queue 이 전부 비었으므로 브라우저 렌더링이 진행된다. 이때 rAF 가 인자로 받은 `console.log('d')` 이 실행된다.
7. Task Queue 에 있던 작업이 Call Stack 으로 적재되고, `console.log('b')` 가 실행된다.
cancelAnimationFrame(id)
메서드로 취소가 가능하다.추후 시간이 남으면 살펴봐야 할 문서들
// 배열 구조 분해 할당
const [isOpen, setIsOpen] = useState(false);
// 객체 구조 분해 할당
const Component = ({ totalCount }: PropsType) => {
return (...)
}
...
) 를 사용하여 특정 객체를 새로운 객체 내부에 선언할 수 있고, 배열 또한 같은 원리로 사용이 가능하다.const obj = { a: 1, b: 2 };
const newObj = { ...obj, c: 3, d: 4 };
객체 구조 분해 할당의 트랜스파일링 구조를 파헤쳐보자.
"use strict";
// source 객체에서 excluded 배열 내 key 를 제외한 나머지를 반환하는 함수
// _objectWithoutPropertiesLoose 와는 다르게 Symbol Key 가 있는 경우도 고려한다.
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
// Symbol 키가 있다면 getOwnPropertySymbols 로 목록을 받아 순회한다.
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
// 객체 내부의 key 를 순회하며 excluded 에 포함되지 않는 나머지를 target 에 추가한다.
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue; // 해당 key 가 excluded 배열에 있다면 continue.
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
// source 객체에서 excluded 배열 내 key 를 제외한 나머지를 반환하는 함수
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source); // 여기서는 Symbol Key 를 반환하지 않는다.
var key, i;
// 객체 내부의 key 를 순회하며 excluded 에 포함되지 않는 나머지를 target 에 추가한다.
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue; // 해당 key 가 excluded 배열에 있다면 continue.
target[key] = source[key];
}
return target;
}
var obj = {
a: 1,
b: 2,
c: 3,
d: 4,
};
var a = obj.a,
b = obj.b,
rest = _objectWithoutProperties(obj, ["a", "b"]);
_objectWithoutPropertiesLoose
함수_objectWithoutProperties
함수_objectWithoutPropertiesLoose
와 같으나 Object.keys()
메서드가 Symbol 형태의 key 를 찾지 못하는 예외를 처리하는 로직이 추가되었다._objectWithoutPropertiesLoose
함수의 결과 (target) 를 인계 받는다.Object.getOwnPropertySymbols
메서드로 받는다.Object.is
기반의 동등성 검사에서 값이 달라짐을 유도한다.책에서는 Typescript 를 다뤄야 하는 이유에 대해서 설명했기에 간결하게 내용만 정리하고자 한다.