[JS] JS의 심화문법 정리

Updated:

JavaScript 심화문법 정리

1. 변수

호이스팅


-> scope 내부 어디서든 변수의 선언은 최상위에서 선언된 것처럼 작동하는 방식을 의미한다
-> 호이스팅은 var, const, let 모두에게 적용되는 방식이다

console.log(name);      // undefined

var name = "saemyung";
console.log(name);      // 오류

const name = "saemyung";        // let으로 지정해도 마찬가지

-> 위의 2가지에서 볼 수 있듯이, var와 달리 const와 let의 경우 Temporal Dead Zone 즉, 변수할당 전에는 사용할 수 없다

cf>Temporal Dead Zone
-> 위의 코드처럼 변수선언 및 초기화 이전의 상태
-> var는 const, let과 달리 Temporal Dead Zone의 영향을 받지 않아 변수할당 전에도 사용할 수 있다
-> 이는 오류발생의 원인이기 때문에 var사용을 피한다

Scope


-> Scope는 Lexical Scope와 Dynamic Scope 2가지로 나눠진다
-> Lexical Scope는 선언된 위치가 상위 Scope를 정하고, Dynamic Scope는 실행한 위치가 상위 Scope를 정한다
-> JS는 Lexical Scope의 방식으로 작동한다

var num = 10;

function func1(){
    var num = 100;
    func2();
}

function func2(){
    console.log(num);
}

func1();		// 10

-> 위처럼 func1의 내부에서 func2를 실행해도 선언된 위치상 func2의 상위 Scope는 전역이므로, 전역변수 num의 값인 10이 출력됨

+α> var, const, let의 Scope 비교
-> block-scoped와 function-scoped 2가지로 나눠진다
-> block-scoped는 function, if문, for문, switch문 등 각 block안에서 선언된 변수는 해당 block내부에서 밖에 사용할 수 없는 방식으로 const, let이 이에 포함된다
-> function-scoped는 blocked-scoped와 달리 function을 제외한 if문, for문, switch문 등의 block안에서 선언된 변수들은 외부에서도 사용가능한 방식으로 var가 이에 해당한다
-> 사실상, var는 오류발생의 여지가 있어 const, let을 주로 사용한다

변수지정어에 따른 변수의 생성과정


  1. var
    -> ‘선언 및 초기화’ 단계와 ‘할당’ 단계 2가지로 구분(초기화 단계는 undefined를 할당하는 단계)
    -> 그렇기 때문에 값을 할당하기 전에 호출해도 undefined라는 값이 출력된다

  2. let
    -> ‘선언’ 단계, ‘초기화’ 단계, ‘할당’ 단계 3가지로 구분
    -> 호이스팅되며 선언 단계가 일어나지만, 초기화 단계는 해당 코드에 도달했을때 발생하기 때문에 Temporal Dead Zone에서 해당 변수 접근 시 reference오류 발생

  3. const
    -> ‘선언, 초기화 및 할당’ 단계가 한꺼번에 발생
    -> var, let과 달리 선언만 하고 할당을 나중에 하는 것이 불가능

2. JS 내장 메서드

map


-> 배열.map((요소, 인덱스, 배열) => { return 요소; }); 의 형태로 사용한다
-> map의 결과로 나오는 배열은 기존의 배열을 수정하지 않고 새로 만들어진 배열이다

let userList = [
    {name: 'mike', age:30},
    {name: 'tom', age:10},
    {name: 'jane', age:27}
];

let userValidList = userList.map((elem, idx) => {
    return Object.assign({}, elem, {id: idx+1 ,ageValid: elem.age>20});
});

console.log(userList === userValidList);        // false

console.log(userValidList);
//  { name: 'mike', age: 30, id: 1, ageValid: true },
    { name: 'tom', age: 10, id: 2, ageValid: false },
    { name: 'jane', age: 27, id: 3, ageValid: true }

-> Object.assign() 메서드를 이용하여 기존 객체에 다른 property값 추가
-> map() 메서드를 통해 새로만든 userValidList는 userList와는 다른 배열임

reduce


-> 배열.reduce((누적값, 현재값, 인덱스, 요소) => { return 결과; }, 초기값); 의 형태로 사용한다. 초기값을 적지 않을 경우 해당배열의 0번째 인덱스 요소값이 자동으로 초기값으로 설정된다

