[모던 리액트 Deep Dive] 1장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

하니·2025년 5월 29일

React 길잡이

목록 보기
18/21

자바스크립트 동등 비교

리액트의 가상 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

✐ 객체 타입

  • object

💡 null vs. undefined
undefined : 선언됐지만 할당되지 않은 값
null : 명시적으로 비어 있음을 나타내는 값
null은 의도적으로 비어있음을 나타내는 상태인 것만 확실히 알고 가면 된다.

✔️ Object.is : JS의 또 다른 비교 공식
== : 양쪽이 다른 타입이라면 강제로 형변환 후 비교
=== : 타입이 다르면 그냥 false
Object.is : 만족 못하는 몇 가지 특이 케이스 추가 > 좀 더 개발자가 기대하는 방식으로 정확히 비교

  • 객체 비교는 차이 X
  • 리액트에서 사용
-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
  • counstructor를 사용할 수 X
  • arguments 존재 X
  • this 바인딩 X ➡️ this를 사용해야 하는 클래스 컴포넌트 내부에서 화살표 함수 사용 주의
    this : 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값
    일반 함수 - 호출되는 런타임 시점에 결정되는 this (동적)
    화살표 함수 - this가 선언되는 시점에 이미 상위 스코프로 결정되어 있음

✚ 즉시 실행 함수

  • 함수 이름 생략
  1. 글로벌 스코프 오염 X
  2. 어디서든 다시금 호출되지 X는 점 각인 > 리팩터링 굿
(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

함수 생성 시 주의사항

  1. 함수의 부수 효과를 최대한 억제하라 (순수함수)
    함수의 실행과 결과를 최대한 예측 가능하도록 설계하기

  2. 가능한 한 함수를 작게 만들어라

  3. 누구나 이해할 수 있는 이름을 붙여라

  • [Terser](https://ry. terser.org/) : 한글로 네이밍
  • useEffect나 useCallback 등의 훅에 넘겨주는 콜백 함수에 네이밍 붙여주기
    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 변수가 존재하던 환경을 기억한다.

클로저의 활용


  • counter 변수의 문제점 : 전역 레벨에 선언되어 누구나 수정 가능 windw.counter
var 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

JS 코드의 실행이 하나의 스레드에서 순차적으로 이루어진다.
= 코드를 한 줄씩 실행
= 하나의 작업 끝나기 전까지는 뒤이은 작업 실행 X

역사
1. 프로세스 : 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업 단위 (1 프로그램 실행, 1 프로세스)
2. SW가 점차 복잡해지며 하나의 프로그램에서 동시에 여러 개의 작업 수행 필요
3. 스레드 : 더 작은 실행 단위.
하나의 프로세스에서 여러 개의 스레드
스레드끼리는 메모리 공유하며 여러 가지 작업 동시 수행

비동기

  • async (asynchronous) : 동시에 일어나지 않는 것
    동기식과 다르게 요청한 즉시 결과가 주어지지 X을 수도 있기 때문에 응답이 언제 올지 알 수 X다.
    하지만 여러 작업을 동시에 수행할 수 있다.

이벤트 루프

호출 스택 (call stack)

JS에서 수행해야 할 코드는 함수를 순차적으로 담아두는 스택

✔️ 동기 작업 실행 순서

function bar() {
	console.log('bar')
}

function baz() {
	console.log('baz')
}

function foo() {
	console.log('foo')
  	bar()
  	baz()
}

foo()

foo > bar > baz

  1. foo()가 호출 스택에 들어간다. [foo()]
  2. foo() 내부의 console.log가 호출 스택에 들어간다. [foo(), console.log]
  3. 2번이 실행 완료된 이후에 다음 코드로 넘어간다. [foo()]
  4. bar()가 호출 스택에 들어간다. [foo(), bar()]
  5. bar() 내부의 console.log가 호출 스택에 들어간다. [foo(), bar(), console.log]
  6. 5번이 실행 완료된 이후에 다음 코드로 넘어간다. [foo(), bar()]
  7. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다. [foo()]
  8. baz()가 호출 스택에 들어간다. [foo(), baz()]
  9. baz() 내부의 console.log가 호출 스택에 들어간다. [foo(), baz(), console.log]
  10. 9번이 실행 완료된 이후에 다음 코드로 넘어간다. [foo(), baz()]
  11. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. [foo()]
  12. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다. []
  13. 이제 호출 스택이 완전히 비워졌다.

이벤트 루프

(호출 스택이 비어 있는지 여부를 확인하는 것)
호출 스택에 실행 중인 코드가 있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할

호출 스택이 비었다면 태스크 큐에 대기 중인 작업 있는지 확인하고, 실행 가능한 오래된 것부터 순차적으로 꺼내와서 태스크 큐가 빌 때까지 실행하게 된다.

❓ 이때 태스크 큐에서 꺼낸 비동기 함수는 누가 처리할까 ?
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

  1. foo()가 호출 스택에 들어간다. [foo()]
  2. foo() 내부의 console.log가 호출 스택에 들어간다. [foo(), console.log]
  3. 2번이 실행 완료된 이후에 다음 코드로 넘어간다. [foo()]
  4. setTimeout(bar(), 0)이 호출 스택에 들어간다. [foo(), setTimeout]
  5. 4번의 타이머 이벤트가 실행되며 태스크 큐로 들어가고, 그 대신 바로 스택에서 제거된다. [foo()]
  6. baz()가 호출 스택에 들어간다. [foo(), baz()]
  7. baz() 내부의 console.log가 호출 스택에 들어간다. [foo(), baz(), console.log]
  8. 7번이 실행 완료된 이후에 다음 코드로 넘어간다. [foo(), baz()]
  9. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. [foo()]
  10. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다. []
  11. 이제 호출 스택이 완전히 비워졌다.
  12. 이벤트 루프가 호출 스택이 비워져 있다는 것을 확인했다. 그리고 태스크 큐를 확인하니 4번에 들어갔던 내용이 있어 bar()를 호출 스택에 들여보낸다. [bar()]
  13. bar() 내부의 console.log가 호출 스택에 들어간다. [bar(), console.log]
  14. 13번이 실행 완료된 이후에 다음 코드로 넘어간다. [bar()]
  15. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다. []

태스크 큐

실행해야 할 태스크의 집합
이벤트 루프는 태스크 큐를 1개 이상 가지고 있다.

  • 큐가 아닌 set 형태 : 선택된 큐 중에서 실행 가능한 가장 오래된 태스크를 가져오기 위함

태스크 큐와 마이크로 태스크 큐

이벤트 루프는 하나의 마이크로 태스크 큐를 갖는다.

  • 우선순위 : 마이크로 태스크 큐 > 태스크 큐

태스크 큐 : 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 예제 참고)

  • 동기 코드는 for문이 끝나야 렌더링 기회를 얻음 > 0에서 바로 100000으로 점프하여 100000가 찍힘
  • 태스크 큐 코드는 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 > 순차적으로 카운팅이 찍힘 숫자가 빠르게 올라가는 게 보임
  • 마이크로 태스크 큐 코드는 0에서 바로 100000으로 점프하여 100000가 찍힘

    동기: 렌더링 1회 (처음 0 → 마지막 100000)
    마이크로태스크: 렌더링 1회 (처음 0 → 마지막 100000)
    태스크: 렌더링 100000회 (각 setTimeout마다)

profile
Hi, I am HANI Developer(╹◡╹). .....1hani me?

0개의 댓글