자바스크립트 제대로 배우자2

2023. 4. 2. 23:52Javascript/실전 자바스크립트

잠깐, 참고하자

https://frontj.com/entry/7-Javascript%EC%9D%98-this%EC%99%80-execution-context

 

7. Javascript의 this와 execution context

this와 execution context 자바스크립트의 혼란스러운 개념 중 하나로 this키워드에 대해 알아보도록 하겠습니다. 더불어 실행 컨텍스트(execution context)도 간단히 알아보겠습니다. 실행 컨텍스트 또한

frontj.com

https://frontj.com/entry/8-Javascript%EC%9D%98-%EC%BD%9C-%EC%8A%A4%ED%83%9D%EA%B3%BC-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84

 

8. Javascript의 콜 스택과 이벤트 루프

지난 포스트에서 잠깐 다루었던 콜 스택(call stack)에 대해 알아보고, 조금 더 나아가 자바스크립트는 한 순간 하나의 작업만 처리할 수 있는 단일 스레드 기반의 언어임에도 불구하고 동시에 많

frontj.com

https://velog.io/@kich555/Lexical-Environment-Scope-TDZ-wqlit1i9

 

Lexical Environment & Scope & TDZ

아 ㅋㅋ 그냥 let const 쓰라고

velog.io

https://poiemaweb.com/js-closure

 

Closure | PoiemaWeb

클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않

poiemaweb.com

https://opentutorials.org/course/743/6544

 

클로저 - 생활코딩

클로저 클로저(closure)는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킨다. 클로저는 자바스크립트를 이용한 고난이도의 테크닉을 구사하는데 필수적인 개념으로 활용된다.

opentutorials.org

섹션 4. 함수

일급 함수 및 콜 스택

First class citizen(일급함수)

함수가 다른 변수처럼 취급되면, 그 언어는 일급함수를 가지고있다라고 할수있다.

 

그래서 함수를 변수에 담을 수 있다

매개변수로 전달도 가능

함수안에서 도 다른 함수를 반환 가능

// 함수를 변수에 담을 수 있다.
const add10 = function (a) {
  return 10 + b;
};

//함수를 매개변수로 전달도 가능하다
function apply(arr, op) {
  return arr.map(op);
}
apply([1, 2, 3], add10);

// 함수안에서 또 다른 함수를 반환할 수 있다.
function makeAdd(v1) {
  return function (v2) {
    return v1 + v2;
  };
}
const add3 = makeAdd(3);
console.log(add3(10));
const add7 = makeAdd(7);
console.log(add7(10));

클로저 기능

클로저 : 함수와 그 함수를 둘러싸고 있는 주변의 상태를 기억하는 기능

클로저 덕분에 내부함수는 외부함수의 지역변수와 매개변수에 접근할 수 있다.

많은 언어에서 함수의 지역변수와 매개변수는 함수가 실행되는 동안에만 존재한다.

하지만, 자바스크립트에서는 클로저가 있기 때문에!

// 함수안에 함수가 있는데, 클로저의 기능을 할 수 있다.
/*
v1은 함수가 실행을 끝내면 없어지는데, 

*/

function makeAdd(v1) {
  return function (v2) {
    return v1 + v2;
  };
}
const add3 = makeAdd(3); // 여기서 사용한 v1이   >> 3이다가,
console.log(add3(10)); //13 // 클로저를 이용하여 여기서도 v1을 사용한다     >> 13이 됨!
const add7 = makeAdd(7);
console.log(add7(10));// 17

모든 언어에서는 함수의 실행정보를 관리하기 위해서 콜스택(call stack)이라는 것을 관리한다.

함수가 실행될때마다, 현재까지 실행하던 함수의 정보를 콜스택에 저장하고, call stack은 하나가 된다. 

함수가 실행을 종료하면, 콜스택에서 이전에 마지막으로 실행했던 함수의 정보를 꺼내온다.

 

콜스택에 담기는 함수 실행정보를 -> execution context라고 부른다.

 

전체를 감싸는 하나의 커다란 함수가 하나 있다고 생각을 할 수 있는데,

프로그램이 처음 실행될 때 global execution context라는 것이 생성된다고 이해하면된다.

 

global execution context가 만들어진 상태에서 위에서부터 실행이 된다.

그러다 함수실행을 만난다. f2();

그럼 지금까지 가지고있던 execution context를 콜스택에 넣는다. call stack은 두개가 된다. 

global execution context가 콜스택에 담긴것이다!

그리고, 새로운 execution context가 만들어진다.execution context가 1개가 된다.

f2(); 함수 호출을 위한 exection context가 만들어지는 것이다.

 

function f2() {
  f1();
  console.log(v2);
}

 

가 실행되면, f1();의 함수의 실행으 만나게 된다.

그럼, 지금 가지고있던 execution context를 콜스택에 넣는다. 그럼 execution context는 2개가 된다.

 

function f1() {
  const v1 = 123;
  console.log(v1);
}

가 실행되는데,

지역변수가 처음으로 나오게 되었는데,   const v1 = 123;

지역변수를 가지고있는데, 이는 lexical enironment라고 부른다.

변수이름과 그 값을 key 와 value로 하는 map이라고 생각하면된다. (변수의 사전)

{v1:123} 과 같은 map이 있다고 생각하면된다.

v1이 사용될 때 변수를 찾을 텐데,  lexical enironment에서 찾는다.

그래서 f1함수의 실행이 끝이난다.

