JavaScript Fundamental( ES6 )

khxxjxx·2022년 5월 19일
0

ES5 문법 보러가기

1. Variable

  • 저번 ES5를 설명하면서 변수를 선언하기위해 var 키워드를 사용한다고 했습니다.
  • 하지만 var의 경우 중복 선언이 가능하기 때문에 같은 변수명을 사용했을 경우 에러를 표시해주지 않아 버그를 발생시킬 수 있는 위험이 있습니다.
var name = "heejin"
var name = "jin"

console.log(name)  // jin
  • 그래서 var의 문제점을 보완하기 위해 ES6에선 letconst가 추가되었고, 위 예제 코드를 let 또는 const 를 사용하여 변수를 선언하면 이미 선언되었다는 에러 메세지가 나오게 됩니다.
  • 함수 스코프를 갖는 var와 달리 let과 const는 블록 스코프를 갖기 때문에 변수를 선언한 블록과 그 내부 블록들에서 유효합니다.

let

  • rw(read/write)
  • 값을 재할당 하는게 가능합니다.
let name = "heejin"
name = "jin"

console.log(name)  // jin

const

  • r(read only)
  • 값을 한번 할당하면 재할당 할 수 없습니다.
const name = "heejin"
name = "jin"  // 에러발생 Assignment to constant variable
  • 하지만 reference type의 경우 참조하고 있는 값의 변경은 가능합니다.
const person = {
  name : "heejin",
  age : 26
}

person.age = 20

console.log(person)  // {name: 'heejin', age: 20}

person = {
  name : "jin"
}  // 에러발생 Assignment to constant variable
  • 만약 reference type을 정의하고 값의 변동을 원치 않는다면 Object.freeze(변수명)을 사용할 수 있습니다.
const person = {
  name : "heejin",
  age : 26
}

Object.freeze(person)
person.age = 20

console.log(person)  // {name: 'heejin', age: 26}

Primitive Type

  • ES6에서 원시타입인 Symbol 데이터 유형이 추가되었습니다.

Symbol

  • 일반적으로 심볼(symbol)은 객체의 프로퍼티 키를 고유하게 설정함으로써 프로퍼티 키의 충돌을 방지하기 위해 사용됩니다.
  • 심볼은 Symbol 함수를 호출함으로써 생성할 수 있으며 String, Number, Boolean과 같이 래퍼 객체를 생성하지만 new 연산자는 사용할 수 없습니다. *래퍼 객체 : 프로퍼티나 메서드를 참조할 때 일시적으로 임시 객체가 생성되고 참조가 끝나면 사라진다.
  • Symbol() 함수를 호출할 때 문자열을 인자로 전달할 수 있고 이 문자열은 Symbol 생성에 어떠한 영향을 주지 않고 생성된 symbol에 대한 설명(description)으로 디버깅 용도로만 사용됩니다.
  • Symbol은 고유(unique)한 데이터로 호출할 때마다 매번 새로운 심볼이 생성됩니다.
const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol("foo")
const sym4 = Symbol("foo")

console.log(sym1 === sym2)  // false
console.log(sym3 === sym4)  // false

1) 전역 심볼 레지스트리(global symbol registry)

  • 자바스크립트 엔진이 관리하는 전역 심볼 레지스트리는 심볼들이 저장되는 전역 공간으로 심볼을 공유하기 위한 용도로 사용됩니다.
  • 심볼을 공유하기 위해선 그 심볼이 키를 가지고 있어야 하며 레지스트리에 접근할 수 있는 함수로는 Symbol.for, Symbol.keyFor 메서드가 있습니다.

2) Symbol.for() & Symbol.keyFor()

  • Symbol.for 메서드는 인자로 전달받은 문자열 값을 키로 사용해 전역 심볼 레지스트리에 해당 키와 일치하는 심볼 값을 검색하여 레지스트리에 이미 심볼이 있으면 해당 심볼을 반환하고 없으면 새로 생성하여 반환합니다.
  • Symbol.keyFor 메서드는 심볼 값을 인자로 받아서 전역 심볼 레지스트리에 저장된 심볼 값 키를 찾아 반환하고 없으면 undefined를 반환합니다.
const symA = Symbol.for('a')
const symB = Symbol.for('a')
symA === symB // true
Symbol.keyFor(symA) // a
  • Symbol() 함수는 호출될 때마다 심볼 값을 생성하지만 키를 가지고 있지 않아 전역 심볼 레지스트리에 저장되지 않습니다.(인수로 전달하는 문자열은 key값이 아니라 그저 description일 뿐이기 때문)

