딥다이브 스터디 45, 46(프로미스, 제너레이터, async-await)

김영현·2023년 10월 16일
0
post-thumbnail

동기-비동기, 블로킹-논블로킹

동작원리에서 봅시다

프로미스

동기, 비동기, 블로킹, 논블로킹의 개념을 알게되었다면, 프로미스의 시간이다.
콜백함수는 비동기 함수내부의 값을 꺼내쓰기위해 고안되었다.
프로미스는 프로그래머스 데브코스 강의시간에도 배웠었지만, 콜백지옥을 해결하기 위한 방법으로 도입되었다.

step1(value, () => {
	step2(value2, () =>{
    	step3...
    })
})

한 두줄이면 괜찮지만, 계속 쌓이다보면 미사일처럼 발사하는 뎁스를 보여줄 것이다.
또한, 콜백패턴은 에러처리가 곤란하다.

try{
	setTimeout(()=> {throw new Error("what!")}, 1000)
} catch(e){
  //에러가 캐치되지 않는다.
	console.log(e)
}

try-catch는 동기적으로 작동하고, setTimeout은 비동기적으로 작동한다.
따라서 try-catch가 끝난 이후, 에러가 나오게된다

프로미스의 3가지 상태

  • fullfiled: 비동기 처리 성공, resolve호출
  • rejected: 비동기 처리 실패, reject호출
  • pending: 아직 수행되지 않음.

=> resolvereject에 던진 콜백을 호출할때 상태가 결정된다.

프로미스 처리

Promise.prototype.then()문으로 처리한다.

newPromise.then((resolve, reject) => ...)
//둘다 처리 가능하다.

참고로 promise.catch, promise.finally도 호출 가능하다.
각각 reject시 호출, 무조건 한 번 호출이다.

...then().catch().finally()

콜백지옥 해결....?

프로미스가 나왔을땐 프로미스 체이닝으로 콜백지옥을 해결했다.

then(...)
.then(...)
.then(...)
.catch(...)

then,catch,finally메소드는 프로미스 객체를 반환하기에 무한정 체이닝이 가능하다.
콜백 지옥은 사라졌으나, 콜백 패턴 자체는 가독성이 떨어진다.
이를 위해 마치 동기처럼 움직이는 듯한 비동기처리 방식인 async - awaitES8에 추가되었따.(46챕터에서 설명)

Promise의 정적 메서드

여기서 보자

마이크로 태스크 큐

마이크로 태스크 큐 부분에서 설명했지만, 간단하게 짚고넘어간다.
콜백함수가 저장되는 태스크 큐보다 우선순위가 높은 큐.
프로미스의 후속처리 콜백들은 죄다 여기 들어간다.


fetch

이 역시 강의를 보며 따로 정리했었지만, 중요한 부분만 다시 설명.
HTTP에러는 거르지 못한다. 따라서 res.ok같은 상태를 체크해야한다.
이를 보완해서 나온 것이 axios. HTTP에러가 나와도 reject된다.


제너레이터

제너레이터는 코드 블록의 실행을 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수다
=> 블로킹(인터럽트)구나!

제너레이터의 특징은 다음과 같다.

  1. 호출자에게 함수 실행의 제어권을 양도(yield) 할 수있다.
    함수는 본래 호출자가 제어 불가능. 함수가 제어권을 가짐. 제너레이터는 제어권을 호출자에게 양도가능하다.
  2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
    호출자와 양방향으로 상태 소통이 가능하다.
  3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
    제너레이터를 호출하면 이터러블+이터레이터 객체를 반환한다

대충 예상해보면, 한줄씩 불러오는 함수인가...? 일단 예제를 보자.

제너레이터 객체

코드블록을 실행하는게 아닌 제너레이터 객체를 반환하는 제너레이터 함수
뭔지 보기나 하자

// 애스터리스크(별표, *)는 function키워드 바로 뒤에 선언한다.
function* genFunc(){
	yield 1;
  	yield 2;
  	yield 3;
}
const generator = genFunc();

제너레이터 객체는 이터레이터({value : 값, done:boolean} 형태의 객체를 리턴하는 next() 메서드를 가진 객체) 이면서 이터러블(반복 가능 객체)이다.
=> 이터레이터가 있어야 이터러블이 된다.