그럼 현재 가지고 있는 execution context는 삭제하고,

콜스택에서 마지막에 저장된 것을 하나 가져온다. call stack은 다시 하나가 된다. 

console.log(v2);를 실행하기 위해 v2를 찾는다.

execution context안에있는  lexical enironment는 비어있는 map일것이다.(지역변수가 없기에)

v2를 찾으니 없다.

그런데 javascript에서는 456을 사용하는데, 자신의  lexical enironment 안에는 없는데 찾게된다. 이는 나중에...

그래서 함수의 실행은 여기서 끝이난다.

그럼 현재 가지고 있는 execution context는 삭제하고, 

call stack에 담겨있는 execution context를 하나 꺼낸다. >> gloabal execution context이다.

그래서 다시 f2();부터 다시 실행 시작한다.

사실 글로벌 영역에서 변수가 하나 있었다. const v2 = 456;

 gloabal execution context 안에 있는 lexical enironment에 {v2:456}와 같은 값이 있었을 것이다.

그래서 아까 v2를 찾으려 할때, 여기서 찾게 되는데 찾는 방법은 추후에....

function f1() {
  const v1 = 123;
  console.log(v1);
}
const v2 = 456;
function f2() {
  f1();
  console.log(v2);
}
f2();

/*
123
456
*/

 

또 다른 예시

lexical environment 1, lexical environment 2

 

 

function main() {
  var v = 0;
  function f1() {
    v++;
    console.log(v);
  }
  function f2() {
    v++;
    console.log(v);
  }
  return { f1, f2 };
}
const obj = main();
obj.f1();
obj.f2();
obj.f1();
obj.f2();

/*
1
2
3
4
*/

함수 정의 방법

function printLog(a = 1) {
  console.log({ a });
}
printLog();
printLog(3);
/*
{ a: 1 }    >> 얘만 기본값으로 됨. 왜냐하면 값이 없었기 때문이지
{ a: 3 }
*/

기본값에 함수를 입력할 수 도 있지

근데! 기본값이 사용 될때만 함수가 호출된다는 점!  

>> CPU를 효율적으로 사용할 수 있다.

function getDefault() {
  console.log('called getDefault');
  return 1;
}
function printLog(a = getDefault()) {
  console.log({ a });
}
printLog();
printLog(3);

/*
printLog(); 인경우
called getDefault
{ a: 1 }


printLog(3); 인경우
{ a: 3 }
*/

매개변수가 undefined일때만 호출되게 한다면,

매개변수를 필수 값으로 만들수도있다.

함수가 호출될때, 예외처리를 하면되는데!

이렇게 하면 필수값이 된다!

function required() {
  throw new Error('no parameter');
}
function printLog(a = required()) {
  console.log({ a });
}
printLog(10); // 인수 호출 시 함수가 호출되지않음 {a:10}이렇게 나옴
printLog();
/*
아래와 같이 에러가 발생한다.
  throw new Error('no parameter');
  ^

Error: no parameter
*/

나머지 매개변수 정의방법

rest parameter

...을 이용해 표현

이렇게 함으로, 갯수에 제한을 두지않음!

 

첫번째를 제외하고, 모두 rest에 담겼음

function printLog(a, ...rest) {
  console.log({ a, rest });
}
printLog(1, 2, 3);
/*
{ a: 1, rest: [ 2, 3 ] }
*/

나머지 매개변수는 ES6에 추가된 기능으로

이전에는 arguments라는 키워드가 비슷한 역할을 하였다.

그런데 이 방식은 arguments의 정체가 명시적으로 드러나지 않기 때문에 가독성이 좋지 않다!

arguments는 또한 배열이 아니라서, 배열처럼 사용하기 위해서는 배열로 변환하는 과정이 필요하다는 단점이 있다

>> Array.from(arguments)

 

그래서 나머지 매개변수를 사용한 방식이 더 효울적이다!

function printLog(a) {
  const rest = Array.from(arguments).splice(1);
  console.log({ a, rest });
}
printLog(1, 2, 3);
/*
{ a: 1, rest: [ 2, 3 ] }
*/

명명된 매개변수 (named parameter)

함수 호출 시에, 매개변수의 이름과 값을 동시에 적을 수 있기 때문에 가독성이 높다

ex) const result2 = getValues2({ numbers, greaterThan: 5, lessThan: 25 });

 

 

const numbers = [10, 20, 30, 40];
const result1 = getValues1(numbers, 5, 25);

위와 같이 명명된 매개변수 방식을 사용하지않는다면, 각 매개변수가 무엇을 의미하는지 알기 힘들다.

하지만 명명된 매개변수 (named parameter)를 이용하면, 각 값이 의미하는 바를 쉽게 알 수있다.

 

명명된 매개변수 (named parameter)를 이용하기 위해서는 함수를 정의할 때{ } 를 이용해야한다.

function getValues2(  { numbers, greaterThan, lessThan }  ) {}

function getValues1(numbers, greaterThan, lessThan) {
  return numbers.filter(item => greaterThan < item && item < lessThan);
}
function getValues2({ numbers, greaterThan, lessThan }) {
  return numbers.filter(item => greaterThan < item && item < lessThan);
}

const numbers = [10, 20, 30, 40];
const result1 = getValues1(numbers, 5, 25);
const result2 = getValues2({ numbers, greaterThan: 5, lessThan: 25 });

명명된 매개변수에서 기본값을 입력할 때에도 아래와 같이 등호를 사용하면된다.