const arr = [1,3,5];

const result = arr.reduce((prev, curr) => {
    return prev+curr;
});

console.log(result);        // 9

-> 위는 덧셈의 결과를 저장하는 함수의 예시이다

let userList = [
    {name: 'mike', age:30},
    {name: 'tom', age:10},
    {name: 'jane', age:27}
];

let userValidList = userList.reduce((prev, curr, idx, arr) => {
    prev.push(Object.assign({}, curr, {id: idx+1, ageValid: curr.age>20}));
    return prev;
}, []);

console.log(userList === userValidList);        // false

console.log(userValidList);

-> reduce() 메서드를 이용하여 위의 map() 메서드와 같은 결과를 도출함
-> 이처럼 reduce() 메서드를 이용하면 sort, filter, every, some, find, findIndex, includes 메서드를 모두 구현가능함

3. 객체

객체 생성 방법


-> 객체를 만들기 위해서는 하위 3가지 방법이 존재한다

  1. {} 이용

    const lim = {
      name: 'saemyung',
      age: 23,
    };
       
    console.log(lim);		// { name: 'saemyung', age: 23 }
    
  2. Class 이용

    class person {
      name;
      age;
      constructor(name, age){
        this.name = name;
        this.age = age;
      }
    }
       
    const lim = new person('seamyung', 23);
    console.log(lim);		// person { name: 'seamyung', age: 23 }
    

    -> constructor는 생성자로 Class(틀)을 이용하여 객체를 만들 수 있도록 도와줌
    -> 객체를 생성하기 위해 반드시 new 사용

  3. 생성자 함수 이용

    function Person(name, age){
      this.name = name;
      this.age = age;
    }
       
    const lim = new Person('saemyung', 23);
    console.log(lim);		// Person { name: 'saemyung', age: 23 }
    

    -> 위의 Person을 생성자함수라고 하며, 첫글자를 대문자로 적어주는 것이 관례
    -> 생성자 함수를 사용하기 위해 반드시 new 사용
    주의) 화살표 함수에서는 생성자 함수와 new를 이용한 객체생성이 불가능하다!!!

Computed property


function getObj(key, val){
    return {
        [key]: val
    }
};

console.log(test('name', 'lim'));      // { name: 'saemyung' } 출력

-> 변수에 저장된 값을 객체의 property로 이용할 수 있는 방식
-> 위의 코드를 통해 name property값이 lim인 객체를 반환받음

객체 메서드


  1. Object.assign(): 객체 복사
const lim = {
    name: 'saemyung',
    age: 23,
};

const lim2 = {
    weight: 70,
};

const lim3 = Object.assign({height: 180}, lim, lim2);

-> lim3은 lim, lim2의 property에 height를 추가한 객체를 property로 가지는 객체로 생성됨

  1. Object.keys(): 객체 property의 key값들을 배열로 반환
  2. Object.values(): 객체 property의 value값들을 배열로 반환
  3. Object.entries(): 객체 property의 key,value값들을 묶어 배열로 반환

Symbol


-> 객체의 property key값을 고유하게 설정함으로써 key값의 유일성을 보장해준다
-> Symbol()을 통해 생성하고, 이때 인자로 값을 넘길 수 있는데, 이는 디버깅 시에 어떤 값인지 확인하기 용이하도록 돕는다
-> Symbol()을 통해 생성된 값은 항상 다르므로 유일성이 보장된다
-> Symbol.for()을 이용하여 생성한 전역Symbol은 하나의 Symbol만을 생성하여 공유한다

const id = Symbol('id');

const user = {
    name: 'lim',
    age: 23,
    [id]: 'myId',
};

console.log(user[id]);     // myId 출력

-> 위의 코드는 객체의 key로써 Symbol을 사용한 예시이다
-> Object.keys(), entries() 등의 객체 메서드를 사용해도 Symbol은 드러나지 않는다
-> Object.getOwnPropertySymbols(객체명)을 이용하여 Symbol을 확인할 수 있다

const user = {
    name: 'lim',
    age: 23,
};

const showName = Symbol('show name');
user[showName] = function(){
    console.log(this.name);
}

user[showName]();