이는 곧, 순회할 수있고, 다음 값을 불러올 수 있다는 소리다.

...
genartor.next();// {value:1, done:false}

yield를 만날때까지 코드를 실행하고, 값을 던지고 suspend된다. 부르기전까진 멈춰있음.
또한 이때 함수의 호출자에게 제어권이 양도(yield)된다.

참고로 next()를 호출할때 인수를 던질 수 있다.
주의할 점은 던져진 인수가 yield표현식을 할당받는 변수에 할당된다.
=> 표현식의 평가 결과는 할당되지 않는다.

function genFunc(){
  //x에 yield1이 할당되지않는다.
	const x = yield 1;
  //두번째 호출시 10을 받아왔기에 할당되서 반환된다(20)
  	const y = yield (x+10);
  //x에 10, y에 20이 들어가 30이 반환된다
  	return x+y;
}
const generator = genFunc(0);
let res = generator.next();
console.log(res); // {value:1, done:false}
res = generator.next(10);
console.log(res) // {value:20, done:false}
res = genarator.next(20);
console.log(res) // {value:30, done:true}

여기만 봐도 좀 복잡한데...비동기 처리로 넘어가면 더 복잡해진다.

const async = generatorFunc => {
	const generator = generatorFunc();
  const onResolved = arg => {
  	const result = generator.next(arg);
    return result.done ? result.value : result.value.thne(res => onResolved(res))
  }
  return onResolved;
}
(async(function* fetchTodo(){
	const url = "apiUrl"
    const respone = yield fetch(url);
  	const todo = yield response.json();
  	console.log(todo);
})())
  1. 즉시실행함수로 async 함수를 호출하면 인수로 전달받은fetchTodo제너레이터 함수를 호출, onResolved를 반환. 참고로 이 함수는 클로저. 바로 즉시실행하여 next()를 호출한다.
  2. fetchTodo의 첫번째 yield를 만났다. done이 아니기에, onResolved는 재귀적으로 다시 호출한다.
    ...반복.
  3. done을 만나 종료된다.

=> 호출을 완료할때까지 코드블럭을 막아놓는다.

굉장히 복잡하다. 내부적으로 구현해야할 코드도 많고, 실수도 잦을 것 같다.
그래서 ES8때 출시된 빛과 소금친구들이 여기있노라.


async-await

얘도 프로미스 기반이다. 그런데, 매우 간단하다.
제너레이터를 사용하여 구현한 비동기=>동기 처리를 다시 구현해보자

async function fetchTodo(){
	const url = "api"
    const response = await fetch(url);
  	const todo = await response.json();
}
fetchTodo();

끝이다.
놀랍지 않은가?
await키워드를 만나면 동기처럼 동작하여, 결과가 나와야(reject or resolve) 다음 코드가 진행된다.
=> await키워드는 다음 코드를 블로킹한다. 그렇기에 동기적이다.

빛과 소금도 잘 사용해야한다. 남용하면 독이다.
연관 없이 개별적으로 수행되는 비동기처리는 Promise.all과 같은 메소드로 처리하자.

// await으로 모두 묶는 방식
async function foo(){
	const a = await new Promise(resolve => setTimeout(() => resolve(1), 3000))
	const b = await new Promise(resolve => setTimeout(() => resolve(2), 2000))
	const c = await new Promise(resolve => setTimeout(() => resolve(3), 1000))
    
}
foo()//약 6초소요...연관이 없는 작업끼리는 묶지 말자.

async function foo(){
  const res = await Promise.all([
     new Promise(resolve => setTimeout(() => resolve(1), 3000)),
     new Promise(resolve => setTimeout(() => resolve(2), 2000)),
     new Promise(resolve => setTimeout(() => resolve(3), 1000))
  ])
}
foo() // 약 3초소요.

이 친구들의 에러처리는 어떨까?
콜백함수를 인수로 받는 비동기함수가 아닌, 프로미스를 반환하는 비동기 함수는 명시적으로 호출이 가능하다.
=> 호출자가 명확하기에, 에러처리가 가능하다.

맨 마지막에 fetch가 HTTP통신 에러를 잡는다는데, 안잡힌다...res.ok로 체크하거나, axios를 쓰자!

profile
모르는 것을 모른다고 하기

0개의 댓글