function getValues({ numbers, greaterThan = 0, lessThan = Number.MAX_VALUE }) {}

그래서 매개변수를 입력하지않는다면, 기본값을 사용하게 된다!

 

명명된 매개변수에서는 매개변수의 위치는 중요하지않다. 단지 이름이 중요하다!

명명된 매개변수를 사용하지않고, 일부 매개변수를 입력하지 않을 때는 중간에 undefined를 입력하게되는데,

넣는게 불편하기도하고, 보기에도 안좋아서, 입력하고 싶은 매개변수만 입력하면됨

function getValues({ numbers, greaterThan = 0, lessThan = Number.MAX_VALUE }) {
  return numbers.filter(item => greaterThan < item && item < lessThan);
}

const numbers = [10, 20, 30, 40];
console.log(getValues({ numbers, greaterThan: 5, lessThan: 25 }));
console.log(getValues({ numbers, greaterThan: 15 }));
console.log(getValues({ lessThan: 25, numbers }));
// getValues(numbers, undefined, 25);
/*
[ 10, 20 ]
[ 20, 30, 40 ]
[ 10, 20 ]
*/

명명된 매개변수에서도 나머지 매개변수를 넣을 수 있다. ...rest를 이용하여!

만약 나머지 매개변수가 엇ㅂ다면, 빈 객체가 담겨서    { }   를 출력한다!

function f1({ p1, p3, ...rest }) {
  console.log({ p1, p3, rest });
}

f1({ p1: 'a', p2: 'b', p3: 'c', p4: 'd' });
f1({ p1: 'a', p3: 'b' });
/*
{ p1: 'a', p3: 'c', rest: { p2: 'b', p4: 'd' } }
{ p1: 'a', p3: 'b', rest: {} }
*/

화살표 함수를 이용하면, 함수를 간결하게 작성할 수 있다.

 

함수를 중괄호로 감싸지 않으면, 계산 결과가 반환값이 된다.

return을 입력하지 않아도 되기에, 코드가 간결해짐!

const add = (a, b) => a + b;

 

매개변수가 하나라면 소괄호를 생략할 수 있다.

const add5 = a => a + 5;

 

객체를 반환해야한다면, 객체를 소괄호로 한번 더 감싸주면된다.

const addAndReturnObject = (a, b) => ({ result: a + b });

const add = (a, b) => a + b;
const add5 = a => a + 5;
const addAndReturnObject = (a, b) => ({ result: a + b });

화살표 함수에 여러줄의 코드가 필요하다면, 

다음과 같이 중괄호로 묶어주면된다.

이 경우에는 return을 사용해야한다.

const add = (a, b) => {
  if (a <= 0 || b <= 0) {
    throw new Error('must be positive number');
  }
  return a + b;
};

화살표함수와 일반함수의 다른점은

this와 arguments가 바인딩 되지않는다는 점이다.

 

화살표함수에서 arguments가 필요하다면, 나머지 매개변수를 이용하면된다.

const printLog = (...rest) => console.log(rest);
printLog(1, 2);
// [ 1, 2 ]


this

// 화살표 함수의 this는 화살표함수가 생성될 당시의 this를 가리킨다
// 화살표함수가 생성될 당시 >> Counter 함수의 this를 가리킴. counter 객체가 되는 것
// 화살표와 함수의 this는 이 화살표 함수가 생성될 당시의 this로 고정이 되기 때문에
// 정적이라 할수 있다.
// 화살표 함수의 this는 이 화살표 함수가 생성될 당시의 this로 고정이 되기 때문에 정적이라할 할 수 있음
// add함수를 주체하는 누구인가가 상관이 없다는 것

// 반대로 일반함수일때에는 , 호출된 당시의 상황에 따라서 this가 바뀌기 때문에 동적이라 할 수 있다
// 화살표함수는 정적으로 결정 되어있는 것
function Counter() {
  this.value = 0;
  this.add = (amount) => {
    this.value += amount;
  };
}
// new는 인스턴스를 생성시킨다.
// Counter객체가 생성이 되는것
// 그래서 위에서 가르키는 this는 아래의 counter가 되는것
// 그래서 이 안에는 value와 add 메소드가 있는 것
const counter = new Counter();
console.log(counter.value); //0
counter.add(5);
console.log(counter.value); //5
const add = counter.add;
add(5);
console.log(counter.value); //10

//   위에서는 화살표 함수를 이용했다면, 여기서는 일반함수를 정의했다
function Counter2() {
  this.value = 0;
  this.add = function (amount) {
    this.value += amount;
    console.log(this === global); // false, true 한번씩
  };
}
const counter2 = new Counter2();
console.log(counter2.value); //0
counter2.add(5);
console.log(counter2.value); //5
const add2 = counter2.add;
// 화살표함수 this는 함수가 생설될 당시의 this를 가리킴
// 여기서 this가 가르키는 것이 Counter2가 아니기 때문
// 일반함수의 this는 이 함수를 호출한 주체를 가르킨다
// counter2.add(5);에서는 counter2가 주체가 되는것이고
// add2(5); 에서는 주체가 보이지않아, 전역객체를 가르킨다
// 브라우저에서는 window 객체를 가리킬 것이고, 지금은 node환경이라 global 객체를 가리킴
add2(5);
console.log(counter2.value); //5
// 0 flase 5 true 5

// class의 경우에도 마찬가지
// 일반함수일 경우 동적을 결정이 됨
class Counter3 {
  value = 0;
  add(amount) {
    this.value += amount;
  }
}