for (let key in user){
    console.log(`His ${key} is ${user[key]}`);
}

-> 위처럼 겉으로는 드러나지 않으면서 객체의 key값으로 존재할 수 있다

Property Attribute


-> 객체의 각 property에는 value말고도 writable, enumerable, configurable이라는 attribute를 가진다

  1. value: 실제 property의 값

  2. writable: 해당 property값을 수정할 수 있는지의 여부

  3. enumerable: 열거가 가능한지의 여부(반복문, consol.log등을 통해 해당 property가 나열되는지)

  4. configurable
    -> 상위 3가지 property attribute값들을 변경할 수 있는지의 여부
    -> 단, writable을 true에서 false로 변경하는 경우와 writable이 true일 경우 value를 변경하는 것은 가능하다

const lim = {
    name: 'saemyung',
    age: 23,
};

console.log(Object.getOwnPropertyDescriptors(lim));

Object.defineProperty(lim, 'name', {
    value: 'lim',
    writable: false,
    enumerable: false,
    configurable: false
});

console.log(Object.getOwnPropertyDescriptor(lim, 'name'));
lim.name = "saemyung";		// 수정불가(오류는 발생하지 않음)
console.log(lim);		// { age: 23 }

-> Object.getOwnPropertyDescriptor(인스턴스명, 프로퍼티명)을 통해 하나의 property attribute값을 확인할 수 있고, Object.getOwnPropertyDescriptors(인스턴스명)을 통해 모든 property attribute값을 확인가능하다
-> Object.defineProperty(인스턴스명, 프로퍼티명, 수정사항객체)를 통해 property attribute를 설정
-> 위의 예시에서는 name property의 writable이 false이기 때문에 value값이 수정불가하고, configurable이 false이기 때문에 마지막줄 console.log에서도 name property가 출력되지 않는다

불변객체


  1. extensible

    const lim = {
        name: 'saemyung',
        age: 23,
    };
       
    console.log(Object.isExtensible(lim));		// true
    Object.preventExtensions(lim);
    console.log(Object.isExtensible(lim));		// false
       
    lim.height = 170;
    console.log(lim);		{ name: 'saemyung', age: 23 }
       
    delete lim.age;
    console.log(lim);		{ name: 'saemyung' }
    

    -> Object.isExtensible(인스턴스명)은 default값이 true(기본적으로 property 추가가 가능)
    -> Object.preventExtensions(인스턴스명)로 해당객체에 property 추가하는 것을 막을 수 있음
    -> 추가하는 것이 안될뿐 delete를 통한 삭제는 가능함

  2. Seal
    -> property attribute 중 configurable 값이 false이고, 별도의 property 추가 및 삭제가 불가능하도록 만든 것과 같음

    const lim = {
        name: 'saemyung',
        age: 23,
    };
       
    console.log(Object.isSealed(lim));		// false
    Object.seal(lim);
    console.log(Object.isSealed(lim));		// true
       
    lim.height = 170;
    console.log(lim);		// { name: 'saemyung', age: 23 }
       
    delete lim.age;
    console.log(lim);		// { name: 'saemyung', age: 23 }
    

    -> Object.seal(인스턴스명)을 통해 해당 객체를 seal 할 수 있다
    -> 원래 있던 property의 value 값 변경은 가능하지만, 별도의 property 추가 및 삭제가 불가능하다
    -> configurable값이 false인 것과 같기 때문에 예외(property attribute 설명쪽에 나와있는 2가지 예외)를 제외하면 Object.defineProperty()를 통해 property attribute를 변경하는 것이 불가능하다

  3. Freezed
    -> read 외의 모든 기능을 막는것
    -> Seal에서 property attribute 중 writable 까지 false된 것이라고 생각하면 됨

    const lim = {
        name: 'saemyung',
        age: 23,
    };
       
    console.log(Object.isFrozen(lim));		// false
    Object.freeze(lim);
    console.log(Object.isFrozen(lim));		// true
       
    lim.height = 170;
    console.log(lim);		// { name: 'saemyung', age: 23 }
       
    delete lim.age;
    console.log(lim);		// { name: 'saemyung', age: 23 }
    

    -> Object.freeze(인스턴스명)을 통해 해당 객체를 freeze할 수 있다
    -> 별도의 property 추가 및 삭제가 불가능할 뿐만 아니라, 원래 있던 property의 value값 변경 역시 불가능하다
    -> property attribute 중 writable과 configurable이 false이기 때문에 모든 property attribute 값을 변경하는 것이 불가능하다

