본문 바로가기
React-study/dil

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

by 어느새벽 2024. 11. 5.

 

리액트와 자바스크립트

  • 리액트는 사용자 인터페이스를 만들기 위한 자바스크립트 라이브러리로, 웹 애플리케이션을 더 쉽고 빠르게 개발할 수 있도록 도와준다. 리액트는 UI를 컴포넌트라는 독립적인 단위로 구성하여 코드의 재사용성과 관리 편의성을 높인다.
  • 자바스크립트는 웹 브라우저에서 실행되는 프로그래밍 언어이다. 우리가 웹 페이지에서 상호작용하는 기능들을 구현할 수 있게 해준다. 예를 들어, 버튼을 클릭했을 때 뭔가가 일어나게 하는 것이 자바스크립트를 통해 가능한 것이다.

1.1  자바스크립트의 동등 비교와 데이터 타입

  • 자바스크립트에서 동등 비교는 리액트의 가상 DOM과 실제 DOM 비교, 렌더링판단, 메모이제이션등 여러 작업의 기초이므로 중요하다.
  • 모든 값은 데이터 타입을 가지며, 원시 타입객체 타입으로 나뉘고, null 타입은 객체 타입으로 간주된다는 점이 특징이다 . 
  • 객체 타입은 참조를 통해 데이터에 접근하며, 메모리 할당 방식이 원시 타입과 다르기 때문에 객체를 비교할 때 주의가 필요하다 .
let hello = 'hello world'
let hi = hello

console.log(hello === hi) //true

let hello = 'hello world'
let hi = 'hello world'

console.log(hello === hi) // true

 

이렇게 원시 타입은 불변 형태의 값으로 저장되고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장된다.

또 위에 값들을 비교하면 모두 true가 나오는데, 값을 전달하는 방식이 아닌 각각 선언하는 방식으로도 동일한 결과를 볼 수 있기 때문이다.

var hello = {
	greet: 'hello, world',
}

var hi = {
	greet: 'hello, world',
}

console.log(hello === hi) // false
console.log(hello.greet === hi.greet) // true

 

반면 객체는 원시 값과 다르게 변경 가능한 형태로 저장되며, 값을 복사할 때도 값이 아닌 참조를 전달한다. 저장하는 순간 다른 참조를 바라보기 때문이다. 따라서 자바스크립를 사용할 때 객체 간에 비교가 발생하면 우리가 이해하는 내부의 값이 같다 해도 결과가 true가 아닐 수 있음을 항상 인지 해야 한다.

  • Object.is 함수는 동등 연산자와 비슷하지만 형변환을 하지 않아 타입이 다르면 false를 반환하고, === 연산자 보다 더 정확하다. 하지만 이 함수도 객체 비교에서는 위의 원리와 같다.
-0 === +0 //true
Object.is(-0, +0) //false

Object.is({}, {}) //false

const a = {
	hello: 'hi',
}
const b = a

Object.is(a, b) //true
a === b //true
  • 리액트에서는 얕은 비교를 통해 프롭스를 평가하는 데 사용된다.
  • 리액트에서 동등 비교를 위한 `shallowEqual` 함수는 성능 최적화를 위해 깊은 비교 대신 얕은 비교를 수행하는 것으로 추정된다 . 