// 화살표 함수일 경우에는 this는 항상 Counter3 클래스의 객체를 가리키게 될것이다.
class Counter3 {
  value = 0;
  add = (amount) => {
    this.value += amount;
  };
}

// 여기서는 함수가 아닌 객체로 감쌌고
// 일반함수로 add를 정의했다
const counter3 = {
  value: 0,
  add: function (amount) {
    this.value += amount;
  },
};
console.log(counter3.value); //0
counter3.add(5);
console.log(counter3.value); //5
const add3 = counter3.add;
add3(5);
console.log(counter3.value); //5

// 화살표 함수라면
// 화살표 함수가 생성될 당시의 this는 화살표함수를 감싸고 있는 일반함수가 없기 때문에
// 항상 전역 객체를 가리키게 됨
// 그래서 아무리 호출되어도 value가 증가하지않는것 > 이를 가리키는게 아니기 때문
const counter4 = {
  value: 0,
  add: (amount) => {
    this.value += amount;
  },
};
console.log(counter4.value); //0
counter4.add(5);
console.log(counter4.value); //0
const add4 = counter4.add;
add4(5);
console.log(counter4.value); //0

 

섹션 5. 비동기 처리

자바스크립트의 비동기 처리 : Promise, 콜백패턴

프로미스 1

아래 requestData함수는 콜백패턴으로 작성된 함수이다.

콜백함수를 입력받고 비동기처리가 끝나면, 이 함수를 호출해준다.

사용하는 부분을 보면, 비동기함수(requestData)를 호출하면서, 콜백함수(onSsuccess)를 입력하고있다.

그래서 먼저, consolelog로 call requestdata를 출력하고,

1초 후에 console.log(data)가 실행된다!

function requestData(callback) {
  setTimeout(() => {
    callback({ name: 'abc', age: 23 });
  }, 1000);
}

function onSuccess(data) {
  console.log(data);
}
console.log('call requestData');
requestData(onSuccess);
/*
call requestData
{ name: 'abc', age: 23 }
*/

콜백패턴을 콜백이 조금만 중첩되어도, 코드가 상당히 복잡해지는 단점이 있다.

requestData1,requestData2  두개의 비동기함수가있는데!

맨 아래에서 첫번째 비동기함수를 호출하고있다. 그리고 콜백함수로 입력하고있다.

그리고 두번째 비동기함수를 호출하고 두번째 콜백함수로 입력하고있다.

굉장히 간단함에도 불구하고, 상당히 복잡하다

function requestData1(callback) {
  // ...
  callback(data);
}
function requestData2(callback) {
  // ...
  callback(data);
}
function onSuccess1(data) {
  console.log(data);
  requestData2(onSuccess2);
}
function onSuccess2(data) {
  console.log(data);
  // ...
}
requestData1(onSuccess1);

그래서 앞에서 봤던 예를 Promise로 작성한 예이다.

Promise는 비동기함수를 값으로 다룰 수 있는 객체인데

Promise를 사용하면 비동기 프로그래밍을 할 때, 동기 프로그래밍 방식으로 코드를 작성할 수 있다.

// 비동기함수 호출
requestData1()
  // 처리 후 또 필요한 처리
  .then((data) => {
    console.log(data);
    // 두번째 함수 호출, 그리고 끝나면
    return requestData2();
  })
  // 데이터를 받아서 필요한 처리
  .then((data) => {
    console.log(data);
    // ...
  });

 

프로미스를 사용하면 비동기 프로그래밍을 할 때, 코드를 순차적으로 작성할 수 있다는 사실만 기억하면된다!

 

프로미스 객체 생성방법 3가지

1. new 키워드 이용

2. 함수호출 (resolve, reject 두개가 다 함수)({}처리가 끝나면, 두 함수중 하나를 호출하게됨, 비동기처리가 끝났다는 것을 알려줌)

 

Promise의 3개의 상태 -- 이 중 하나의 상태로 존재

1. 대기중(pending) -- 비동기상태가 끝나지 않은 경우

2. 성공(fulfilled) -- 비동기 처리가 끝나고 성공했을 때

3. 실패(rejected) -- 실패

 

fulfilled, rejected상태를 settled상태라고 부르기도한다.

Promise는  settled상태가 되면, 더이상 다른상태로 변경되지않는다.

pending 상태일때만 다른 상태로 변할 수 있다.

 

new 키워드 사용시 peding상태

두 함수중 하나를 호출하기 전에는 상태가 변경되지않음

resolve를 호출하면, fulfilled상태가 된다.

rejecte상태를 호출하면, rejected상태가 된다.

 

Promise.reject 객체를 만들 때에는, rejected상태인 Promise 객체가 만들어진다.

Promise.resolve 로 만들면, fulfilled상태인 Promise 객체가 만들어진다.

 

Promise는 상태말고도, 데이터를 가질 수 있다.

아래의 'error message', param등이 Promise 데이터라고 생각하면된다

함수를 호출할 때도, => {resolve('안녕')} 처럼 데이터를 입력해 줄 수 있다.

const p1 = new Promise((resolve, reject) => {});
const p2 = Promise.reject('error messgae');
const p3 = Promise.resolve(param);

Promise 객체는 then 메서드를 가지고 있다. 비동기 처리가 끝난 다음에 처리할 일을 then 메서드로 정의할 수 있다.

onResolve, onReject

