이 글은 책 모던 자바스크립트 33장 ~ 38장을 읽고 정리한 글입니다.
1997년 JS가 ECMAScript로 표준화된 이래로 6개의 타입
심벌은 ES6에서 도입된 7번째 데이터 타입으로 변경 불가능한 원시 타입의 값이다.
다른 값과 중복이 되지 않아서 이름의 충돌 위험이 없는 유일한 프로퍼티 키를 만들기 위해 사용!
프로퍼티 키로 사용할 수 있는 것 --> 빈 문자열을 포함하는 모든 문자열 또는 심벌 값
심벌 값은 Symbol 함수를 호출하여 생성해야 한다. 외부로 노출되지 않아 확인할 수 없고, 다른 값과 절대 중복되지 않는 유일무이한 값이다.
const mySymbol = Symbol();
console.log(typeof mySymbol); // symbol
// 심벌 값은 외부로 노출되지 않아 확인할 수 없다.
console.log(mySymbol) // Symbol()
Symbol 함수는 new 연산자와 함께 호출 하면 TypeError가 발생한다.
Symbol에 인수로 전달되는 문자열은 생성된 심벌 값에 대한 설명(Description)으로 디버깅 용으로만 사용되며, 심벌 값 생성에 어떠한 영향동 주지 않는다. --> 심벌 값에 대한 설명이 같더라도 유일무이한 심벌 값이다.
심벌 값 또한 암묵적으로 래퍼 객체를 생성하는데, description 프로퍼티와 Symbol.prototype의 toString 메서드를 가지고 있다.
암묵적으로 문자열이나 숫자 타입으로 변환되지는 않고, 불리언 타입으로는 변환된다.
Symbol.for 메서드는 인수로 전달받은 문자열을 키로 사용하여 키와 심벌 값의 쌍들이 저장되어 있는 전역 심벌 레지스트리에서 해당하는 키와 일치하는 심벌 값을 검색한다.
Symbol의 경우 호출 시마다 유일무이한 심벌 값을 생성하지만 전역 심벌 레지스트리에서 관리되지 않는다. Symbol.for 메서드를 사용하면 전역에서 중복되지 않는 심벌 값을 단 하나만 생성하여 전역 심벌 레지스트리에서 공유 가능하다.
Symbol.keyFor 메서드를 사용하면 전역 심벌 레지스트리에 저장된 심벌 값의 키를 추출할 수 있다.
const s1 = Symbol.for('mySymbol');
Symbol.keyFor(s1); // mySymbol
const s2 = Symbol('foo');
Symbol.keyFor(s2); // undefined
객체 안에서 의미있는 상수를 정의하기 위해서는 다른 변수값과 중복되지 않는 심벌 값을 사용하여 정의하면 좋다.
const Direction = {
UP: Symbol('up'),
DOWN: Symbol('down'),
LEFT: Symbol('left'),
RIGHT: Symbol('right')
};
enum은 JS에서 지원하지는 않지만 객체의 변경을 방지하기 위해 객체를 동결하는 Object.freeze 메서드와 심벌값을 같이 사용하여 구현할 수 있다.
const Direction = Object.freeze({
UP: Symbol('up'),
DOWN: Symbol('down'),
LEFT: Symbol('left'),
RIGHT: Symbol('right')
});
심벌 값으로 프로퍼티 키를 만들기 위해서는 대괄호를 사용해야 한다. 접근시에도 물론 대괄호를 사용한다.
const obj = {
[Symbol.for('mySymbol')]: 1
};
obj[Symbol.for('mySymbol')]; // -> 1
심벌 값은 유일무이한 값으로 심벌 값으로 프로퍼티 키를 만들면 다른 프로퍼티 키와 절대 충돌하지 않는다.
심벌 값을 프로퍼티 키로 만든 프로퍼티는 for ... in 문이나 Object.keys, Object.getOwnPropertyNames 메서드로 찾을 수 없다.
그러나 ES6에서 도입된 Object.getOwnPropertySymbols 메서드 사용 시, 심벌 값을 프로퍼티 키로 사용하여 생성한 프로퍼티를 찾을 수 있다.
console.log(Object.getOwnPropertySymbols(obj));
표준 빌트인 객체에 사용자 정의 메서드를 직접 추가하는 것은 이름이 중복되어 덮어 씌어질 가능성이 있기 때문에 권장하지 않는다.
이러한 경우 심벌 값으로 프로퍼티 키를 생성하여 이용한다.
Array.prototype[Symbol.for('sum')] = function () {
return this.reduce((acc,cur) => acc + cur, 0);
};
[1,2][Symbol.for('sum')](); // -> 3
자바스크립트가 기본 제공하는 빌트인 심벌 값으로, Array, String, Map, Set과 같이 for ... of 문으로 순회 가능한 빌트인 이터러블은 Well-known Symbol인 Symbol.iterator를 키로 갖는 메서드를 갖고, 이를 호출 하면 이터레이터를 반환하도록 되어 있다.
빌트인 이터러블 --> 이터레이션 프로토콜 준수
일반 객체에 이터러블처럼 동작하도록 구현하는 방법
const iterable = {
// 이 메서드를 구현하여 이터러블 프로토콜 준수
[Symbol.iterator]() {
let cur = 1;
const max = 5;
return {
// Symbol.iterator 메서드는 next 메서드를 소유한 이터레이터를 반환
next() {
return { value: cur++, done: cur > max + 1};
}
};
}
};
for (const num of iterable) {
console.log(num); // 1 2 3 4 5
}
ES6에서 도입된 순회 가능한 데이터 컬렉션을 만들기 위한 규칙이다.
이터레이션 프로토콜을 준수하는 이터러블은 for ... of문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.
이터레이션 프로토콜은 2가지로 나뉜다.
이터러블 프로토콜
Symbol.iterator 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 호출 시 이터레이터 프로토콜을 준수한 이터레이터를 반환함. --> 이터러블 프로토콜이라하며 for..of, 스프레드, 디스트럭처링 할당의 대상으로 사용 가능하다.
이터레이터 프로토콜
Symbol.iterator 메서드 호출 시 이터레이터 프로토콜을 준수한 이터레이터를 반환함. 이는 next 메서드를 소유하는 데, next 메서드 호출 시 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환
이러한 규약을 이터레이터 프로토콜이라 하며 이터레이터 프로토콜을 준수한 객체를 이터레이터라 한다. 이터레이터는 이터러블 요소를 탐색하기 위한 포인터 역할!
[Symbol.iterator]() { ... } --> 이터러블
위 Symbol.iterator가 return 시 아래의 이터레이터 반환
// 이터레이터
{
next() {
return { value: any, done: boolean }; // 이터러블 리절트 객체
}
}
이터러블 인지 확인
const isIterable = v => v!== null & typeof v[Symbol.iterator] === 'function';
배열은 Array.prototype으로 Symbol.iterator 메서드를 상속 받아 for ... of 문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용 가능
이 Symbol.iterator를 직접 구현하지 않거나 상속받지 않은 일반 객체는 사용 불가하다.
2021년 1월 현재 stage 4단계에 제안되어 있는 스프레드 프로퍼티 제안은 일반 객체에 스프레드 문법의 사용을 허용한다고 함.
Symbol.iterator 메서드 호출 시 이터레이터를 반환하는데 이 이터레이터는 next 메서드를 갖는다.
next는 각 요소를 순회하기 위한 포인터의 역할.
next 메서드 호출 시 이터러블을 순차적으로 한 단계씩 순회하며 결과를 나타내는 이터레이터 리절트 객체 반환
리절트 객체의 value 프로퍼티는 현재 순회중인 이터러블 값을, done 프로퍼티는 이터러블 순회 완료 여부를 나타냄
빌트인 이터러블 | Symbol.iterator 메서드 |
---|---|
Array | Array.prototype[Symbol.iterator] |
String | String.prototype[Symbol.iterator] |
Map | Map.prototype[Symbol.iterator] |
Set | Set.prototype[Symbol.iterator] |
TypedArray | TypedArray.prototype[Symbol.iterator] |
arguments | arguments.prototype[Symbol.iterator] |
DOM 컬렉션 | NodeList, HTML.prototype[Symbol.iterator] |
for ... in 문 --> 객체의 프로토타입 체인상에 존재하는 모든 프로퍼티 중 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거한다. (심벌 제외)
for .. of 문 --> 내부적으로 이터레이터의 next 메서드를 호출하여 이터러블 순회하며 next 메서드가 변환한 이터레이터 리절트 객체의 value 프로퍼티 값을 for ... of문 변수에 할당. (done이 true면 이터러블 순회 종료)
유사 배열 객체는 이터러블이 아닌 일반 객체여서 Symbol.iterator 메서드가 없고, for ... of 문으로 순회 불가능
ES6 이전의 순회 가능한 데이터 컬렉션들은 통일된 규약 없이 for, for...in, forEach 등으로 다양한 방법으로 순회를 함. --> 이터레이션 프로토콜을 준수하는 이터러블로 통일해 for ... of, 스프레드, 배열 디스트럭처링 할당의 대상으로 사용할수 있도록 일원화(더 효율적이다)
이터레이션 프로토콜은 데이터 소비자와 데이터 공급자를 연결하는 인터페이스 역할.
피보나치 수열 사용자 정의 이터러블
const fibonacci = {
[Symbol.iterator]() {
let [pre, cur] = [0, 1];
const max = 10;
return {
next() {
[pre,cur] = [cur, pre + cur];
return { value: cur, done: cur >= max };
}
};
}
};
for (const num of fibonacci) {
console.log(num);
}
수열의 최대값을 인수로 전달받아 이터러블을 반환하는 함수로 만들면 된다.
const fibonacciFunc = function(max) {
...
return {
[Symbol.iterator]() {
return {...};
}
};
};
이터레이터를 생성하려면 Symbol.iterator 메서드를 호출해야함
이터러블이면서 이터레이터인 객체
{
[Symbol.iterator]() { return this; },
next() {
return { value: any, done: boolean };
}
}
이터러블이면서 이터레이터인 객체는 Symbol.iterator 메서드를 호출하지 않아도 next 메서드 사용 가능.
그러나 일반적인 이터러블인 객체는 next메서드를 사용하기 위해서는const iterator = [Symbol.iterator]();
와 같이 한번 호출해야함
done 프로퍼티 생략시 무한 이터러블 생성 가능
이터러블은 지연 평가를 통해 데이터를 생성하므로 for ... of 문이나 배열 디스트럭처링 할당 등이 실행되기 이전까지 데이터 생성 X --> for ... of 의 경우 next 메서드 호출 전까지 데이터 생성 하지 않음
--> 불필요한 데이터를 미리 생성하지 않고 필요한 데이터를 필요한 순간에 생성하여 빠른 실행 속도, 불필요한 메모리 소비를 하지 않고 무한도 표현가능
ES6에서 도입되어 하나로 뭉쳐 있는 여러 값들의 집합을 펼쳐서 개별적인 값들의 목록으로 만든다.
스프레드 문법의 대상은 for ... of 문으로 순회할 수 있는 이터러블에 한정된다.
... 은 피연산자를 연산하여 값을 생성하는 연산자가 아닌 값들의 목록을 나타낸다.
아래와 같이 쉼표로 구분한 값의 목록을 사용하는 문맥에서만 사용 할 수 있다.
Math.max(...arr);
이전에는 이것과 동일한 연산을 하기 위해 Function.prototype.apply를 사용하여 배열을 2 번째 인수에 전달하여 요소들의 목록을 제공하였다.
rest 파라미터의 경우 인수들의 목록을 배열로 전달받기 위해 매개변수 이름 앞에 ...을 붙임
스프레드 문법은 뭉쳐있는 배열과 같은 이터러블을 펼쳐서 개별적인 값들의 목록을 만듦
--> 서로 반대의 개념(Rest vs 스프레드)
스프레드 문법 사용 시, ES5에서 사용하던 기존의 방식보다 더욱 간결하고 가독성 좋게 표현 가능하다.
// ES5
var arr = [1, 2].concat([3, 4]);
console.log(arr); // [1, 2, 3, 4]
// ES6
const arr = [...[1, 2], ...[3, 4]];
console.log(arr); // [1, 2, 3, 4]
전에는 배열을 해체하기 위해 Function.prototype.apply 메서드를 사용하여 splice 메서드를 호출한다.
스프레드 문법 사용
// ES6
const arr1 = [1, 4];
const arr2 = [2, 3];
arr1.splice(1, 0, ...arr2);
console.log(arr1); // [1, 2, 3, 4]
전에는 slice 메서드 사용
스프레드 문법
// ES6
const origin = [1, 2];
const copy = [...origin];
console.log(copy); // [1, 2]
console.log(copy === origin); // false
이터러블을 배열로 변환하려면 Function.prototype.apply 또는 Function.prototype.call 메서드를 사용하여 slice 메서드를 호출함 --> 이터러블뿐만 아니라 유사 배열 객체도 배열로 변환 가능
arguments 객체를 스프레드 문법을 사용하여 간단하게 배열로 변환 가능 --> [...arguments]
더 나은 방법은 Rest 파라미터 이용
const sum = (...args) => args.reduce((pre, cur) => pre + cur, 0);
console.log(sum(1, 2, 3,)); // 6
이터러블이 아닌 유사 배열 객체는 스프레드 문법의 대상이 될 순 없다. --> 유사배열 객체를 Array.from 메서드를 사용해 배열로 반환하여 사용
stage 4 단계에 제안이 되어있는 스프레드 프로퍼티를 사용하면 객체 리터럴의 프로퍼티 목록에서도 스프레드 문법을 사용할 수 있다.
스프레드 프로퍼티 전에는 ES6에서 도입된 Object.assign 메서드를 사용하여 여러개의 객체 병합함.
// 객체 병합. 프로퍼티가 중복되는 경우 뒤에 위치한 프로퍼티가 우선권을 갖는다.
const merged = { ... { x:1, y:2 }, ... { y:10, z:3} };
console.log(merged); // { x:1, y:10, z:3 }
// 특정 프로퍼티 변경
const changed = { ... {x: 1, y: 2 }, y: 100 };
console.log(changed); // { x:1, y:100 }
// 프로퍼티 추가
const added = { ... {x:1, y:2}, z: 0};
console.log(added) // { x:1, y:2, z:0 }
디스트럭처링 할당은 구조화된 배열과 같은 이터러블 또는 객체를 destructuring하여 1개 이상의 변수에 개별적으로 할당하는 것을 말한다.
--> 이터러블 또는 객체 리터럴에서 필요한 값만 추출하여 변수에 할당할때 유용하게 사용한다.
ES6 배열 디스트럭처링 할당은 배열의 각 요소를 배열로 부터 추출하여 1개 이상의 변수에 할당하는데, 배열 디스트럭처링 할당의 대상은 이터러블이어야 하며, 할당 기준은 인덱스이다.
const arr = [1, 2, 3];
const [one, two, three] = arr;
console.log(one, two, three);
할당 연산자 왼쪽에 값을 할당받은 변수를 선언해야함 --> 배열 리터럴로
우변에 이터러블 할당안하면 에러 발생
배열 디스트럭처링 할당의 변수 선언문은 선언과 할당을 분리할 수 있지만, const 키워드로 변수를 선언할 수 없어서 권장 X
let x;
let y;
[x, y] = [1, 2];
console.log(x, y);
배열 디스트럭처링 할당을 할 시에 변수의 개수와 이터러블의 요소 개수가 반드시 일치할 필요는 없다.
const [a, b, c = 3] = [1, 2];
console.log(a, b, c);
const [e, f = 10, g = 3] = [1, 2];
console.log(e, f, g);
기본값 설정이 가능하다.
배열 디스트럭처링 할당을 위해 변수에 Rest파라미터와 유사하게 Rest 요소 ... 를 사용할 수 있다. 이 경우 Rest 요소는 반드시 마지막에 위치해야한다.
const [x, ...y] = [1, 2, 3];
console.log(x, y);
객체의 각 프로퍼티를 객체로부터 추출하여 1개 이상의 변수에 할당한다. 할당의 대상은 객체이어야 하며, 할당 기준은 프로퍼티 키다.
const user = { firstName: 'JeongMin', lastName: 'Lee' };
const { lastName, firstName } = user;
console.log(firstName, lastName);
할당 연산자 왼쪽에 프로퍼티 값을 할당받을 변수를 선언해야 하는데, 객체 리터럴로 선언해야한다.
이때, 우변에 객체 또는 객체로 평가될 수 있는 표현식을 할당하지 않으면 에러 발생!
const { lastName, firstName } = user;
// 위와 아래는 동치, 메서드 축약표현을 통해 선언한 것과 같음
const { lastName: lastName, firstName: firstName } = user;
// 객체의 프로퍼티 키와 다른 변수 이름으로 프로퍼티 값을 할당 받으려면 다음과 같이 변수 선언함
const user = { firstName: 'JeongMin', lastName: 'Lee' };
const { lastName: ln, firstName: fn } = user;
console.log(fn, ln); // JeongMin Lee
// 기본값 설정 또한 가능하다.
const { firstName = 'JeongMin', lastName } = { lastName: 'Lee' };
console.log(firstName, lastName);
const { firstName: fn = 'JeongMin', lastName: ln } = { lastName: 'Lee' };
console.log(fn, ln);
객체 디스터럭처링 할당은 객체에서 프로퍼티 키로 필요한 프로퍼티 값만을 추출하여 변수에 할다앟고 싶을 때 유용하다.
또한 객체를 인수로 전달받는 함수의 매개변수에도 사용할 수 있다.
function printTodo(todo) {
console.log(
`할일 ${todo.content}은 ${todo.completed ? '완료' : '비완료'} 상태입니다.`
);
}
function printTodo({ content, completed }) {
console.log(`할일 ${content}은 ${completed ? '완료' : '비완료'} 상태입니다.`);
}
printTodo({ id: 1, content: 'HTML', completed: true });
객체를 인수로 전달받는 매개변수 todo에 객체 디스터럭처링 할당을 사용하면 좀 더 간단하고 가독성 좋게 표현할 수 있다.
중첩 객체의 경우 다음과 같이 사용한다.
const user = {
name: 'Lee',
address: {
zipCode: '03068',
city: 'Seoul'
}
};
const {
address: { city }
} = user;
console.log(city); // Seoul
객체 디스트럭처링 할당을 위한 변수에 Rest 파라미터나 Rest 요소와 유사하게 Rest 프로퍼티 ... 을 사용할 수 있다. --> 반드시 마지막에 위치해야 한다.
const { x, ...rest } = { x: 1, y: 2, z: 3 };
console.log(x, rest); // 1 {y:2, z:3}
Set객체는 중복되지 않는 유일한 값들의 집합이다. Set 객체는 배열과 유사하지만 다음과 같은 차이가 있다.
구분 | 배열 | Set 객체 |
---|---|---|
동일한 값을 중복하여 포함할 수 있다. | O | X |
요소 순서에 의미가 있다. | O | X |
인덱스로 요소에 접근할 수 있다. | O | X |
const set = new Set();
console.log(set); Set(0) {}
생성자 함수로 생성되며, Set 생성자 함수는 이터러블을 인수로 전달받아 Set 객체를 생성한다. 이때 이터러블의 중복된 값을 Set 객체에 요소에 저장되지 않는다.
Set.prototype.size 프로퍼티를 사용하여 확인한다.
size 프로퍼티는 getter함수만 존재하는 접근자 프로퍼티 --> setter가 존재하지 않아 프로퍼티에 숫자를 할당하여 Set 객체의 요소 개수 변경 불가
Set.prototype.add 메서드를 사용
add 메서드는 새로운 요소가 추가된 Set 객체를 반환하여 add뒤에 add를 연속적으로 호출할 수 있다.
const set = new Set();
set.add(1).add(2).add(2);
console.log(set); // Set(2) { 1, 2 }
===은 NaN과 NaN을 다르다고 평가하지만 Set은 NaN과 NaN을 같다고 평가하여 중복 추가를 허용하지 않는다. +0, -0도 마찬가지로 같다고 평가함.
--> NaN, +0, -0 중복추가 허용하지 않는다.
Set객체는 객체나 배열과 같이 모든 값을 요소로 저장할 수 있다.
Set.prototype.has 메서드를 사용 --> 불리언 값 반환
Set.prototype.delete 메서드 사용 --> 불리언 값 반환
인덱스가 아닌 삭제하려는 요소 값을 인수로 전달함
존재하지 않는 요소 삭제시 에러 없이 무시됨.
add 메서드와 달리 연속적으로 호출이 불가능하다 --> 불리언 값을 반환하기 때문에
Set.prototype.clear 메서드를 사용한다. --> 언제나 undefined 반환
Set.prototype.forEach 메서드를 사용한다. forEach 메서드의 콜백 함수 내부에서 this로 사용될 객체를 인수로 전달하는데 이때 콜백 함수는 3개의 인수를 전달받는다.
Array.prototype.forEach 메서드와 인터페이스 통일하기 위해 다음과 같이 동작
Set 객체는 이터러블이기 때문에 for ... of 문으로 순회가 가능하며, 스프레드 문법과 배열 디스트럭처링의 대상이 될 수 있다.
const set = new Set([1, 2, 3]);
console.log(Symbol.iterator in set); // true
for (const value of set) {
console.log(value); // 1 2 3
}
console.log([...set]); // [ 1, 2, 3 ]
const [a, ...rest] = set;
console.log(a, rest); // 1 [ 2, 3 ]
Set 객체는 요소의 순서에 의미를 갖지 않지만 순회하는 순서는 요소가 추가된 순서에 따른다. --> 다른 이터러블의 순회와 호환성 유지를 위해
교집합 -> Set.prototype.intersection 메서드를 사용
합집함 -> Set.prototype.union 메서드를 사용
차집합 -> Set.prototype.difference 메서드를 사용
부분집합과 상위 집합 -> Set.prototype.isSuperset 메서드를 사용
Map 객체는 키와 값의 쌍으로 이루어진 컬렉션이다. Map 객체는 객체와 유사하지만 다음과 같은 차이가 있다.
구분 | 객체 | Map 객체 |
---|---|---|
키로 사용할 수 있는 값 | 문자열 또는 심벌 값 | 객체를 포함하는 모든 값 |
이터러블 | X | O |
요소 개수 확인 | Object.keys(obj).length | map.size |
Map 생성자 함수로 생성한다. 인수를 전달하지 않을 시 빈 Map 객체 생성.
Map 생성자 함수는 이터러블을 인수로 전달받아 Map 객체를 생성한다. 이때 인수로 전달되는 이터러블은 키와 값의 쌍으로 이루어진 요소로 구성되어야 한다.
중복된 키를 갖는 요소 존재시 값이 덮어 씌워진다. --> 중복된 키를 갖는 요소 존재 불가
Map.prototype.size 프로퍼티 사용 --> Set과 마찬가지로 getter 함수만 존재하는 접근자 프로퍼티여서 size 프로퍼티에 숫자를 할당하여 Map 객체의 요소 개수 변경 불가.
Map 객체에 요소를 추가할 때는 Map.prototype.set 메서드를 사용 --> 새로운 요소가 추가된 Map 객체를 반환하기 때문에 연속적으로 set 메서드를 호출할 수 있다.
Set과 마찬가지로 NaN과 +0, -0을 같다고 평가하여 중복 추가를 허용하지 않는다.
Map은 객체와 달리 키 타입에 제한이 없고, 객체 포함 모든 값을 키로 사용할 수 있다. --> 일반 객체와 가장 두드러지는 차이점
Map.prototype.get 메서드를 사용 --> 인수로 전달한 키를 갖는 요소가 존재하지 않으면 undefined 반환하고 존재하면 전달한 키를 갖는 값을 반환함.
Map.prototype.has 메서드 사용 --> 불리언 값 반환
Map.prototype.delete --> 불리언 값 반환
Map.prototype.clear 메서드 사용 --> undefined 반환
Map.prototype.forEach 메서드 사용
forEach 메서드 콜백 함수는 3개의 인수를 전달 받음
Map 객체는 이터러블이기 때문에 for...of, 스프레드 문법, 배열 디스트럭처링 할당의 대상이 될 수 있다.
Map 객체는 이터러블이면서 동시에 이터레이터인 객체를 반환하는 메서드를 제공한다.
Map.prototype.keys --> 키를 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환
Map.prototype.values --> 요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환
Map.prototype.entries --> 요소키와 요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체 반환
Map 또한 객체를 순회하는 순서는 요소가 추가된 순서를 따른다.
--> 다른 이터러블의 순회와 호환성을 유지하기 위함
웹 어플리케이션의 클라이언트 사이드 자바스크립트는 브라우저에서 HTML, CSS와 함께 실행된다. 이와 관련하여 브라우저가 HTML, CSS, JS로 작성된 문서를 어떻게 파싱하여 브라우저에 렌더링하는 지 알아보자.
파싱: 프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어 들여 실행하기 위해 텍스트 문서의 문자열을 토큰으로 분해하고, 토큰에 문법적 의미와 구조를 반영하여 트리 구조의 자료구조인 파스트리를 생성하는 일련의 과정
렌더링: HTML, CSS, JS로 작성된 문서를 파싱하여 브라우저에 시각적으로 출력하는 것
브라우저 렌더링 과정
브라우저는 렌더링에 필요한 리소스를 서버에 요청하여 응답한 리소스를 파싱하여 렌더링한다.
이를 위해 주소창을 제공하고, URL을 입력하면 URL의 호스트 이름이 DNS를 통해 IP주소로 변환되고 이 IP 주소를 갖는 서버어게 요청을 전송한다.
브라우저의 렌더링 엔진이 HTML을 파싱하는 도중 외부 리소스를 로드하는 태그 (link, img, script)를 만나면 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청한다.
HTTP는 웹에서 브라우저와 서버가 통신하기 위한 프로토콜이다.
HTTP/1.1은 기본적으로 커넥션 당 하나의 요청과 응답만을 처리한다. --> 리소스의 동시 전송 불가, 리소스의 개수에 비례하여 응답시간이 증가하는 단점을 가짐
HTTP/2는 커넥션당 여러개의 요청과 응답, 즉 다중 요청/응답이 가능해졌다. --> 1.1에 비해 약 50% 정도 속도가 빠르다.
index.html이 서버로부터 응답되었다고 가정해보자.
브라우저 렌더링 엔진은 아래의 과정을 통해 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다.
서버에 존재하던 HTML 파일이 브라우저의 요청에 의해 응답된다. 서버는 요청받은 HTML파일을 읽어 들여 메모리에 저장후, 저장도니 바이트를 인터넷을 경유하여 응답함.
브라우저는 서버에서 HTML 문서를 바이트 형태로 응답받고 그 이후 meta 태그의 charset에 의해 지정된 인코딩 방식을 기준으로 문자열로 변환됨.(response header에 담겨 응답됨)
문자열로 변환된 HTML 문서를 읽어 들여 문법적 의미를 갖는 코드의 최소단위인 토큰들로 분해
각 토큰들을 객체로 변환하여 노드들을 생성: 토큰의 내용에 따라 문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드가 생성 --> DOM을 구성하는 기본 요소
HTML 문서는 HTML 요소들의 집합으로 이루어지며 HTML 요소는 중첩 관계를 갖는다. 이러한 관계를 반영하여 모든 노드들을 트리 자료구조로 구성한다. --> DOM이라 부름
DOM은 HTML을 파싱한 결과물이다.
렌더링 엔진은 HTML을 처음부터 한 줄씩 파싱하여 DOM을 생성하는데 이 과정에서 link나 style 태그를 만나면 일시적으로 중지하고 CSS 파일을 서버에 요청한다.
CSS를 HTML과 동일한 파싱과정(바이트 -> 문자 -> 토큰 -> 노드 -> CSSOM)을 거치며 해석하여 CSSOM을 생성한다. 이후 파싱이 완료되면 다시 HTML을 파싱이 중단된 지점부터 파싱하여 DOM 생성 재개 한다.
CSSOM 은 CSS의 상속을 반영하여 생성되는데 부모요소에 적용된 스타일 CSS가 자식에게 또한 적용되는 경우 그러한 것을 모두 반영한다.
DOM과 CSSOM은 렌더링을 위해 렌더 트리로 결합된다.
브라우저 화면에 렌더링 되지 않는 노드(meta, script태그)와 CSS에 의해 비표시(display:none)되는 노드들을 포함 X
렌더 트리 --> 브라우저 화면에 렌더링 되는 노드만으로 구성, HTML 요소의 레이아웃을 계산하는데 사용되며 브라우저 화면에 픽셀을 렌더링 하는 페인팅 처리에 입력.
레이아웃 계산과 페인팅이 재차 실행되는 경우
JS에 의한 노드 추가 또는 삭제
브라우저 창의 리사이징에 의한 뷰표트 크기 변경
HTML 요소의 레이아웃에 변경을 발생시키는 width/height, margin, padding, border, display, position, top/right/bottom/left 등의 스타일 변경
--> 가급적 리렌더링이 빈번하게 발생하지 않도록 만드는 것이 필요
HTML을 한 줄씩 파싱하다가 script 태그를 만나면 DOM 생성을 일시 중지하고 script 태그 내의 JS 코드를 파싱과 실행이 종료 되면 렌더링 엔진으로 다시 제어권을 넘겨 DOM 생성 재개한다.
JS 엔진은 JS를 해석하여 AST(추상적 구문 트리)를 생성하고 이것을 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트 코드를 생성하여 실행한다.
토크나이징: 소스코드를 어휘 분석하여 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해한다
파싱: 토큰들의 집합을 구문 분서하여 AST를 생성한다. 단순히 인터프리터나 컴파일러만 사용하는 것이 아닌 TypeScript, Babel, Prettier 같은 트랜스파일러를 구현할 수 있다.
바이트코드 생성과 실행: 파싱의 결과물로 생성된 AST는 인터프리터가 실행될 수 있는 중간 코드인 바이트코드로 변환되고 인터프리터에 의해 실행된다.(V8의 경우 터보팬이라 불리는 컴파일러에 의해 최적화된 머신 코드로 컴파일 되어 성능을 최적화함, 코드 사용 빈도가 줄면 다시 디옵티마이징을 한다)
JS 코드에 의해 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우 변경된 DOM, CSSOM은 다시 렌더 트리로 결합되어 변경된 렌더트리 기반으로 레이아웃과 페인팅 과정을 거친다.
리플로우: 레이아웃 재 계산(레이아웃에 영향을 주는 변경이 발생한 경우)
리페인트: 재결합된 렌더 트리르 기반으로 다시 페인트
무조건 리플로우가 발생해야 리페인트가 발생하는 것은 아님. 리플로우 없이도 리페인트 발생가능(레이아웃에 영향이 없는 변경인 경우)
렌더링 엔진과 JS 엔진은 직렬적으로 파싱 수행
DOM의 생성이 제대로 완료되지 않은 상태에서 script 호출 시 문제가 발생할 수 있음 --> 닫는 body 태그의 바로 위에 위치시켜야함
그 이유는 다음과 같다
async와 defer는 HTML5부터 추가되었으며 src 어트리뷰트를 통해 JS 파일을 로드하는 경우에만 사용이 가능하다. 둘다 IE10 이상에서 지원된다.
async --> JS 파일의 로드가 비동기적으로 동시에 진행. 파싱과 실행은 로드가 완료된 직후이다.
defer --> 마찬가지로 비동기적으로 동시에 진행되지만 HTML파싱이 완료된 직후에 JS 파싱과 실행이 진행된다.
35장
... --> 스프레드 연산자가아닌 스프레드 문법!!!
연산자인 경우 그 결과가 값이 만들어져야 하는데 값이 만들어지는 것이 아니기 때문!
Concat --> 배열 풀어서 값을 넣어줌
Push --> 배열을 풀지 않고 그대로 값을 넣어줌
38장
프론트엔드 개발자가 만드는 js는 html이 없으면 무용지물
html은 정보와 구조를 가지고있음
정보 --> 텍스트 요소
태그 --> 텍스트 요소를 부가 설명하는 메타 정보
구조 --> 중첩 관계
Css --> 스타일링
Js --> 웹사이트 동적 담당뿐만아니라 서버와 통신 및 DOM 조작
Html, css,js가 모여 view를 만듦
Html --> 브라우저에서만 동작, js 또한 html이 브라우저에서 어떻게 동작해야하는 지 알아야 효율적으로 코드를 작성할 수 있다.
렌더의미 중의성
브라우저 렌더링과정 38장에서 가장 중요한 것은 리플로우!
리플로우가 웬만큼 발생하지 않도록 짜야한다.(가장 무겁기 때문에)
클라이언트는 서버에 정적 리소스를 요청!(html, css, js, 이미지, 폰트 파일 데이터등등..)
Host .com 으로 끝나는 곳 까지
Path .com뒤의 /로 나누어진 경로들을 말함
header에 무슨 파일인지 정보가 담겨서 옴
Http의 Post method request의 body에 담겨서 보냄
Https/http: 프로토콜
google.com --> 도메인
쿼리스트링 --> path없이도 사용 가능
index.html을 암묵적으로 반환함 (루트 요청이라고함)
Dom -> document가 가리킴
window.document안에 존재.
API --> application programming interface
DOM과 CSSOM의 트리 구조는 다르다.
DOMContentLoaded 이것은 알아두기(돔이 완성되어 로드되는 시점)