리액트의 가상 DOM과 실제 DOM의 비교, 리액트 컴포넌트가 렌더링할지를 판단하는 방법, 변수나 함수의 메모제이션 등 모든 작업은 JS의 동등 비교를 기반으로 한다.
✐ 원시 타입
boolean : true1, false0
null : 아직 값이 없거나 비어 있는 값을 표현할 때 사용
undefined : 선언 후 값을 할당하지 X은 변수 또는 주어지지 않은 인수에 자동으로 할당되는 값
number
bigint : 끝에 n을 붙이거나, BigInt('숫자') 함수 사용
string : 변경 불가능
symbol : 중복되지 않는 어떠한 고유한 값을 나타냄. 반드시 Symbol() 함수를 사용해야 함.
// Symbol 함수에 같은 인수를 넘겨주도라도 동일한 값으로 인정 X
const key = Symbol('key');
const key2 = Symbol('key');
key === key2 // false
// 동일한 값을 사용하기 위해서는 Symbol.for 활용
Symbol.for('hello') === Symbol.for('hello') // true
✐ 객체 타입
💡 null vs. undefined
undefined : 선언됐지만 할당되지 않은 값
null : 명시적으로 비어 있음을 나타내는 값
null은 의도적으로 비어있음을 나타내는 상태인 것만 확실히 알고 가면 된다.
✔️ Object.is : JS의 또 다른 비교 공식
== : 양쪽이 다른 타입이라면 강제로 형변환 후 비교
=== : 타입이 다르면 그냥 false
Object.is : 만족 못하는 몇 가지 특이 케이스 추가 > 좀 더 개발자가 기대하는 방식으로 정확히 비교
-0 === +0 // true
Object.is(-0, +0) // false
Number.NaN === NaN // false
Object.is(Number.NaN, NaN) // true
NaN === 0 / 0 // false
Object.is(NaN, 0 / 0) // true
Object.is({}, {}) // false
const a = {
hello: 'hi',
}
const b = a;
Object.is(a, b) // true
a === b // true
Object.is로 먼저 비교 수행 > 객체 간 얕은 비교 수행
// Object.is는 참조가 다른 객체에 대해 비교 불가능
Object.is({hello: 'world'}, {hello: 'world'}) // false
// 리액트 팀이 구현한 shallowEqual은 객체의 1 depth까지 비교 가능
shallowEqual({hello: 'world'}, {hello: 'world'}) // true
// 2 depth X
shallowEqual({hello: {hi: 'world'}}, {hello: {hi: 'world'}}) // false
➡️ props가 깊어지는 경우한 객체 안에 또다른 객체가 있을 경우, React.memo는 컴포넌트에 실제로 변경되 값이 없음에도 불구하고 메모이제이션된 컴포넌트를 반환하지 못한다. (매번 새로운 객체가 생성되어 참조가 달라지므로)
💬 이 특징을 잘 숙지하여 향후 함수 컴포넌트에서 사용되는
훅의 의존성 배열의 비교, useMemo와 useCallback의 필요성, React.memo를 올바르게 작동시키자
함수 선언문과 함수 표현식의 차이점 : 호이스팅 여부
✐ 함수 선언문
add(10, 20) // ✅ 30
function add(a, b) {
return a + b
}
✐ 함수 표현식
console.log(typeof sum === 'undefined') // true
sum(10, 20) // ❌ Uncaught TypeError: sum is not a function
const sum = function (a, b) {
return a + b
}
✐ Function 생성자
const add = new Function('a', 'b', 'return a + b')
✐ 화살표 함수
const add = (a, b) => {
return a + b
}
const add = (a, b) => a + b
this : 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값✚ 즉시 실행 함수
(function (a, b) {
return a + b
})(10, 24); // 34
((a, b) => {
return a + b
},
)(10, 24) // 34
✚ 고차 함수
함수를 인수로 받거나, 결과로 새로운 함수를 반환하는 역할을 하는 함수
// 함수를 매개변수로 받는 대표적인 고차 함수, Array.prototype.map
// map은 함수를 인자로 받아 처리하는 함수
const doubledArray = [1, 2, 3].map((item) => item * 2)
console.log(doubledArray) // [2, 4, 6]
// 함수를 반환하는 고차 함수의 예
const add = function (a) {
// a가 존재하는 클로저를 생성
return function (b) {
// b를 인수로 받아 두 합을 반환하는 또 다른 함수를 생성
return a + b
}
}
const addOne = add(1); // addOne은 함수!
console.log(typeof addOne); // "function"
const result = addOne(3); // 4
// 또는
add(1)(3) // 4
함수의 부수 효과를 최대한 억제하라 (순수함수)
함수의 실행과 결과를 최대한 예측 가능하도록 설계하기
가능한 한 함수를 작게 만들어라
누구나 이해할 수 있는 이름을 붙여라
useEffect(() => {
// ...
}, [])useEffect(function apiRequest() {
// ...
}, [])추가 예정
함수 컴포넌트의 구조와 작동 방식, 훅의 원리, 의존성 배열 등 함수 컴포넌트의 대부분의 기술이 모두 클로저에 의존하고 있다.
: 함수와 함수가 선언된 어휘적 환경변수가 코드 내부에서 어디서 선언됐는지의 조합
function outerFunction() {
var x = 'hello'
function innerFunction() {
console.log(x)
}
return innerFunction
}
const innerFunction = outerFunction();
innerFunction() // "hello"
반환된 innerFunction 함수에는 x 변수가 존재하지 않는다.
outerFunctioninnerFunction이 선언된 어휘적 환경에는 x 변수가 존재하고 접근할 수 있다.
➡️ 같은 환경에서 선언되고 반환된 innerFunction에서는 x 변수가 존재하던 환경을 기억한다.
windw.countervar counter = 0
function handleClick() {
counter++;
}
function Counter() {
var counter = 0
return {
increase: function () {
return ++counter
},
decrease: function () {
return --counter
}
counter: function () {
console.log('counter에 접근!')
return counter
},
}
}
var c = Counter()
console.log(c.increase()) // 1
console.log(c.increase()) // 2
console.log(c.increase()) // 3
console.log(c.decrease()) // 2
console.log(c.counter()) // 2
useState의 변수를 저장해 두고, useState의 변수 접근 및 수정 또한 클로저 내부 React 내부 영역에서 확인이 가능해 값이 변하면 렌더링 함수를 호출하는 등의 작업이 이루어진다.
function Component() {
const [state, setState] = useState()
function handleClick() {
// useState 호출은 위에서 끝났지만,
// setState는 계속 내부의 최신값(prev)을 알고 있다. > 클로저를 활용했기 때문에
setState((prev) => prev + 1)
}
// ...
}
외부 함수useState가 반환된 내부 함수setState는 외부 함수useState의 호출이 끝났음에도 자신이 선언된 외부 함수가 선언된 환경state가 저장돼 있는 어딘가을 기억하기 때문에 계속해서 state 값을 사용할 수 있는 것이다.
for (var i=0; i<5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
의도 : 0부터 시작해 1초 간격으로 0, 1, 2, 3, 4 차례대로 출력
결과 : 5만 출력
❗️ 문제점 : i가 전역 변수로 작동
JS는 기본적으로 함수 레벨 스코프를 따름 > var는 for문의 존재와 상관 없이 해당 구문이 선언된 함수 레벨 스코프를 바라보고 있음 > for문을 다 순회 후, 태스크 큐에 있는 setTimeout을 실행할 때, 이미 전역 레벨에 있는 i는 5로 업데이트 완료
🌟 실행 순서
1. for문 실행동기 코드
setTimeout들을 태스크 큐에 등록 (실행은 X)
2. for문 완료
이때 i = 5가 됨
3. 콜 스택이 비워짐
4. 태스크 큐의 setTimeout의 콜백함수들이 차례로 실행비동기 코드
5. 모든 setTimeout이 같은 i5를 참조추가 설명
이벤트 루프: JS는 단일 스레드 > 동기 코드를 먼저 다 실행, 비동기 코드는 나중에 실행
콜 스택: 함수 호출을 관리하는 메모리 공간 (LIFO 구조)
태스크 큐: 비동기 작업들이 완료되었을 때 그 콜백 함수들이 대기하는 공간
🙆🏻♀️ 1. 블록 레벨 스코프를 갖는 let으로 수정하기
let i가 for문을 순회하면서 각각의 스코프를 갖게 됨 > setTimeout이 실행되는 시점에도 유효해서 각 콜백이 의도한 i 값을 바라봄
🙆🏻♀️ 2. 클로저 제대로 활용하기
for (var i=0; i<5; i++) {
setTimeout(
(function (sec) { // ← 즉시 실행 익명 함수
return function () { // ← setTimeout의 콜백함수
console.log(sec) // ← 여기서 sec를 참조
}
})(i),
i * 1000
)
}
for문 내부에 즉시 실행 익명 함수 선언 > 인수로 받은 i를 함수 내부에서는 sec 인수에 저장해둠 > setTimeout의 콜백 함수에 넘김
setTimeout의 콜백함수가 바라보는 클로저 : 즉시 실행 익명 함수
클로저 덕분에 즉시 실행 함수가 끝나도 sec 값이 살아있기 때문에, 각 콜백함수마다 서로 다른 sec 값을 기억한다.
➡️ '함수와 함수가 선언된 어휘적 환경의 조합'을 주의 깊게 살펴봐야 클로저를 제대로 활용할 수 있다..!
💡 클로저 사용은 비용이 든다
클로저는 생성될 때마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생한다.
➡️ 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 메커니즘은 성능에 영향을 미친다.
🔒 JavaScript에서 클로저는 오직 함수에서만 만들어진다.
함수가 선언될 때의 렉시컬 환경(변수들)을 기억하는 게 클로저의 핵심이다.
- 클래스나 객체에 선언한 메서드는 클로저가 아니라 그냥 메서드
JS는 싱글 스레드에서 작동한다.
JS는 한 번에 하나의 작업만 동기 방식으로만 처리할 수 있다.
JS 코드의 실행이 하나의 스레드에서 순차적으로 이루어진다.
= 코드를 한 줄씩 실행
= 하나의 작업 끝나기 전까지는 뒤이은 작업 실행 X
역사
1. 프로세스 : 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업 단위 (1 프로그램 실행, 1 프로세스)
2. SW가 점차 복잡해지며 하나의 프로그램에서 동시에 여러 개의 작업 수행 필요
3. 스레드 : 더 작은 실행 단위.
하나의 프로세스에서 여러 개의 스레드
스레드끼리는 메모리 공유하며 여러 가지 작업 동시 수행
비동기
async (asynchronous) : 동시에 일어나지 않는 것JS에서 수행해야 할 코드는 함수를 순차적으로 담아두는 스택
✔️ 동기 작업 실행 순서
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
function foo() {
console.log('foo')
bar()
baz()
}
foo()
foo > bar > baz
foo()가 호출 스택에 들어간다. [foo()]console.log가 호출 스택에 들어간다. [foo(), console.log][foo()]bar()가 호출 스택에 들어간다. [foo(), bar()]console.log가 호출 스택에 들어간다. [foo(), bar(), console.log][foo(), bar()][foo()]baz()가 호출 스택에 들어간다. [foo(), baz()]console.log가 호출 스택에 들어간다. [foo(), baz(), console.log][foo(), baz()][foo()][](호출 스택이 비어 있는지 여부를 확인하는 것)
호출 스택에 실행 중인 코드가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할
호출 스택이 비었다면 태스크 큐에 대기 중인 작업 있는지 확인하고, 실행 가능한 오래된 것부터 순차적으로 꺼내와서 태스크 큐가 빌 때까지 실행하게 된다.
❓ 이때 태스크 큐에서 꺼낸 비동기 함수는 누가 처리할까 ?
JS 코드가 동기식으로 실행되는 메인 스레드가 아닌, 태스크 큐가 할당되는 별도의 스레드에서 수행된다.브라우저나 Node.js의 역할
JS 코드 실행은 싱글 스레드에서 이루어지지만,
외부 Web APIsetTimeout, fetch 등은 모두 JS 코드 외부에서 실행되고 콜백이 태스크 큐로 들어가는 것이다.
✔️ 비동기 작업 실행 순서
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
function foo() {
console.log('foo')
// bar()
setTimeout(bar(), 0) // ✅
baz()
}
foo()
foo > baz > bar
foo()가 호출 스택에 들어간다. [foo()]console.log가 호출 스택에 들어간다. [foo(), console.log][foo()]setTimeout(bar(), 0)이 호출 스택에 들어간다. [foo(), setTimeout][foo()]baz()가 호출 스택에 들어간다. [foo(), baz()]console.log가 호출 스택에 들어간다. [foo(), baz(), console.log][foo(), baz()][foo()][]bar()를 호출 스택에 들여보낸다. [bar()]console.log가 호출 스택에 들어간다. [bar(), console.log][bar()][]실행해야 할 태스크의 집합
이벤트 루프는 태스크 큐를 1개 이상 가지고 있다.
이벤트 루프는 하나의 마이크로 태스크 큐를 갖는다.
태스크 큐 : setTimeout, setInterval, setImmediate
마이크로 태스크 큐 : Promises, process.nextTick, queueMicroTask, Mutationobserver
✔️ 마이크로 태스크 큐가 빌 때까지 기존 태스크 큐의 실행은 뒤로 미루어진다.
function foo() {
console.log('foo')
}
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
setTimeout(foo, 0)
Promise.resolve().then(bar).then(baz)
bar > baz > foo : setTimeout은 Promise보다 늦게 실행된다.
✔️ 렌더링은 태스크 사이에서만 발생하고, 마이크로태스크나 동기 코드 실행 중에는 차단된다! (오타 정정)
(79p 예제 참고)
숫자가 빠르게 올라가는 게 보임동기: 렌더링 1회 (처음 0 → 마지막 100000)
마이크로태스크: 렌더링 1회 (처음 0 → 마지막 100000)
태스크: 렌더링 100000회 (각 setTimeout마다)