Promise 객체가 fulfilled 상태가 되면, 첫번째 함수가 호출된다.

Reject면 두번째 함수가 호출된다.

fulfilled상태인 Promise 객체를 만들면, 첫번째 입력한 함수가 호출 될것이다.

rejected상태인 Promise객체를 만들면, 

두번째 함수가 호출이 될것이다.

requestData().then(onResolve, onReject);
Promise.resolve(123).then(data => console.log(data));
Promise.reject('error').then(null, data => console.log(data));

then 메서드는 이렇게 체인 형식으로 연결할 수 있다.

이는 then 메서드가 프로미스 객체를 반환하기 때문인데,

 

근데 왜 여기서 rejected상태가 되어서, 바로 error로 가지? 그리고 왜 undefined가 되는거지? 사라지는건가?

??????????????

function requestData1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(20);
    }, 1000);
  });
}

requestData1()
  .then(data => {
    console.log(data);
    return requestData2();
  })
  .then(data => {
    console.log(data);
    return data + 1;
  })
  .then(data => {
    console.log(data);
    throw new Error('some error');
  })
  .then(null, error => {
    console.log('error!!!');
  })
  .then(data => {
    console.log(data);
  });

/*
10
20
21
error!!!
undefined
*/

rejected상태

????????????????

Promise.reject('err')
  .then(() => console.log('then 1'))
  .then(() => console.log('then 2'))
  .then(
    () => console.log('then 3'),
    () => console.log('then 4'),
  )
  .then(
    () => console.log('then 5'),
    () => console.log('then 6'),
  );
/*
then 4
then 5
*/

catch는 rejected상태인 Promise객체를 처리하기위해서 사용

then을 이용해서 두번째 함수를 입력하는것과 같다. (error=>{})

근데, 예외 처리시, then보다는 catch를 사용하는것이 더 가독성에 좋다.

Promise.reject(1).then(null, error => {
  console.log(error);
});
Promise.reject(1).catch(error => {
  console.log(error);
});
/*
1
1
*/

여기서는 then에 두개의 함수를 입력하고있는데,

첫번째 함수에서 예외가 발생했을 때, 두번째 함수가 호출되지않는다.

throw~~에서 발생된 예외는 뒤에서 추가로 처리를 해야겠지!

    console.log(error);
  },
).then~~~~~;

Promise.resolve().then(
  () => {
    throw new Error('some error');
  },
  error => {
    console.log(error);
  },
);

이렇게 작성하면 좋다!then과 catch

Promise.resolve()
  .then(() => {
    throw new Error('some error');
  })
  .catch(error => {
    console.log(error);
  });

then과 마찬가지로, catch도 Promise 객체를 반환한다.

그래서 catch이후에도 then을 계속해서 사용할 수 있다.

 

reject이기때문에, 첫번째 then은 생략되고, catch로 넘어가면된다.

catch에서는 30을 반환하는데 이는 fulfilled가 되기에 then으로 가서 30을 출력!

Promise.reject(10)
  .then(data => {
    console.log('then1:', data);
    return 20;
  })
  .catch(data => {
    console.log('catch:', data);
    return 30;
  })
  .then(data => {
    console.log('then2:', data);
  });
  /*
  catch: 10
then2: 30
*/

finally메서드

fulfilled상태와 rejected상태 모두를 처리할 수 있다.

finally는 then에 두개의 함수를 입력하는 것과 같음, 하지만 이와 동일하게 동작하지는 않음

.then(() => {

console.log('onFinally')

}, () => {

console.log('onFinally')

})

차이점은 finally에는 데이터가 넘어오지않음, 이전에 있던 Promise 객체를 그대로 반환한다.

함수안에서 반환하는 값과는 상관없다.

Promise.resolve(10) //fulfilled라서 다음 then을 시행
  .then(data => {
    console.log('onThen', data);
    return data + 1;
  })
  .catch(error => {//fulfilled라서 뛰어넘음
    console.log('onCatch');
    return 100;
  })
  .finally(() => {//출력, 첫번째 then에서 만들어진 Promise객체를 그대로 반환
    console.log('onFinally');
  })
  .then(data => { //그래서 11 받음
    console.log('onThen', data);
    return data + 1;
  });
/*
onThen 10
onFinally
onThen 11
*/

reject경우

Promise.reject(10)
  .then((data) => {
    console.log("onThen", data);
    return data + 1;
  })
  .catch((error) => {
    console.log("onCatch");
    return 100;
  })
  .finally(() => {
    console.log("onFinally");
  })
  .then((data) => {
    console.log("onThen", data);
    return data + 1;
  });
/* onCatch
onFinally
onThen 100*/

서버통신으로 데이터를 받아오는 경우,

fetch에서 서버와 통신을 할 것이고,

만약 문제가 있었다면, catch에서 처리했을 것임

에러 여부 상관없이 finally는 처리됨

그리고 함수가 반환하는 값은 finally와 상관없이, finally는 이전에 생성된 Promise 객체를 그대로 반환하기 때문

그래서 대부분 fulfilled상태인 PRomise객체가 넘어온다.

왜냐하면 rejected상태인Promise 객체가 반환됐다고 하더라도, catch에서 처리될 것이고,

예외가 발생하지않는다면, fulfilled상태인 Promise객체가 반환되겠지

만약 

.catch(error => {

return null

})

과 같이 입력이 되어있다면, 서버통신에 문제가 있을 때 null이 넘어오게 된다. 