+α) 객체 안의 객체의 경우

const lim = {
    name: 'saemyung',
    age: 23,
  	lim2: {
      name: 'sammyung',
      age: 32
    },
};

Object.freeze(lim);
console.log(Object.isFrozen(lim));		// true
console.log(Object.isFrozen(lim.lim2));		// false

-> 어떤 객체를 freeze 시켜도 그 객체 내부의 객체는 freeze되지 않음을 알 수 있다
-> 이는 freeze 뿐만 아니라 extensible, seal에서도 동일하다

this


-> JS는 Lexical Scope 방식이기 때문에 함수의 선언위치에 따라 상위 Scope가 결정되지만, **this 키워드는 객체 생성시점에 binding이 결정**된다
-> 다른 OOP언어와 다르게 상황에 따라 this가 달라지는 경우가 발생한다

[상황에 따른 this값]

  1. 전역공간에서
    -> this가 전역객체를 가르킨다
    -> 브라우저일 경우 window 이고, Node.js일 경우 global 이다
  2. 함수 호출시
    -> 마찬가지로 전역객체를 가르킨다
  3. 메서드 호출시
    -> (인스턴스명).메서드명() 으로 실행시에 해당 인스턴스명이 this가 된다
    -> eg) a.b.func(); 으로 실행시 a.b가 this가 된다
  4. new로 생성자함수 호출시
    -> new 키워드에 의해 생성된 인스턴스명이 this가 된다
  5. Callback 호출시
    -> Callback 함수를 인자로 넘겨받은 함수가 어떻게 처리하는지에 따라 this가 변한다