3) for ... in & JSON.stringify()

  • 심볼은 일반적으로 객체의 프로퍼티 키로 사용된다고 했지만 for ...in 문법에서 키가 심볼인 프로퍼티들은 열거되지 않습니다. 또한, Object.getOwnPropertyNames 메서드도 키가 심볼인 프로퍼티들은 반환하지않고 JSON.stringify 메서드도 키가 심볼인 프로퍼티들은 무시됩니다.
  • 만약 키가 심볼인 프로퍼티들의 목록을 확인하고 싶다면 Object.getOwnPropertySymbols 메서드를 사용하면 됩니다. 만약 심볼로 이뤄진 프로퍼티가 없다면 빈 배열을 반환하게 됩니다.

4) Symbol.iterator

  • JavaScript 엔진은 Symbol.iterator를 프로퍼티 키로 갖는 메서드가 있으면 iterable 객체로 인식합니다.
Array.prototype[Symbol.iterator];

String.prototype[Symbol.iterator];

Map.prototype[Symbol.iterator];

Set.prototype[Symbol.iterator];

arguments[Symbol.iterator];

NodeList.prototype[Symbol.iterator];

HTMLCollection.prototype[Symbol.iterator];
  • iterable 객체로 인식되는 객체들만 for ...of 문법 등을 이용한 반복이 가능합니다.
  • iterable 객체에 대한 자세한 설명은 아래에서 하겠습니다.

Reference Type

  • ES6에서 참조타입인 Map, Set, WeakMap, WeakSet 데이터 유형이 추가되었습니다.

Map

  • Map 객체는 키와 값으로 이루어진 컬렉션으로 Object와 상당히 유사하나 Object는 key값으로 string(숫자로 써도 내부적으로 문자열로 변환)과 symbol만 가능한데 비해 map은 객체를 포함한 어떤 값도 가질 수 있습니다.
  • Object는 not iterable 객체지만 Map은 iterable 객체이고 Map은 Object와 달리 값을 넣은 순서를 기억해 그 순서를 보장합니다.
  • 단점으로 Map은 JSON을 지원하지 않습니다.
const myMap = new Map();  // {}
myMap.set(1, 'hello');  // {1 => 'hello'}
myMap.set('1', 'hello');  // {1 => 'hello', '1' => 'hello'}
myMap.set(1, 'world');  // {1 => 'world', '1' => 'hello'} // 중복시 덮어씀
myMap.size;  // 2
myMap.entries();  // {1 => 'world', '1' => 'hello'}
myMap.keys()  // {1, '1'}
myMap.values()  // {'world', 'hello'}
myMap.get(1);  // 'world'
myMap.has(1);  // true
myMap.delete(1);  // {'1' => 'hello'} // 특정값 지우기 불리언 반환
myMap.clear();  // 일괄삭제

const myMap = new Map([["name", "heejin"], ["age", 26]]);  // {'name' => 'heejin', 'age' => 26}

Set

  • Set은 Array를 변형한 것으로 중복을 허용하지 않는 데이터 집합입니다. Set을 사용하면 데이터에 빠르게 엑세스 할 수 있습니다.
  • 1과 '1'은 다른것으로 간주하며 중복을 확인하기 위해 강제적으로 자료형을 변형하지 않습니다.
const mySet = new Set(); // {}
mySet.add(1);  // {1}
mySet.add('1');  // {1, '1'}
mySet.add(1);  // {1, '1'}
mySet.size;  // 2
mySet.has(1);  // true
mySet.entries();  // {1 => 1, '1' => '1'}
mySet.keys();  // {1, '1'}
mySet.values();  // {1, '1'}
mySet.delete(1);  //  {'1'} // 특정값 지우기 불리언 반환
mySet.clear();  // 일괄삭제

const mySet = new Set([1, 2, 3, '1']);  // {1, 2, 3, '1'}

WeakMap & WeakSet

  • WeakMap은 객체만을 key값으로 받고 WeakSet은 객체만을 값으로 받습니다. 굳이 WeakMap, WeakSet을 사용하는 이유는 기존 객체를 약한 참조해서 가비지 컬렉션을 방해하지 않기 때문에 메모리 누수로부터 자유롭기 때문입니다.
  • 대신에 entries, keys, values 메소드를 사용할 수 없습니다.