그래서 아래 then(data)에 null이 넘어오게 된다.

function requestData() {
  return fetch()
    .catch(error => {
      // ...
      return null;
    })
    .finally(() => {
      sendLogToServer('requestData finished');
    });
}
requestData().then(data => console.log(data));

프로미스 2

Promise사용할때 then 메서드로 연결하면, 순차저긍로 실행이 된다.

근데, 이때 각각의 처리가 병렬로 철리되지 않는 단점이 있다.

근데 이 두 함수간에 의존성이 없다면, 병렬처리하면 훨씬 빠르겠지!

function requestData1() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(20);
    }, 1000);
  });
}

requestData1()
  .then(data => {
    console.log(data);
    return requestData2();
  })
  .then(data => {
    console.log(data);
  });
  /*
  10
20
*/

병렬로 처리하는 코드를 만들어보았다! 그래서 2번은 1번을 기다리지 않겠지

function requestData1() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(20);
    }, 1000);
  });
}

requestData1().then(data => console.log(data));
requestData2().then(data => console.log(data));
/*
10
20
*/

여러 Promise를 처리하고싶은 경우

Promise.all을 사용하면된다

여기서는 매개변수를 배열로 입력하였다.

Promise.all함수는 Promise 객체를 반환한다. >> 그래서 then 메소드를 사용할 수 있다.

Promise.all 함수가 반환하는 Promise 객체는 입력된 모든 Promise 객체가 fulfilled 상태가 되어야 마찬가지로 fulfilled상태가 된다.

하나라도 rejected상태가 된다면, Promise.all가 반환하는 Promise 객체도 rejected 상태가 된다.

Promise.all([requestData1(), requestData2()]).then(([data1, data2]) => {
  console.log(data1, data2);
});

Promise.race

여기에도 배열을 입력하고 있다. 

여러개의 Promise 중에서 가장 빨리 settled 상태가 된 Promise를 반환하는 함수

하나라도 settled상태가되면, 그 Promise와 같은 데이터와 상태를 가진 Promise객체를 반환한다.

requestData()가 3초 안에 안끝난다면, race가 반환하는 Promise는 rejected상태가되는것

3초안에 끝난다면, fulfilled상태인 Promise객체가 반환될것

function requestData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

Promise.race([
  requestData(),
  new Promise((_, reject) => setTimeout(reject, 3000)),
])
  .then(data => console.log('fulfilled', data))
  .catch(error => console.log('rejected'));
  //fulfilled 10

Promise 객체가 pending상태이면, fulfilled또는 rejected상태로 변경될 수 있는데

pending 상태가 아닌경우면, 상태와 데이터가 변경되지않는다.

settled상태가 되면, 그 상태를 유지하는 Promise의 성질을 이용해서 데이터를 캐싱하는 용도로 사용할 수 있다.

getData()함수가 Promise객체를 이용해서 데이터 캐싱을 구현한 예인데

getData()함수를 처음에 호출하면, requestData 함수가 실행이 될것이다.

처음에는 Promise 객체가 만들어지고, 그것이 캐싱이 될 것이다.

그리고 두번째 호출하게 된다면, 

캐싱되었던 Promise객체가 그대로 반환이 될것이다.

데이터를 가져오는 작업에 실패하는 경우가 고려되지않았지만, 이렇게 Promise를 통해서 캐싱을 구현할 수 있다는것을 볼 수 있다.

function requestData() {
  console.log('called requestData');
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

let cachedPromise;
function getData() {
  cachedPromise = cachedPromise || requestData();
  return cachedPromise;
}
getData().then(v => console.log(v));
getData().then(v => console.log(v));
/*
called requestData
10
10
*/

then안에서 return 키워드를 입력하는 것을 깜빡하기 쉬운데, 

이 경우에는 데이터가 undefined가 될것이다.

그래서 마지막에 보면, undefined가 출력이 된다.

Promise.resolve(10)
  .then(data => {
    console.log(data);
    Promise.resolve(20);
  })
  .then(data => {
    console.log(data);
  });
/*
10
undefined
*/

그래서 아래와 같이 return을 넣고 수정하면 다음과 같이 정상적으로 출력된다.

Promise.resolve(10)
  .then((data) => {
    console.log(data); //10
    return Promise.resolve(20);
  })
  .then((data) => {
    console.log(data); // 20
  });

then 메서드는 기존 객체를 수정하지않고, 새로운 Promise 객체를 반환한다.

이를 인지하지 못하고 코드를 작성하면, 다음과 같은 실수를 범할수있다.

여기서 then을 사용한 다음에 이전 Promise 객체를 반환하고있다.

  p.then(data => {
    return data + 20;
  });
  return p;
}

function requestData() {
  const p = Promise.resolve(10);
  p.then(data => {
    return data + 20;
  });
  return p;
}
requestData().then(v => {
  console.log(v);
});
// 10

그래서 버그를 삭제하기 위해서 다음과 같이 return p를 제거하고

return의 위치를 바꾸면 된다.

function requestData() {
  const p = Promise.resolve(10);
  return p.then((data) => {
    return data + 20;
  });
}
requestData().then((v) => {
  console.log(v); //30
});

Promise 를 중첩해서 사용하면, 콜백 패턴처럼 코드가 복잡해지므로 사용을 권하지않는다.

function requestData1() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2(param) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(param + 20);
    }, 1000);
  });
}