=> call(), apply(), bind() 3개의 메서드를 이용하여 this를 내가 원하는대로 변경하는 것이 가능하다!!!

  1. call
    -> 첫번째 인자값으로 this로 binding하고자 하는 인스턴스, 두번째 인자값부터는 해당 함수에서 사용할 인자값을 받는다

    function avg(height, weight){
    	return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`;
    }
       
    const lim = {
        name: 'saemyung',
        height: 171,
        weight: 70,
    };
       
    console.log(avg.call(lim, lim.height, lim.weight));
    // saemyung님의 평균은 120.5 입니다
    
  2. apply
    -> call과 같지만, 2번째 인자값으로 해당 함수에서 사용할 모든 인자값을 리스트로 받는다는 차이가 있다

    function avg(height, weight){
        return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`;
    }
       
    const lim = {
        name: 'saemyung',
        height: 171,
        weight: 70,
    };
       
    console.log(avg.apply(lim, [lim.height, lim.weight]));
    // saemyung님의 평균은 120.5 입니다
    
  3. bind
    -> call과 동일하지만, 바로 실행을 하지않고 나중에 실행시킬 수 있다는 차이가 있다

    function avg(height, weight){
        return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`;
    }
       
    const lim = {
        name: 'saemyung',
        height: 171,
        weight: 70,
    };
       
    const laterExecute = avg.bind(lim, lim.height, lim.weight);
    console.log(laterExecute());
    // saemyung님의 평균은 120.5 입니다
    

4. 프로토타입

-> 프로토타입 객체는 원형을 의미한다
-> 같은 생성자로부터 만들어진 객체들은 같은 프로토타입 객체를 공유한다
-> 메서드를 만들때, ‘인스턴스.메서드’의 방식보다 프로토타입을 이용하는 방식이 더 효율적이다(프로토타입을 통해 만들어놓으면, 굳이 각각의 인스턴스에서 만들 필요가 없기 때문)

__proto__


-> __proto__는 모든 객체에 존재하는 property라고 생각하면됨
-> (객체명).__proto__ 값은 해당 객체의 부모 클래스에 해당됨

class person{
    name;
    age;

    constructor(name, age){
        this.name = name;
        this.age = age;
    }
};

class people extends person{
    sayHello(){
        console.log(`hi ${this.name}`);
    }
}

const lim = new people('saemyung', 23);
console.log(lim.__proto__);		// person {}

-> people 클래스를 통해 생성한 lim객체의 __proto__ 값이 people의 부모클래스인 person임

prototype chain


function Person(name, age){
    this.name = name;
    this.age = age;
}

const lim = new Person('saemyung', 23);
console.log(lim.__proto__ === Person.prototype);		// true
console.log(Person.__proto__ === Function.prototype);		// true
console.log(Function.prototype.__proto__ === Object.prototype);		// true
console.log(Person.prototype.__proto__ === Object.prototype);		// true

-> 모든 것들의 최상위 __proto__ 값은 Object.prototype 임을 알 수 있음
-> 이런 식으로 여러 prototype끼리 연결되어 있는 것을 prototype chain이라고 부름
-> toString() 같은 메서드들도 Object로 부터 상속받아 사용하는 것임

function Person(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayHello = function(){
    return `hi ${this.name}`;
}

const lim = new Person('saemyung', 23);
const lim2 = new Person('sammyung', 25);
console.log(lim.sayHello === lim2.sayHello);		// true

-> Person이라는 생성자함수에 sayHello() 메서드를 넣으면 lim과 lim2 인스턴스를 각자 생성했을때, 각각의 sayHello() 메서드가 다른 메모리 공간에 존재하기 때문에 메모리 공간의 낭비가 발생함
-> 위의 방식에서는 Person의 prototype에 sayHello() 메서드를 넣어서 new Person()을 통해 생성된 인스턴스들에서는 모두 같은 메모리 공간의 sayHello()를 사용하도록함
-> 이런 식으로 prototype chain을 잘 활용하면, 효율적인 코딩이 가능함

인스턴스의 prototype변경 vs. 함수의 prototype변경


-> JS에서는 특이하게 함수를 통해 생성된 인스턴스의 prototype을 변경할 수도 있고, 함수 자체의 prototype을 변경할 수도 있다

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayHello = function(){
    return `hi ${this.name}`;
}
// Person 생성자함수 -> prototype에 sayHello() 추가

function Person2(name, age){
    this.name = name;
    this.age = age;

    this.coding = function(){
        return `${this.name}이 코딩중~`;
    }
}
// Person2 생성자함수 -> 해당객체의 property에 coding() 추가

const lim = new Person('saemyung', 23);
const lim2 = new Person2('sammyung', 25);

console.log(lim2.sayHello());		// 오류

// 인스턴스의 prototype 변경
Object.setPrototypeOf(lim2, Person.prototype);
console.log(lim2.sayHello());		// hi sammyung
console.log(lim2.constructor === Person);		// true
console.log(Person.prototype === Person2.prototype);		// false		


// 함수의 prototype 변경
Person2.prototype  = Person.prototype
const lim3 = new Person2('myungsae', 27);
console.log(Object.getPrototypeOf(lim3) === Person2.prototype);		// true
console.log(Object.getPrototypeOf(lim3) === Person.prototype);		// true
console.log(Person.prototype === Person2.prototype);		// true


-> Object.setPrototypeOf(인스턴스명, 변경하려는 prototype) 을 통해 인스턴스의 prototype변경이 가능하다. 인스턴스의 prototype만을 변경하는 것이므로 해당 인스턴스를 생성한 함수의 prototype은 변하지 않는다 **
-> 함수1.prototype = 함수2.prototype 의 방식으로 함수의 prototype을 변경할 수 있다. 이 경우, 함수 자체의 prototype을 변경하는 것이므로 **인스턴스의 prototype뿐만 아니라 함수의 prototype까지 변경된다

정리


function Person(name, age){
  	this.name = name;
  	this.age = age;
}

const lim = new Person('saemyung', 23);

const p1 = new Person.prototype.constructor('proto1', 10);
const p2 = new lim.constructor('proto2', 20);
const p3 = new lim.__proto__.constructor('proto3', 30);
const p4 = new Object.getPrototypeOf(lim).constructor('proto4', 40);

console.log(Person.prototype.constructor === Person);		// true

-> 위의 코드처럼 4가지 방식을 통해 동일한 프로토타입 설계가 가능하다
-> 함수명.prototype.constructor을 적용하면 원래의 생성자와 같아진다

5. 실행 컨텍스트

-> JS 코드 실행에 필요한 모든 데이터를 가지고 있는 환경
-> 크게 Global Context 와 Function Context로 나눠진다. Global Context는 최상위 context로, 브라우저의 window 객체 or Nods.js 의 global 객체를 가진다. Function Context는 함수 실행시 함수별로 실행되는 context로 함수 실행에 대한 정보를 가진다

cf) 싱글스레드 기반의 JS -> Memory Heap과 Call Stack(Execution Context Stack)으로 구분됨

Execution Context Stack


-> Execution Context Stack은 Creation Phase와 Execution Phase 2단계로 구성된다

  1. Creation Phase
    -> Global Object를 생성
    -> this를 window 또는 global에 binding
    -> 변수와 함수를 Memory Heap에 올려놓음(호이스팅)
  2. Execution Phase
    -> 코드 실행후 필요시 새로운 Execution Context를 생성함

6. Closure

-> 실행컨텍스트A의 내부에서 함수B가 실행된다고 가정할때, ‘A의 Lexical환경과 내부함수B의 조합’에서 발생하는 특별한 현상을 의미함
-> 요약하면, 컨텍스트A에서 선언한 변수를 내부함수B에서 참조할 경우 발생하는 현상
-> 상위함수보다 하위함수가 더 오래 살아남아있는 현상

function getNum(){		// 외부함수
    var num = 10;
    function innerFunc() {		// 내부함수
      return num;
    }
    return innerFunc;
}

var inner = getNum();
console.log(inner());

-> getNum()을 통해 innerFunc이라는 내부함수를 반환받음
-> 외부함수인 getNum()이 사라진 후에도 내부함수인 innerFunc()이 남아있는 상황

Closure를 사용하는 경우


  1. data caching
    -> 시간이 오래걸리는 코드를 여러번 반복하지 않고 한번만 실행하도록함

    function cacheFunc(){
        var num = 100 * 100;		// 시간이 오래걸리는 코드라고 가정
        function innerCacheFunc(newNum){
            return num * newNum;
        }
        return innerCacheFunc;
    }
       
    var inner = cacheFunc();
    console.log(inner(10));
    console.log(inner(20));
    

    -> cacheFunc()이 종료되어도 inner변수에서 innerCacheFunc()을 참조하고 있고, innerCacheFunc()에서는 cacheFunc함수의 num변수를 참조하고 있기 때문에, num값은 계속 남아있는 상황임
    -> innerCacheFunc()만 반복해서 실행하여 시간이 오래걸리는 코드인 100 * 100을 한번만 실행하도록 만듬

  2. 정보 은닉

    function Person(name, age){
        this.name = name;
        var _age = age;
       
        this.sayNameandYear = function(){
            return `hi ${this.name},${_age}`;
        }
    }
       
    const lim = new Person('saemyung', 23);
    console.log(lim.sayNameandYear());		// hi saemyung, 23
    

    -> this.age = age; 를 이용하지 않고 _age 라는 변수를 만들어 sayNameandYear 함수에서만 접근할 수 있도록 함으로써 정보를 은닉할 수 있다
    -> 추가적으로 property값에 #을 붙임으로써 최근 JS의 기능인 private property를 사용할 수도 있다

7. Async Programming(비동기 프로그래밍)

-> Sync(동기): 현재 실행 중인 task가 종료하기 전까지 다음 task가 실행하지 못하고 대기하는 방식
-> Async(비동기): 현재 실행 중인 task가 종료되지 않았더라도 다음 task를 실행시키는 방식

=> JS는 하나의 실행 컨텍스트를 가지는 싱글스레드로 작동하지만, 이벤트루프의 도움을 받아 비동기처리가 가능하다

Event Loop


-> JS는 동기식 처리를 위해 Memory Heap과 Call Stack을 가지며 당연히 이 둘로는 비동기 처리를 하지 못한다
-> JS는 Event Loop의 도움을 받아 비동기식 처리가 가능하다. 처리 방식은 아래와 같다

  1. Call Stack에 동기함수가 들어오면 Call Stack에서 처리한다
  2. Call Stack에 비동기함수가 들어오면 Event Loop에 의해 백그라운드로 옮겨지고, 여기서 비동기 처리를 진행한다 eg) setTimeout() 함수에서 정해진 시간만큼 백그라운드에서 타이머 작동
  3. 백그라운드에서 타이머에 의해 비동기 처리가 완료되면 Task Queue로 콜백함수를 옮긴다
  4. Call Stack이 비어있을 경우에만 해당 콜백함수를 Task Queue에서 Call Stack으로 옮긴다. 단, Task Queue에 Promise의 콜백함수와 setTimeout()의 콜백함수가 함께 있을 경우, Promise가 우선순위를 가진다

비동기 프로그래밍 예시


function longWork(){
    const now = new Date();
    const millisec = now.getTime();
    const afterTwoSec = millisec + 2 * 1000;

    while(new Date().getTime() < afterTwoSec){		// 2초동안 반복

    }
    console.log('완료');
}

console.log('hello');
longWork();
console.log('world');

-> 위의 코드는 동기식으로 작동하기 때문에, ‘hello’가 출력되고 2초간 longWork() 작업 후 ‘완료’를 출력하고 나서야 ‘world’가 출력된다

function longWork(){
    setTimeout(() => {
        console.log('완료');
    }, 2000);
}

console.log('hello');
longWork();
console.log('world');

-> 이 코드는 setTimeout()이라는 비동기함수의 도움을 받아 비동기식으로 작동한다
-> ‘hello’가 출력되고 longWork() 작업을 하지만, 비동기식으로 작동하기 때문에, ‘world’가 먼저 출력되고 2초 작업 후 ‘완료’가 출력된다

Promise


과거, JS는 비동기 처리를 하기 위해 콜백함수를 사용하였는데, 아래처럼 복잡한 형태의 콜백지옥의 문제가 발생

function callBack(){
    setTimeout(() => {
        console.log('1번 callback 끝');
        setTimeout(() => {
            console.log('2번 callback 끝');
            setTimeout(() => {
                console.log('3번 callback 끝');
            }, 2000);
        }, 2000);
    }, 2000);
}
callBack();

-> 에러처리가 힘들고, 여러 개의 비동기 처리를 한번에 하는데 한계가 있음

=> 이를 해결하기 위해 생겨난 것이 프로미스임

const promise = new Promise((resolve, reject)=>{
    const num = 1 + 1;
    setTimeout(() => {
        if(num==2) resolve('일치');
        else reject('불일치');
    }, 2000);
});

promise.then((res)=>{
    console.log(res);
}).catch((res)=>{
    console.log(res);
})
// 일치

-> Promise객체는 resolve와 reject를 인자로 받는 콜백함수를 인자로 받음
-> 비동기 처리가 성공일 경우 resolve함수를, 실패일 경우 reject함수를 호출
-> new를 통해 생성한 인스턴스를 통해 후속처리가 가능한데, then()은 주로 비동기 처리가 성공시에 사용하고, catch()는 비동기 처리가 실패시에만 예외처리를 위해 사용함
-> Promise는 resolve, reject까지는 동기식으로 처리되고, .then 또는 .catch 부터 비동기식으로 처리됨
-> 이와 별개로 console.log()를 실행 시에 비동기식으로 처리하기 때문에 해당 console.log()가 먼저 출력됨

const promise = (msg)=>{
    return new Promise((resolve, reject)=>{
        if(msg == 'success') resolve('success');
        else reject('fail');
    })
}

promise('success')
    .then(res => promise(res))
    .then(res => console.log(`task is ${res}`))
    .catch(err => console.log(`task is ${err}`));
// task is success

-> 이런 식으로 복잡했던 콜백지옥을 promise chaining으로 여러 비동기 함수를 손쉽게 처리가 가능

const getPromise = (sec)=> {
    return new Promise((resolve, reject)=>{
        setTimeout(() => {
            resolve(sec);
        }, sec * 1000);
    })
}

Promise.all([
    getPromise(1),
    getPromise(2),
    getPromise(3)
]).then((res)=>{
    console.log(res);		// [1, 2, 3]
})

-> 서로 의존하고 있지 않은 Promise들끼리는 Promise.all()을 이용하여 동시에 실행하여 빠른 처리가 가능
-> 이 경우, 가장 늦은 Promise를 기준으로 처리된다. 위 코드의 경우 제일 늦은 3초 후에 처리결과가 출력됨

async & await


-> Async Programming을 위해 최근에 주로 사용하 는 것이 async와 await 키워드이다

const promise = (sec)=>new Promise((resolve, reject)=>{
    setTimeout(() => {
        resolve('완료');
    }, sec * 1000);
});

async function runner(){
    try{
        const result1 = await promise(1);
        console.log(result1);

        const result2 = await promise(2);
        console.log(result2);

        const result3 = await promise(3);
        console.log(result3);
    } catch(e){
        console.log(e);
    }
}

runner();

-> 비동기적으로 처리할 함수 앞에 async 키워드를 붙이고, 처리할 Promise앞에 await를 붙이면 비동기식 처리가 가능하다. 단, await 키워드는 오직 Promise함수에만 붙일 수 있다
-> 위의 비동기 처리방식과 다르게 resolve 처리시에 여기서는 await 키워드를 붙이고 변수로 받아 console.log()로 출력하면 되기 때문에 훨씬 간단해졌다. 그리고 reject 처리시에는 try-catch 구문에서 e로 해당 인자를 받아 처리한다

8. 그외

구조분해


-> 배열 및 객체 구조분해를 이용하면 값변경이 용이하다. 실제로 두 변수의 값을 바꾸려면 또다른 하나의 변수를 선언해야하지만 구조분해를 이용하면 이런 과정없이 해결가능하다

let n1=1, n2=2;
[n1,n2] = [n2,n1];

-> 위의 방식으로 n1과 n2의 값을 바꿀 수 있다

let user = {
    name: 'lim',
    age:23,
};

let {name, age} = user;
console.log(name);      // lim
console.log(age);       // 23

-> name과 age 각각의 변수값에 user.name과 user.age의 값이 들어간다

나머지 매개변수


-> 함수로 인자값이 여러개 들어올 경우 일일이 적는 것을 대체하기 위해 ES6에서는 나머지 매개변수라는 방법을 도입했다
-> 나머지 매개변수에는 배열의 형태로 들어간다

function User(name, age, ...skills){
    this.name = name;
    this.age = age;
    this.skills = skills;
}

const user1 = new User('lim', 23, 'js', 'ts', 'node');
const user2 = new User('kim', 33, 'java', 'spring');
const user3 = new User('park', 43, 'python', 'django', 'flask');

console.log(user1);
console.log(user2);
console.log(user3);

-> 생성자로 많은 인수를 받아야할때 나머지 매개변수를 이용하면 간단히 표현이 가능하다

전개구문


-> 전개구문을 사용한 손쉬운 배열, 객체 복사가 가능하다
-> 여기서 복사라고 표현한 이유는 전개구문을 통해 생성된 배열 or 객체는 기존의 것과 별개로 새로 만들어진 배열 or 객체이다

const arr = [1,2,3];
const arr2 = [...arr];
console.log(arr2);      // [1,2,3]
console.log(arr===arr2);        // false

const obj = {name:'lim', age:23};
const obj2 = {...obj};
console.log(obj2);      // { name: 'lim', age: 23 }
console.log(obj===obj2);        // false

-> 전개구문을 통해 새로 만든 배열 or 객체는 기존의 배열 or 객체와 다르다

let user = {name: 'lim'};
let info = {age: 23};
let fe = ['js', 'node'];
let lang = ['kor', 'eng'];

let obj = Object.assign({},
    user, info,
    {skills: []}
);

fe.forEach((elem) => {
    obj.skills.push(elem);
});
lang.forEach((elem) => {
    obj.skills.push(elem);
});

console.log(obj);
// { name: 'lim', age: 23, skills: [ 'js', 'node', 'kor', 'eng' ] }
let user = {name: 'lim'};
let info = {age: 23};
let fe = ['js', 'node'];
let lang = ['kor', 'eng'];

let obj = {...user, ...info, skills: [...fe, ...lang]};

console.log(obj);
// { name: 'lim', age: 23, skills: [ 'js', 'node', 'kor', 'eng' ] }

-> 2가지 코드 중 위의 것이 전개구문을 사용하지 않고 복사한 경우이고, 아래가 전개구문을 사용하여 복사한 경우인데, 훨씬 효율적인 코드로 바뀌었음을 알 수 있다

Leave a comment