2. template literal

  • 템플릿 리터널은 표현식/문자열 삽입, 여러 줄 문자열, 문자열 형식화, 문자열 태깅 등 다양한 기능을 제공합니다.
  • 작은 따옴표나 큰 따옴표 대신 백틱(`)을 이용해서 문자열로 나타낼 수 있고 달러기호$ 와 중괄호{}를 사용하여 플레이스 홀더를 만들어 변수나 연산 등을 표기할 수 있습니다.
    템플릿 리터널 안에서 백틱 문자를 사용하려면 백틱 앞에 백슬러시를 삽입하면 되고 코드상의 줄바꿈 처리가 그대로 수행되기 때문에 개행문자(\n)를 사용하지 않아도 됩니다.
const name = "김희진";

console.log(`안녕하세요 저는 "${name}"입니다`);  // template literal 사용
console.log('안녕하세요 저는 "' + name + '"입니다')  // template literal 사용X

// Tagged templates
const person = 'heejin';
const age = 26;

function myTag(strings, personExp, ageExp) {  // personExp = heejin, ageExp = 26
  const str0 = strings[0]; // "that "
  const str1 = strings[1]; // " is a "

  let ageStr;
  if (ageExp > 99){
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  // 심지어 이 함수내에서도 template literal을 반환할 수 있습니다.
  return str0 + personExp + str1 + ageStr;
}

var output = myTag`that ${ person } is a ${ age }`;

console.log(output);  // that heejin is a youngster

3. Arrow Function

  • Arrow 함수는 function 키워드 대신 화살표 => 문법을 사용하는 축약형 함수입니다.
  • 자바스크립트의 경우 함수 호출 방식에 의해 this에 바인딩할 객체가 동적으로 결정됐지만 ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐기 때문에 함수 내부에는 this가 아예 없으며 접근하고자 하면 스코프 체인상 가장 가까운(상위 스코프) this에 접근하게 됩니다. 이를 Lexical this라 합니다.
  • 또한, this가 바인딩되지 않았기 때문에 call, apply, bind 메서드를 사용해서 this를 변경할 수 없고 arguments 객체도 바인딩 되지 않습니다.
  • 화살표 함수는 다른 함수와 다르게 prototype 프로퍼티를 가지고 있지 않기 때문에 생성자 함수로 사용할 수 없고 yield 키워드도 사용할 수 없습니다.
  • 매개변수가 하나라면 소괄호 생략이 가능하고 유일한 문장이 return 한줄 뿐이라면 중괄호와 return도 생략가능합니다.(중괄호가 없으면 암묵적으로 return 되기 때문)
const addTwo = (x) => { return x + 2 }const addTwo = x => x + 2

4. ES-Module(ESM)

  • 개발을 하다보면 점점 파일의 크기가 커지면서 파일을 여러 개로 분리해야 하는 시점이 오는데 이때 분리된 각각의 파일을 모듈(Module)이라고 부릅니다.
  • 초기 자바스크립트는 큰 스크립트가 필요하지 않았지만 jQuery가 생겨나고 어플리케이션의 규모가 커지면서 script 파일을 나누기 시작했고 파일간의 변수, 함수 등을 전달하기 위해 ESM 이전에는 script 파일을 전역 스코프처럼 사용(HTML 파일에서 위에있는 script 파일은 전역 스코프처럼 하위의 script 태그에서 접근, 변경이 가능)했습니다.
  • 하지만 이 구조는 파일 순서가 뒤틀리면 에러가 발생하고, 하위 script가 상위 script의 값을 쉽게 변경시키는 '전역오염'이 발생하기 쉬우며 해당 script가 어떤 script에 의존성을 갖고 있는지 파악하기 힘들다는 문제점이 있었습니다.
  • 이러한 문제속에서 모듈화에 대한 필요성이 높아져 ES6에서 ES Module이 등장하게 되었고 import, export를 사용해 분리된 자바스크립트 파일끼리 서로 접근할 수 있게되었습니다.

ES 모듈의 동작 방식

  • 브라우저에서 ES Module은 구성, 인스턴스화, 평가의 단계를 거쳐 동작합니다.

1) 구성

  • ES 모듈 명세는 모듈 레코드에 파일을 구문분석하는 방법과 인스턴스화 하는 방법, 그리고 그 모듈을 평가하는 방법을 알려주지만 파일을 불러오는 것은 로더(loader)가 하며 이는 ES 모듈 명세가 아닌 HTML 명세를 따릅니다.
  • 로더가 파일을 찾아 다운로드 하기 위해 script 태그에 type="module" 속성을 적어 entry 파일을 지정해주면 로더는 entry 파일부터 import문을 찾아가며 파일을 다운로드합니다.
  • 파일 불러오기가 끝나면 브라우저의 자바스크립트는 파일 자체를 사용할 수 없기 때문에 파일을 모듈 레코드로 구문분석하는데 모듈 레코드는 한번 만들어지면 모듈맵에 추가되고 그 다음부터는 필요할 때 마다 로더가 모듈맵에서 가져올 수 있습니다.
  • 이 때문에 동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 단 한번만 실행됩니다.
  • 모든 모듈은 코드 상단에 'use strict' 키워드를가 없어도 있는 것처럼 구문분석되고 모듈 스크립트는 외부 스크립트, 인라인 스크립트와 관계없이 마치 defer 속성을 붙인 것처럼 항상 지연 실행되기 때문에 script를 다운로드 할 때 브라우저의 HTML 처리가 멈추지 않습니다.

2) 인스턴스화

  • export 된 값을 모두 배치하기 위해 메모리에 있는 공간들을 찾은다음 export와 import들이 이런 메모리 공간들을 가리키도록(아직 실제 값은 채우지 않음) 합니다. 이를 연결(linking) 이라고 합니다.

3) 평가

  • 코드를 실행하여 상자의 값을 변수의 실제 값으로 채웁니다.

Node

  • nodejs는 brower보다 빨리 CommonJS, AMD, Webpack, babel 등과 같은 모듈 기반 시스템이 있었고 이 중 CommonJS(require)가 가장 널리 사용되었습니다.
  • Node에서는 HTML 태그를 사용하지 않으므로 type 속성을 사용할 수 없기에 Node에서 ESM을 사용하기 위한 방법으로 파일의 확장자를 .js 대신 .mjs를 사용하면 파일 단위로 ES 모듈을 적용할 수 있습니다.
  • 이 밖에 package.json 파일의 type을 module 로 설정해 프로젝트 단위로도 ES 모듈을 적용할 수 있습니다.

CommonJS vs ES Module

1) CommonJS

  • 파일 시스템에서 파일을 로드하고 파일을 불러오는 동안 주 스레드를 차단합니다.
  • 파일 로드 - 구문 분석 - 인스턴스화 - 평가가 각 파일마다 바로 실행되기 때문에 모듈 지정자에 변수를 넣을 수 있습니다.
  • export 객체에 값을 복사해서 넣습니다.

2) ES Module

  • entry 파일의 구문 분석 후 의존성(import)을 확인해서 해당 의존성 파일을 찾아서 다시 구문 분석을 반복합니다.
  • 파일을 불러오는 동안 주 스레드를 차단하지 않고, 더이상 구문 분석할 의존성이 발견되지 않으면 인스턴스화, 평가를 실행합니다.
  • 그렇기에 모듈 지정자에 변수를 넣을 순 없지만 동적 import를 쓰면 가능합니다.
  • 동적 import는 별개의 entry 파일로 취급되어 새로운 그래프를 만듭니다.
  • export는 참조를 반환하는 함수를 정의합니다.

5. Destructuring

  • Destructuring은 배열과 객체에 패턴 매칭을 통한 데이터 바인딩을 제공하고 할당 실패에 유연하여 실패 시 undefined 값이 자동할당 됩니다. 또한 객체의 속성 값도 자동으로 검색하여 바인딩해줍니다.
const person = {
  name : {
  first : 'heejin',
  last: 'kim'
  },
  age : 26,
  address : '경기'
};

const { name, age2 } = person;

console.log(name.first);  // heejin
console.log(age2);  // undefined

const [a, b, ...rest] = [1, 2, 3, 4, 5];

console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]

6. Rest/Spread Operator

  • Rest Operator와 Spread Operator는 둘다 점 세개(...)를 사용해서 비슷해 보이지만 Rest Operator는 여러 엘리먼트를 수집하여 이를 하나의 엘리먼트로 '압축'하는 반면, Spread Operator는 그 엘리먼트로 '확장'합니다.

Rest Operator

  • 객체, 배열, 함수의 파라미터에서 사용이 가능합니다.
  • 위에서 제가 Destructuring 예시를 들면서 rest operator를 사용한 것을 볼 수 있는데 rest는 객체와 배열에서 사용할때는 이렇게 비구조화 할당 문법과 함께 사용됩니다.
  • rest는 나머지 값을 하나의 엘리먼트로 압축하는 것이기 때문에 마지막에 사용하여야 하며 그렇기 때문에 한번에 한개밖에 사용할 수 없습니다.
  • 함수의 매개변수에 사용하면 모든 후속 매개변수를 배열에 넣도록 지정합니다.
const person = {
  firstName : 'heejin',
  lastName: 'kim',
  age : 26,
  address : '경기'
};

// const { ...rest, ...info } = person // Rest element must be last element
const { lastName, ...info } = person;

console.log(info);  // {firstName: 'heejin', age: 26, address: '경기'}

Spread Operator

  • Spread Operator는 반복 가능한(iterable) 객체에 적용할 수 있는 문법으로 배열이나 문자열 등을 풀어서 요소 하나 하나로 전개 시킬 수 있습니다.
  • 배열에 추가로 요소를 넣는다던지, 배열 복사, 배열 이어 붙이기 등 다양한 작업에서 사용할 수 있습니다.
const arr = [3, 4, 5];
const newArr = [1, 2, ...arr, 6, 7];

console.log(newArr);  // [1, 2, 3, 4, 5, 6, 7]
  • Object 자체는 iterable 객체가 아니며, 아래와 같이 사용하면 에러가 발생합니다.
const obj = { name: 'heejin', age: '26' };

console.log(...obj); // TypeError: obj is not iterable
  • 하지만 ES2018(ES9) 에서는 객체의 프로퍼티를 전개할 수 있도록 지원하고 있으며 아래 예시와 같이 객체 내부에서 사용할 수 있습니다.
const person = {
  name : {
  first : 'heejin',
      last: 'kim'
  },
  age : 26,
    address : '경기'
}

const newPerson = { ...person }

console.log(newPerson === person);  // false 참조값이 다름

7. Class

  • ES6에서 클래스는 프로토타입 기반 객체지향 패턴을 더 쉽게 사용할 수 있는 대체재입니다.
  • Class는 사실 함수이기 때문에 함수와 동일하게 클래스 표현, 클래스 선언 두 가지 방법을 제공합니다. 다만, 함수의 경우 함수 선언일 경우 호이스팅이 되지만 클래스 선언의 경우 호이스팅되지 않습니다.
var ES5 = function(name) {
  this.name = name;
};

ES5.staticMethod = function () {
  return this.name + ' staticMethod';
};

ES5.prototype.method = function () {
  return this.name + ' method';
};

var es5Instance = new ES5('es5');
console.log(ES5.staticMethod());
// es5 staticMethod
console.log(es5Instance.methid());
// es5 method
const ES6 = class {
  constructor(name) {
    this.name = name
  }
  static staticMethod() {
    return this.name + ' staticMethod';
  }
  method() {
    return this.name + ' method';
  }
};

const es6Instance = new ES6('es6');
console.log(ES6.staticMethod());
// es6 staticMethod
console.log(es6Instance.method());
// es6 method

constructor

  • constructor 메서드는 class로 생성된 객체를 생성하고 초기화하기 위한 특수한 메서드로 constructor는 클래스 안에 한 개만 존재할 수 있습니다.

static

  • static 키워드는 클래스를 위한 정적 메서드 또는 정적 속성을 정의합니다. class 자체에 연결되어있기 때문에 class 이름을 통해 호출하며 클래스의 인스턴스에서는 호출할 수 없습니다.
  • 주로 들어오는 데이터에 상관없이 공통적으로 class에서 쓸때 사용하는 것 같습니다.

Fields

1) public

  • field를 그냥 정의한 것으로 외부에서 접근이 가능합니다.

2) private

  • field 앞에 해시태그(#)를 붙여 정의한 것으로 class 내부에서만 읽고 쓸 수 있습니다.
class Experiment {
  publicField = 2;
  #privateField = 0;
}

const experiment = new Experiment();
console.log(experiment.publicField);  // 2
console.log(experiment.privateField);  // undefined

extends

  • extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 클래스의 자식 클래스를 생성하기 위해 사용됩니다.
class Shape {
  constructor(width, height, color) {
    this.width = width;
    this.height = height; 
  }

  getArea() {
    console.log(this.width * this.height);
  }
}

class Triangle extends Shape {  // shape을 연장
  constructor(width, height, color) {
    super(width, height); // this 키워드 전에 super 먼저 호출
    this.color = color;
  }

  getArea() {  // overriding
    super.getArea(); // 부모 함수 호출
    console.log(this.color);
    console.log((this.width * this.height) / 2);
  }
}

const triangle = new Triangle(20, 20, 'red');
triangle.getArea();  // 400 // red //200

super

  • super 키워드는 부모클래스를 호출합니다.
super([arguments]); // 부모 생성자 호출
super.functionOnParent([arguments]); // 부모 메서드 호출
  • 자식 컴포넌트에서 새로운 속성을 추가하거나 부모속성을 덮어 씌우기 위해 반드시 this 키워드 전에 super() 메서드가 먼저 호출되어야 합니다.

8. Proxy

  • 프록시(Proxy)를 사용하면 호스트 객체에 다양한 기능(속성 접근, 속성 할당 등)을 추가하여 객체를 생성할 수 있습니다.
  • Proxy를 사용하게 되면 기존 객체를 래핑하고 객체의 속성이나 메서드에 접근하는 어떤 것이든 인터셉트 할 수 있게 되는데 심지어 사전에 정의된 속성이 아니여도 가능합니다.
  • new 키워드를 통해 생성(new Proxy(target, handler))할 수 있고 첫번째 인자로 래핑할 객체(기본 객체 or proxy)를 넣어주고 두번째 인자로 객체의 기본적인 동작(접근, 할당, 순회 등등)의 작업이 수행될 때 proxy의 동작을 정의하는 함수 객체를 넣어줍니다.
  • 여기서 지정 가능한 함수 객체는 Proxy MDN을 참고하시면 됩니다.
// get은 속성을 조회할때 호출되는 함수입니다.
const person = new Proxy({}, {
  get: function (target, prop) {  // target : 해당 객체, prop : 조회시 해당 속성
    return target[prop]+' 호출 가로채기!!';
  },
});

person.name = 'heejin';
console.log(person.name); // heejin 호출 가로채기!!
// set은 속성에 값을 할당할때 호출되는 함수입니다.
const person = new Proxy({}, {
  set: function (target, prop, value) {
    target[prop] = value +'-set!!';
    console.log(value +'-set!!');
    return true;
  }
});

person.name = 'heejin'; // heejin-set!!

9. Promise(Macro/Micro task)

  • Promise는 비동기 프로그래밍을 위한 객체로 콜백 지옥에서 벗어나게 해준 고마운 친구입니다.
  • new 키워드를 통해 생성할 수 있고 Promise는 말 그대로 '약속'으로 최종결과를 반환하는 것이 아니라 미래의 어떤 시점에 결과를 제공하겠다는 약속을 반환합니다.
  • 따라서 Promise는 다음 중 하나의 상태를 가집니다.
  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.
  • Promise를 사용하면 비동기 작업들을 순차적으로 진행하거나, 병렬로 진행하는 등의 컨트롤이 보다 수월해지고 코드의 가독성이 좋아집니다. 또, 내부적으로 예외처리에 대한 구조가 탄탄하기 때문에 오류가 발생했을 때 오류 처리에 대해 보다 가시적으로 관리해줄 수 있는 장점이 있습니다.
  • Promise를 반환하는 객체는 프라미스 핸들러 - .then/catch/finally 를 사용할 수 있는데 이 핸들러들은 항상 비동기적으로 실행되고 microtask에 담기게 됩니다.
  • .then : 값이 정상적으로 수행되서 마지막에 최종적으로 resolve라는 콜백함수를 통해서 전달한 된 값이 파라미터로 전달(또 다른 비동기인 promise를 전달해도 된다.)
  • .catch : 에러가 발생했을 때 처리 할 값
  • .finally : 성공하든 실패하든 상관없이 무조건 마지막에 호출
const promise = new Promise((resolve, reject) => { // resolve: 성공, reject: 실패
  // doing some heavy work
  console.log('doing something...');
  setTimeout(() => {
    reject(new Error('no network'));
  }, 2000);
});

promise
  .then(value => console.log(value))
  .catch(error => console.log(error))
  .finally(() => console.log('finally'))

Macro & Micro task

  • 자바스크립트 엔진 이벤트 루프는 우선적으로 마이크로 태스크들을 실행하고 마이크로 태스크들은 실행하면서 새로운 마이크로 태스크를 큐에 추가할 수 있습니다.
  • 이벤트 루프는 새롭게 추가된 마이크로 태스크 전부를 처리하면 매크로 태스크 큐를 실행시키는데 매크로 태스크 큐는 마이크로 태스크 큐와 달리 연속적으로 쌓인 태스크들을 처리하는 것이 아니라 하나를 처리한 후 마이크로 태스크에 쌓인 작업이 있는지 확인합니다.

1) Macro task

  • 외부 스크립트를 불러올 때, 유저가 마우스를 움직일 때, 예약된 setTimeout 시간이 만료될 때 등 작업들이 생기면 자바스크립트 엔진은 순차적으로 작업을 처리하는데 만약 엔진이 바쁠 때 새로운 작업이 들어온다면 그 작업은 대기열에 들어갑니다.
  • 이렇게 엔진이 바쁠 때 미뤄진 직업들로 형성된 대기열을 Macrotask queue라고 합니다.

2) Micro task

  • 위에 설명한 Macrotask 말고도 Microtask라는 것도 존재하는데 Micro task는 코드를 사용해서만 만들 수 있고 주로 프라미스를 사용해 만듭니다. 위에서 설명한 .then/catch/finally 핸들러와 프라미스를 핸들링하는 또 다른 문법인 await을 사용해 만들 수 있습니다.
  • 이 외에도 표준 API인 queueMicrotask(func)를 사용하면 함수 func을 마이크로태스크 큐에 넣어 처리할 수 있습니다.

10. Iterable object(반복 가능 객체)

  • iterable 객체란 반복 가능한 객체(for ... of 등의 문법을 이용하여 각 요소를 반복할 수 있는 객체)를 의미하며, iterator 객체란 해당 iterable 객체에서 각 요소를 반복하기 위해 사용하는 객체를 의미합니다.
  • iterable 객체가 Symbol.iterator 메서드를 호출해서 얻은 iterator 객체가 자기 자신인 경우도 있는데 대표적인 예시로 Object.entries() 메소드가 있고 제네레이터(Generator) 객체도 iterable 객체이면서 동시에 iterator 객체입니다.

Iterable

  • iterable 객체가 되기 위해선 이름이 Symbol.iterator인 메서드를 정의하고 iterator 객체를 반환해야 하는데 여기서 말하는 iterator 객체란 Symbol.iterator 메소드가 반환하는 객체가 특정 조건을 만족하면 그 객체는 iterator 객체가 됩니다.

Iterator

  • iterator 객체가 되기 위한 조건으로 next() 메소드를 정의해야하고 value 프로퍼티와 done 프로퍼티를 가지는 객체({value : someValue, done : Boolean})를 반환해야 합니다.
  • done : 반복이 끝났다면 true, 아니라면 false
  • value : 반복이 끝났다면 undefined, 아니라면 현재 위치의 요소 값

Iterable 객체를 이용하는 JavaScript 문법

  • 내부적으로 iterable 객체와 iterator 객체를 이용하여 구현되어 있는 문법들이 몇가지 있습니다. 가장 대표적인 예시는 for ... of 문법이 있고 앞에서 설명한 전개 연산자(Spread Operator), 구조 분해 할당(Destructing Assignment), 제네레이터(Generator)의 yield* 등이 있습니다.

1) for ...of

  • ES5 일때 자바스크립트에서 object를 순회하는 방법은 for ...in 문법을 사용하는 것 뿐이었습니다.
  • 하지만 for ...in 문법을 사용해서 배열을 순회하면 index가 문자열이라는 점, prototype chain까지 순회한다는 점 등 배열을 순회하기엔 문제점이 많아서 ES6에선 for ...of 문법이 추가되었습니다.
  • for ...of 문법은 열거가능(enumerable)한 객체라면 모두 순회가 가능합니다.

2) Generator

  • Generator는 함수 실행을 잠시 멈췄다가 다시 실행할 수 있는 독특한 함수로 function* 키워드를 사용하여 생성하고 제너레이터 함수를 호출하면 코드가 실행되는 것이 아니라 Iterator 객체가 반환되게 됩니다.
  • 그리고 반환된 Iterator 객체의 next() 메서드를 호출하면 Generator가 실행되면서 yield를 만날 때까지 실행되고, 이 때 Generator 함수는 나중에 다시 접근하기 위해서 컨텍스트를 저장된 상태로 남겨둡니다.
function* gen(){
  yield* ["a", "b", "c"];  // generator composition
}

const a = gen();

a.next();  // {value: 'a', done: false}
a.next();  // {value: 'b', done: false}
a.next();  // {value: 'c', done: false}
a.next();  // {value: undefined, done: true}
  • next()를 호출할 때 인수를 넘기면 제너레이터에게 값을 전달 할 수도 있습니다

11. Async, Await

  • async 키워드를 함수 앞에 붙이면 번거롭게 Promise를 쓰지 않아도 자동적으로 함수안에 있는 코드 블럭들이 Promise로 변환됩니다.
  • await은 반드시 async 함수 내부에서 사용해야 하며 비동기로 처리되는 부분 앞에 await 키워드를 붙여주게 되면 프로미스가 처리될때 까지 기다립니다.

Promise와 Generator를 활용한 Async

  • Promise 자체로도 비동기 프로그래밍을 다루는데 문제 없지만 여전히 사람의 동기식 사고방식과는 다르기 때문에 generator를 함께 사용하여 비동기적 구현을 마치 동기적으로 작동하는 것처럼 구현할 수 있습니다.
axios.get('http://some-url/resources')
  .then(res => {
    renderData(res.data);
  });function* main() {
  const res = yield axios.get('http://some-url/resources');
  renderData(res.data);
}

const iter = main();
const request = iter.next().value; /* axios.get(...)을 받는다. */
request.then(res => {
  iter.next(res); /* iter에 res를 넘겨주면서 iter을 재실행시킨다. */
});
  • 하지만 위 코드의 문제점이 하나 있는데 바로 iter가 멈출 때마다 iter가 yield한 Promise에 .then()을 붙여 iter.next(res)를 호출해줘야 한다는 점입니다. 이 작업을 손으로 일일이 해줘야 해서 코드가 지저분하게 느껴질 수 있습니다.
  • 이를 해결하는 방법은 Promise에 의해 제어되는 형태의 generator를 실행시켜주는 helper function을 구현하는것입니다.
function run(generator, ...args) { // run: helper function
  const iter = generator(args);
  function resumeIter(prevRes) {
    const next = iter.next(prevRes);
    if (next.done) return Promise.resolve(next.value);
    Promise.resolve(next.value)
      .then(res => {
        resumeIter(res);
      });
  }

  resumeIter();
}

function* main() {
  const res = yield axios.get('http://some-url/resources');
  renderData(res.data);
}

run(main);
  • 위 코드를 보면 일일이 .then, next() 해주었던 작업을 resumeIter() 함수를 통해 함수가 끝날때까지 재귀적으로 호출하게 되고 이로써 완벽하게 동기적으로 추상화 시킬 수 있습니다.(위의 코드는 코드를 단순하게 만들기 위해 예외처리와 같은 많은 작업들이 생략되었지만 이는 해당 기능들을 추가하면 됨)
  • 위 방법을 간단히 구현하기 위해 라이브러리를 사용할 수도 있지만 우리에겐 async, await이 있습니다.
  • 아래 코드를 살펴보면 Generator 코드와 Async 코드의 형태가 동일하다는 것을 볼 수 있고 실제로 Bebel을 통해 async, await 을 ES5문법으로 변경해보면 async 키워드를 generator로 바꾸고 await 키워드를 yield로 바꾸는걸 볼 수 있습니다.
function* main() {
  const res = yield axios.get('http://some-url/resources');
  renderData(res.data);
}

async function main() {
  const res = await axios.get('http://some-url/resources');
  renderData(res.data);
}
// ES7
async function foo() {
  await bar();
}

// ES5 complied
let foo = (() => {
  var _ref = _asyncToGenerator(function*() {
    yield bar();
  });

  return function foo() {
    return _ref.apply(this, arguments);
  };
})();

function _asyncToGenerator(fn) {
  // generator 함수를 받아온다
  return function() {
    var gen = fn.apply(this, arguments);
    // .apply로 generator 실행
    // iterator 객체를 gen 안에 넣어서 클로저로 저장해둔다

    return new Promise(function(resolve, reject) {
      function step(key, arg) {
        try {
          var info = gen[key](arg); // next() 호출, iterator 객체 소환
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }
        if (info.done) {  // true 면 종료
          resolve(value);
        } else {
          return Promise.resolve(value).then(
            function(value) {
              step("next", value);  // 재귀함수를 통해 반복실행
            },
            function(err) {
              step("throw", err);
            }
          );
        }
      }
      return step("next");
    });
  };
}
  • 이로써 async await 문법은 Promise의 then, catch의 가독성 문제를 해결하기 위해 등장했고 비동기적 구조를 동기적으로 작성할 수 있게 도와주며 제네레이터 기반으로 만들어 진걸 알 수 있습니다.
profile
코린이

0개의 댓글