requestData1()
  .then(result1 => {
    return requestData2(result1).then(result2 => {
      console.log({ result2 });
    });
  })
  .then(() => console.log('end'));
/*
{ result2: 30 }
end

*/

그래서 위와같이 중첩하기보다는 아래와 같이 중첩하는것이 좋다.

function requestData1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2(param) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(param + 20);
    }, 1000);
  });
}
// 이렇게!!!!!!!!!!!!!!!!!
requestData1()
  .then((result1) => {
    return requestData2(result1);
  })
  .then((result2) => {
    console.log({ result2 });
  })
  .then(() => console.log("end"));

// requestData1()
//   .then((result1) => {
//     return requestData2(result1).then((result2) => {
//       console.log({ result2 });
//     });
//   })
//   .then(() => console.log("end"));

 

여기서는 then으로 두개의 데이터를 사용하려하고있다.

하지만 지금은 한개의 데이터만넘어올 예정이다.

requestData1()
  .then(result1 => {
    return requestData2(result1);
  })
  .then((result1, result2) => {
    console.log({ result1, result2 });
  });
function requestData1() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2(param) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(param + 20);
    }, 1000);
  });
}

requestData1()
  .then(result1 => {
    return requestData2(result1);
  })
  .then((result1, result2) => {
    console.log({ result1, result2 });
  });
// { result1: 30, result2: undefined }

두번째 then에서 두개의 데이터를 참조하려면 어떻게 해야할까?

1. Promise를 중첩하는것!

우선 두번째 데이터까지 받아놓고, 배열형식으로 두개를 반환을 하면된다.

function requestData1() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function requestData2(param) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(param + 20);
    }, 1000);
  });
}

requestData1()
  .then(result1 => {
    return requestData2(result1).then(result2 => {
      console.log({ result2 });
      return [result1, result2];
    });
  })
  .then(([result1, result2]) => {
    console.log({ result1, result2 });
  });
  /*
  { result2: 30 }
{ result1: 10, result2: 30 }
*/

Promise.all함수를 사용하면, Promise를 중첩하지않아도, 다음과 같이 해결할 수 있다.

Promise.all함수로 입력하는 배열에 Promise가 아닌 값을 넣으면 

그 값 그대로 fulfilled상태로 Promise 객체처럼 처리가 된다.

requestData1()
  .then(result1 => {
    return Promise.all([result1, requestData2(result1)]);
  })
  .then(([result1, result2]) => {
    // ...
  });

Promise를 비동기가 아닌 동기코드와 같이 사용할 때는 예외 처리에 신경을 써야한다.

동기코드와 같이 사용할 때는 예외 처리에 신경을 써야한다.

동기함수에서 예외가 발생하는 경우에는 예외를 처리하는 곳이 없어서 문제가될 수 있다.

여기서는 catch쪽에서 처리가 안겠지

function doSync() {
  throw new Error('some error');
}
function requestData() {
  doSync();
  return fetch()
    .then(data => console.log(data))
    .catch(error => console.log(error));
}

doSync함수가 반드시 fetch전에 호출되어야하는게 아니라면, then 메서드 안쪽으로 넣어주는 것이 좋다고한다.

이렇게하면, doSync안에서 일어난 예외처리는 catch메서드에서 처리가 될것이다.

 

 

function requestData() {
  return fetch()
    .then(data => {
      doSync();
      console.log(data);
    })
    .catch(error => console.log(error));
}

만약 doSync함수가 fetch전에 호출되어야한다면, 

fulfilled상태인 Promise 객체를 만들고, doSync 호출

이러면, doSync안의 예외는 catch내에서 처리가 될것이다.

 

function requestData() {
  return Promise.resolve()
    .then(doSync)
    .then(fetch)
    .then(data => {
      console.log(data);
    })
    .catch(error => console.log(error));
}

 

async await

비동기 프로그래밍을 동기 프로그램으로 작성하는데 특화된 문법

Promise then 메서드보다 가독성이 좋다

하지만, Promise를 완전히 대체하는것은 아니다.

Promise는 비동기 상태를 값으로 다룰 수 있기 때문에 async await보다는 큰 개념이다.

Promise는 객체로 존재하지만,  async await는 함수에 적용되는 개녕이다.

 

async await가 반환하는 객체는하상 promise 객체이다. 그래서 then 메소드를 사용할 수 있다.

async function getData() {
  return 123;
}
getData().then(data => console.log(data));
//123

async await 내부에서 반환하는 값이 Promise라면, 그 상태와 데이터를 그대로 반환한다.

지금은 fulfilled 상태인 Promise객체를 반환하고 있는데, 그래서 'fulfilled 123'를 출력한다.

 

resolve를 rejected로 한다면, catch부분을 반환한다.

async function getData() {
  return Promise.resolve(123);
}
getData()
  .then(data => console.log('fulfilled', data))
  .catch(data => console.log('rejected', data));
// fulfilled 123

async await 내부에서 await 키워드를 사용할 수 있다.

await 키워드 오른쪽에 Promise 객체를 입력하면,

Promise 가 settled상태가 될 때까지 기다린다.

함수가 반환하는 값이 Promise 객체이다.

Promise 가 fulfilled 상태가 되면, 그 데이터를 왼쪽에 있는 변수(data1)에 저장을 한다.

그렇게 첫번째 비동기 처리가 끝이나면, 그 아래에 있는 코드가 실행이된다.

그래서, Promise 가 settled상태가 될 때까지 기다린다. 그리고 독같이~