//Object.is는 참조가 다른 객체에 대해 비교가 불가능하다.
Object.is({hello: 'world', {hello: 'world'}) // false

//반면 리액트에서 구현한 shallowEqual은 객체의 1 depth까지는 비교가 가능하다.
shallowEqual({hello: 'world'}, {hello: 'world'}) // true

//그러나 2 depth까지 가면 이를 비교할 방법이 없어 false를 반환한다.
shallowEqual({hello: {hi: 'workl'}}, {hello" {hi: 'world'}}) // false

1.2 함수

  • 함수는 작업을 수행하거나 값을 계산하는 과정으로 정의되며, 일반 함수와 화살표 함수의 차이점을 이해하는 것이 중요하다 .
  • 함수를 정의하는 방법으로는 함수선언문, 함수 표현식, Function 생성자 등이 있으며, 각 방법은 특정한 특징을 가지고 있다 . 
  • 함수는 일급 객체로, 다른 함수의 매개변수로 사용되거나 반환값으로 직접 들어가며, 변수에 할당될 수 있는 특성을 가진다 .
  • 함수표현식과 함수선언문의 중요한 차이는 호이스팅이며, 이는 실행 순서에 영향을 미치는 개념이다 .
//함수 선언식과 표현식의 차이

//함수 선언식

hello() // hello

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

hello() // hello


//함수 표현식

consol.log(typeof hello === 'undefined') // true

hello() // hello is not a function

var hello = function () {
	console.log('hello')
}

hello()

 

함수 선언식은 함수의 호이스팅 특징 덕분에 미리 메모리에 등록됐고, 코드의 순서에 상관없이 정상적으로 함수를 호출 할 수 있는 반면,

함수 표현식은 함수를 변수에 할당되기 때문에 변수의 호이스팅은 런타임 이전에 undefined로 초기화되고, 할당문이 실행되는 시점, 즉 런타임 시점에 함수가 할당되어 작동한다.

  • 함수작성 시에는 부수 효과를 억제해야 하며, 이를 최소화하기 위한 주의가 필요하다. 또 가능한 한 함수를 작게 만드는 것이 좋고 누구나 이해할 수 있는 이름을 붙여야 한다.
//useEffect에도 이름 붙여주기

useEffect(function apiRequest() {
//...do something
}, [])

 


1.3 클래스

  • 클래스는 특정 객체를 만들기 위한 템플릿으로, 프로토타입기반으로 동작한다.
  • constructor 는 생성자로 객체를 생성하는 데 사용하는 특수한 메서드이다. 단 하나만 존재할 수 있고, 생략하는 것도 가능하다.
class Car {
	constructor (name) {
		this.name = name
	}

	constructor (name) {
		this.name = name
	}
}
// 하나만 사용할 수 있어서 위에는 틀린 사용법이다.

class Car {
	//constructor 생략 가능
}
  • 프로퍼티는 클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값을 의미한다.
class Car {
	constructor(name) {
		// 값을 받으면 내부에 프로퍼티로 할당한다.
		this.name= name
	}
}

const myCar = new Car('자동차') // 프로퍼티 값을 넘겨줬다.

 

  • getter는 앞에 get을 붙여 사용하고 클래스에서 무언가 값을 가져올 때 사용된다.  setter는 앞에 set을 붙여 사용하고 클래스 필드에 값을 할당할 때 사용한다.
  • 인스턴스 메서드는 클래스 내부에서 선언한 메서드를 말하며, 자바스크립트의 prototype에 선언된다.
class Car {
	constructor(name) {
	this.name = name
	}
    
    //인스턴스 메서드 정의
    hello () {
		console.log(`안녕하세요, ${this,name}입니다.`)
	}
} 

const myCar = new Car('자동차')
myCar.hello() //안녕하세요, 자동차입니다.

Object.getPrototype(myCar) === Car.prototype // true

 

프로토타입과 프로토타입 체이닝이라는 특성으로 인해 생성한 객체에서도 직접 선언하지 않은, 클래스에 선언한 hello() 메서드를 호출할 수 있고, 이 메서드 내부에서 this도 접근해 사용할 수 있게 된다.

  • 정적메서드는 이름으로 호출할 수 있는 메서드로 정적 메서드 내부의 this는 클래스로 생성된 인스턴스가 아닌, 클래스 자신을 가리키기 때문에 다른 메서드에서 일반적으로 사용하는 this를 사용할 수 없다.
  • 상속 extends는 기존 클래스를 상속 받고, 상속 받은 클래스를 기반으로 자식 클래스에서 확장하는 개념이다.
class Car {
	constructor(name) {
		this.name = name
	}
    
    honk() {
		console.log(`${this.name} 경적을 울립니다!`)
	}
}

class Truck extends Car {
	constuctor(name) {
    	//부모 클래스의 constructor, 즉 Car의 constructor를 호출한다.
		super(name)
	}
    
    load() {
    	console.log('짐을 싣습니다.')
    }
}

const myCar = new Car('자동차')
myCar.honk() // 자동차 경적을 울립니다!

const truck = new Truck('트럭')
truck.honk() // 트럭 경적을 울립니다!
truch.load() // 짐을 싣습니다.

1.4 클로저의 정의와 활용 및 주의점

  • MDN에서의 클로저 정의를 보면 "함수와 함수가 선언된 어휘적 환경의 조합"이라고 설명한다.
function add() {
	const a = 10
	function innerAdd() {
		const b = 20
        console.log(a + b)
	}
    innerAdd() // 30
}
add()

 

 

이 예시를 보면 함수가 이처럼 중첩돼 있는 상황에서 변수의 범위가 어떻게 정의되는지 알 수 있다. a 변수의 유효 범위는 add 전체이고, b의 유효 범위는 innerAdd의 전체다. innerAdd는 add 내부에서 선언돼 있어 a를 사용할 수 있게 된 것이다. 즉, "선언된 어휘적 환경"이란 변수가 코드 내부의 어디서 선언됐는지를 말하는 것이다. 이는 호출되는 방식에 따라 동적으로 결정되는 this와 달리 코드가 작성된 순간에 정적으로 결정된다는 뜻이다. 또 이러한 유효 범위를 스코프(scope)라고 한다.

  • 전역스코프(global scope)는 전역 레벨에서 선언하는 것을 말한다. 이 스코프에서 변수를 선언하면 어디서든 호출할 수 있다. 브라우저 환경에서의 전역 객체는 window, Node.js 환경에서는 global이라고 한다. 
  • 자바스크립트의 함수 스코프는 함수 레벨 스코프를 따른다. ({} 블록이 스코프 범위를 결정하는 것이 아니다!)
if(true) {
	var global = 'global scope'
}

console.log(global) // 'global scope'
console.log(global === window.global) // true

--------

function hello() {
	var local = 'local variable'
    console.log(local) // local variable
}

hello()
console.log(local) // local is not defined

 

위 예제를 보면 단순 if문에서는 {} 블록 밖에서도 접근이 가능하지만, 함수 블록 내부에서는 스코프가 결정되는 것을 알 수 있다.

  • 전역 스코프는 어디서든 원하는 값을 꺼내올 수 있다는 장점이 있지만 반대로 누구든 접근할 수 있고 수정할 수 있어 주의가 필요하다.
  • 리액트에서의 클로저의 원리를 대표적으로 사용하고 있는 것이 바로 useState 훅으로, 내부 함수가 외부 환경의 변수를 기억함으로써 상태를 지속적으로 업데이트할 수 있다 .
  • 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 매커니즘은 당연히 메모리 비용을 많이 쓰기에 사용할 때 주의가 필요하다.

1.5 이벤트 루프와 비동기 통신의 이해

  • 자바스크립트는 싱글 스레드에서 작동하는 즉, 한 번에 하나의 작업만 동기 방식으로 처리한다. 하지만 비동기식으로도 많은 작업이 존재한다. 이를 이해하기 위해서 '이벤트 루트' 개념을 이해해야 한다.
  • 프로세스(pocess)란 프로그램을 구동해 프로그램의 상태가 메모리상에 실행되는 작업 단위를 의미한다.
  • 스레드(thread)는 하나의 프로세스에서 여러 개의 작업을 실행할 수 있도록 더 작은 실행 단위를 말한다. 스레드끼리 메모리를 공유할 수 있어 여러 가지 작업을 동시에 수행할 수 있다.
  • 이벤트 루트란 자바스크립트 런타임 외부에서 자바스크립트의 비동기 실행을 돕기 위해 만들어진 장치라 볼 수 있다.
  • 호출 스택(call stack)은 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택이다.
function bar() {
	console.log('bar')
}

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

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

foo()

 

 

이처럼 호출 스택이 비어 있는지 여부를 확인하는 것이 바로 이벤트 루프다. 코드를 실행하는 것호출 스택이 비어 있는지 확인하는 것이 따로 일어난다. 즉, 두 작업은 동시에 일어날 수 없다. 아래는 비동기 작업의 예문이다.

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

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

function foo() {
	console.log('foo')
    setTimeout(bar(), 0) // setTimeout만 추가
    baz()
}

foo()

 

 

위 코드를 보면 setTimeout이 정확히 0초 뒤에 실행된다는 것을 보장하지 못한다는 것을 알 수 있다.

  • 태스크 큐란 실행히야 할 태스크의 집합을 의미한다. 태스크 큐는 무조건 FIFO형식으로 꺼내오는 자료 구조의 큐(queue)가 아닌 set 형태를 띤다. 실행 가능한 가장 오래된 태스크를 가져오기 때문이다.
  • 이벤트 루프의 역할은 호출 스택에 실행 중인 코드가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는 지 반복해서 확인하는 역할을 한다.
  • 자바스크립트 코드 실행은 싱글 스레드에서 이루어지지만 이러한 외부 Web API 등은 모두 자바스크립트 코드 외부(브라우저나 Node.js)에서 실행되고 콜백이 태스크 큐로 들어가는 것이다.
  • 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다. 대표적으로 Promise가 있다.
  • 렌더링하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어난다.

1.6 리액트에서 자주 사용하는 자바스크립트 문법

  • 바벨은 자바스크립트의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일한다.
  • 구조분해할당이란 배열 또는 객체의 값을 말 그대로 분해해 개별 변수에 즉시 할당하는 것을 의미한다.
  • useState는 배열구조분해할당으로 ,의 위치에 따라 값이 결정된다.
const array = [1, 2, 3, 4, 5]

const [first, second, third, ...arrayRest] = array
// first 1
// second 2
// third 3
// arrayRest [4, 5]
-----------
const array = [1, 2]
const [a=10, b=10, c=10] = array
// a 1
// b 2
// c 10 값이 없는 경우(undefined) 기본값을 사용한다.
  • 전개연산자(spread operator)는 뒤에 ...을 사용하면 나머지 모든 값을 해당 변수에 배열로 넣게 된다.
const array = [1, 2, 3, 4, 5]
const [first, ...rest] = array

//first 1
//rest [2, 3, 4, 5]
  • 객체구조분해할당은 말 그대로 객체에서 값을 꺼내온 뒤 할당하는 것이다.
const object = {
	a: 1,
	b: 2,
	c: 3,
	d: 4,
	e: 5,
}

const { a, b, c, ...objectRest} = object
// a 1
// b 2
// c 3
// objectRest = { d: 4, e: 5}
-------------
// 새로운 이름으로 다시 할당하는 것도 가능하다.

const object = {
	a: 1,
	b: 2,
}

const { a: first, b: second } = object
// first 1
// second 2
  • 전개구문은 배열이나 객체, 문자열과 같이 순회할 수 있는 값에 대해 말 그대로 전개해 간결하게 사용할 수 있는 구문이다.
const arr1 = [ 'a', 'b' ]
const arr2 = arr1

arr1 === arr2 // true. 내용이 아닌 참조를 복사하기 때문

const arr1 = [ 'a', 'b' ]
const arr2 = [...arr1]

arr1 === arr2 //false. 실제로 값만 복사됐을 뿐, 참조는 다르므로 false가 반환된다.
  • 전개 구문에 있는 값을 덮어쓸 것인지, 혹인 그 값을 받아들일지에 따라 순서에 차이가 생기므로 주의해야 한다.
  • 객체초기자는 객체를 선안할 때 객체에 넣고자 하는 키와 값을 가지고 있는 변수가 이미 존재한다면 해당 값을 간결하게 넣어줄 수 있는 방식이다.
const a = 1
const b = 2

const obj = {
	a,
	b,
}

//{a: 1, b: 2}

 

  • Array 프로토타입의 메서드 (map, filter, reduce, forEach)는 배열과 관련된 메서드로 특히 map, filter, reduce는 자주 쓰이면서도 기존 배열의 값을 건드리지 않고 새로운 값을 만들어 내서 안전하게 사용할 수 있다.
  • 삼항 조건 연산자는 자바스크립트에서 유일하게 3개의 피연산자를 취할 수 있는 문법으로 맨 앞에는 true/false를 판별할 수 있는 조건문이 들어가고 그 이후에 물음표가 들어간다. 물음표 뒤에는 참일 경우 반환할 값을, : 뒤에는 거짓일 때 반환할 값을 지정한다.

1.7 선택이 아닌 필수, 타입스크립트

  • 타입스크립트는 티입 체크를 정적으로 런타임이 아닌 빌드(트랜스파일) 타임에 수행할 수 있게 해준다. 
  • any 대신 unknown을 사용하는 것이 좋다.
  • instanceof는 지정한 인스턴스가 특정 클래스의 인스턴스인지 확인할 수 있는 연산자이다.
  • typeof 연산자는 특정 요소에 대해 자료형을 확인하는 데 사용된다.
  • in property in object로 사용되는데 주로 어떤 객체에 키가 존재하는지 확인하는 용도로 사용된다.
  • 제네릭(generic)은 함수나 클래스 내부에서 단일 타입이 아닌 다양한 타입에 대응할 수 있도록 도와준다.
  • 인덱스 시그니처는 객체의 키를 정의하는 방식이다. 

리액트를 시작한지 얼마 안돼서 이유도 모르고 바로 함수 컴포넌트나 화살표함수 등 무턱대고 썼었는데 클래스 컴포넌트를 이해하니 지금 쓰는 함수 컴포넌트가 구동하는 데에 어떤 차이가 있는지 알 수 있었다. 클로저도 이번에 제대로 이해하게 되었으며 비용이 큰 것을 항상 생각해야겠다는 생각이 들었다.