function requestData(value) {
  return new Promise(resolve =>
    setTimeout(() => {
      console.log('requestData:', value);
      resolve(value);
    }, 1000),
  );
}
async function printData() {
  const data1 = await requestData(10);
  const data2 = await requestData(20);
  console.log(data1, data2);
}
printData();
/*
requestData: 10
requestData: 20
10 20
*/

await 오른쪽에 있는 Promise  객체가 rejected상태가 되면, async await함수는 그 Promise 의 상태와 데이터를 그대로 반환한다.

async function getData() {
  console.log('getData 1');
  await Promise.reject(); //rejected상태인 Promise 객체 입력
  console.log('getData 2');
  await Promise.resolve();
  console.log('getData 3');
}
getData() //함수형태 >> rejected상태인 Promise 객체 >> catch부분의 로그 출력
  .then(() => console.log('fulfilled'))
  .catch(error => console.log('rejected'));
/*
getData 1
rejected
*/

함수는 await Promise.reject()에서 끝나기 때문에, 아래의 코드는 실행이 되지않는다.

 

await 는 async await  함수내부에서만 사용될 수 있다.

일반함수내에서 사용하면 에러가 발생한다.

function getData() {
  const data = await requestData(10); 
  console.log(data);
}

 async await  Promise는 비동기 프로그램을 동기방식으로 작성할 수 있게 해준다.

다음은 async await  Promise 를 비교하기 위해 같은 기능을 각각의 방식으로 구현한 것이다.

 async await  는 then 메서드를 사용할 필요가 없기 때문에, 좀 더 간결

function getDataPromise() {
  asyncFunc1()
    .then(data => {
      console.log(data);
      return asyncFunc2();
    })
    .then(data => {
      console.log(data);
    });
}
async function getDataAsync() {
  const data1 = await asyncFunc1();
  console.log(data1);
  const data2 = await asyncFunc2();
  console.log(data2);
}

비동기 함수간의 의존성이 높아질수록  async await  Promise의 가독성 차이는 선명하다.

 async await 는 복잡해도 직관적이다.

function getDataPromise() {
  return asyncFunc1()
    .then(data1 => Promise.all([data1, asyncFunc2(data1)]))
    .then(([data1, data2]) => {
      return asyncFunc3(data1, data2);
    });
}
async function getDataAsync() {
  const data1 = await asyncFunc1();
  const data2 = await asyncFunc2(data1);
  return asyncFunc3(data1, data2);
}

여러 비동기함수를 병렬로 처리하는 방법을 알아보자.

근데, 두 함수사이에 의존성이 없다면 동시에 실행하는것이 좋겠지!

async function getData() {
  const data1 = await asyncFunc1();
  const data2 = await asyncFunc2();
  // ...
}

Promise객체는 생성과 동시에 비동기 코드가 실행이된다.

따라서 두개의 Promise객체를 먼저 생성을 하고, await키워드를 나중에 사용하면, 병렬로 실행되는 코드가 된다.

 

그래서 두개의 비동기처리가 동시에 진행된다(처리중1, 처리중2)

그래서 1초가 걸린 후 , 마지막줄이 출력됨(병렬이기에)

function asyncFunc1() {
  return new Promise(resolve => {
    console.log('처리 중1');
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}
function asyncFunc2() {
  return new Promise(resolve => {
    console.log('처리 중2');
    setTimeout(() => {
      resolve(20);
    }, 1000);
  });
}

async function getData() {
  const p1 = asyncFunc1();
  const p2 = asyncFunc2();
  const data1 = await p1;
  const data2 = await p2;
  console.log({ data1, data2 });
}
getData();
/*
처리 중1
처리 중2
{ data1: 10, data2: 20 }
*/

다음과 같이 진행했다면, 2초가 걸렸을 것이다. 하나씩 하고 진행되니까.

async function getData() {
  cosnt data1 = await asyncFunc1();
  cosnt data2 = await asyncFunc2();
  console.log({ data1, data2 });
}
getData();

Promise.all을 사용하면 조금 더 간단하게 표현할 수 있다.

이 또한 병렬로 처리하면된다. 배열로 입력하였기에

async function getData() {
  const [data1, data2] = await Promise.all([asyncFunc1(), asyncFunc2()]);
  // ...
}

예외 처리

try, catch문으로 하면좋은데

여기서는 비동기함수(await)와 동기함수(return) 모두가 호출되고 있는데, 

두 함수에서 발생하는 모든 예외가 catch문에서 처리가 된다.

이 함수가 async await함수가 아니었다면, doAsync함수에서 발생하는 예외는 catch문에서 처리가 되지않는다.

이는 doAsync함수의 끝나는 시점을 알 수 없기 때문

async function getData() {
  try {
    await doAsync();
    return doSync();
  } catch (error) {
    console.log(error);
    return Promise.reject(error);
  }
}

Thenable은 Promise처럼 동작하는 객체이다.

async await는 ES6의 Promise가 아니더라도 then메서드를 가진 객체를 Promise처럼 취급한다.

then 매서드를 가진 객체를 Thenable이라고 부른다.

그래서 이 클래스로 생성하나 객체는 Thenable이다.

그래서 await 오른쪽에 Thenable을 사용할 수 있다.

class ThenableExample {
  then(resolve, reject) {
    setTimeout(() => resolve(123), 1000);
  }
}
async function asyncFunc() {
  const result = await new ThenableExample();
  console.log(result);
}
asyncFunc();
// 123