티스토리 뷰

스터디/하브루타 스터디

3. 비동기

곤이씨 2021. 4. 12. 22:18

내 질문에 대한 정리

Q1. Promise.all에 대해 설명해주시고 어떤 상황에서 쓰면 좋은지 예시를 들어주세요.

Promise.all은 여러개의 promise를 병렬적으로 처리하고 싶을 때 사용한다.

여러개의 url request가 필요한 경우에는 단순하게 await을 여러번 사용할 수 있다. 하지만 이는 동기적으로 정보를 요청하기 때문에 시간이 오래걸린다. 하지만 Promise.all을 사용하면 병렬적으로 여러개의 요청을 보내고 모든 응답이 정상적으로 온 경우에 다음 행동을 시작할 수 있다. 여러개의 Http request를 보내는 경우에 사용하면 효율적이다.

const sleep = (delay) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
      console.log(delay)
    }, delay)
  })
}

const test1 = async () => {
  await sleep(3000)
  await sleep(2000)
  await sleep(1000)
}

const test2 = async () => {
  Promise.all([sleep(3000), sleep(2000), sleep(1000)])
}

test1() // 3초 후 3000, 2초 후 2000, 1초 후 1000 => total 6초
test2() // 1초 후 1000, 1초 후 2000, 1초 후 1000 => total 3초

const sleep = (delay) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(delay)
    }, delay)
  })
}

(async () => {
  const a = sleep(3000)
  const b = sleep(2000)
  const c = sleep(1000)
  console.log(await c)
  console.log(await b)
  console.log(await a)
})() // 1초 후 1000, 1초 후 2000, 1초 후 3000 => total 3초 = Promise.all과 똑같이 병렬적으로 처리

Q2. ES6에 도입된 제너레이터에 대해 설명해주세요.

ES6에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수이다. 제너레이터와 일반 함수의 차이는 다음과 같다.

  1. 제너레이터 함수는 함수 호출자에게 제어권을 양도(yield)할 수 있다.
  2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
  3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.

제너레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고받을 수 있다. 이러한 특성을 사용하면 프로미스를 사용한 비동기 처리를 동기 처리처럼 구현할 수 있다.

아래의 코드는 generator를 이용하여 custom async 함수를 만든 내용.

const async = generatorFunc => {
  const generator = generatorFunc() // 클로저

  const onResolved = arg => {
    const result = generator.next(arg)

    return result.done ? result.value : result.value.then(res => onResolved(res)) 
  } // 비동기 처리 및 재귀함수로 다음 yield문 실행

  return onResolved
}

(async(function* fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1'

  const response = yield fetch(url)
  const todo = yield response.json()
  console.log(todo) // {userId: 1, id: 1, title: "delectus aut autem", completed: false}
})())

ES6의 제너레이터를 사용한 비동기 프로그래밍 : NHN Cloud Meetup

Q3. await과 then을 병기하는 것에 대해 어떻게 생각하시나요

우선, await과 then, catch를 같이 사용해도 전혀 문제 없다. 결국 Promise chaning의 최종결과가 리턴되기 때문이다. 따라서 await과 then, catch를 섞어쓰는 것은 전적으로 개인의 취향에 달려있다.

보통 우테코 리뷰어님들은 혼용해서 사용하는 것을 지양하시지만 의외로 스택오버플로우에서는 꽤나 많은 사람들이 섞어서 사용한다고 한다.

What's wrong with awaiting a promise chain?

Correct Try...Catch Syntax Using Async/Await

Q4. async/await을 사용하는 경우, 일반적으로 throw를 통해 에러를 핸들링하는 것으로 알고 있습니다. throw의 에러 전파 방향을 설명해주세요.

const gonnie = () => {
  throw Error('gonnie에서 발생한 에러');
}

const dito = () => {
  gonnie();
}

const peter = () => {
  dito();
}

try {
  peter();
} catch (err) {
  console.log(err) // Error: 'gonnie에서 발생한 에러'
}

이처럼 throw된 에러를 캐치하지 않으면 호출자 방향으로 전파된다. throw된 에러를 어디에서도 캐치하지 않으면 프로그램은 강제 종료된다. 주의할 것은 비동기 함수인 setTimeout이나 프로미스 후속 처리 메서드의 콜백 함수는 호출자가 없다는 것이다. setTimeout이나 프로미스 후속 처리 메서의 콜백 함수는 태스크 큐나 마이크로태스크 큐에 일시 저장되었다가 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행된다. 이때 콜 스택에 푸시된 콜백 함수의 실행 컨텍스트는 콜 스택의 가장 하부에 존재하게 된다. 따라서 에러를 전파할 호출자가 존재하지 않는다.

try {
  setTimeout(() => {
    throw new Error('Error!')
  }, 1000)
} catch (err) {
  console.log(err)
} // setTimeout의 콜백함수는 콜 스택에 아무것도 없을 때 호출되므로 에러를 전파할 호출자가 존재하지 않는다.
    // 따라서 catch문이 에러를 검출하지 못하므로 프로그램이 죽어버린다.
  // 비동기 함수의 콜백 함수를 호출한 것은 비동기 함수가 아니기 때문에 try catch로 에러를 캐치할 수 없다.

const sleep = (delay) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('rejected')
      console.log(delay)
    }, delay)
  })
}

const test = async () => {
  try {
    await sleep(1000)
  } catch (err) {
    console.log(err) //rejected
  } 
}

test()
// 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호줄자가 명확하다.
// 따라서 에러가 호출자로 전파되기 때문에 try catch문을 사용할 수 있다.

+) reject와 throw의 차이

reject와 throw 모두 error를 전파하고 에러 핸들링이 되지 않는 경우 프로그램을 죽인다.

그러나 둘은 control flow를 종료하냐 아니냐의 차이를 가진다.

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));
// 출력 결과: REJECTED

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));
// 출력 결과: ALWAYS REACHED -> REJECTED

 

디토 질문에 대한 정리

Q1. 태스크 큐와 마이크로태스크 큐의 차이점이 궁금합니다.

  • 태스크 큐는 setTimeout, setInterval, setImmediate 등이 들어간다.
  • 마이크로 태스크 큐에는 process.nextTick, Promise, Object.observe, MutationObserver 등이 들어간다.
  • 우선순위는 마이크로 태스크 큐가 태스크 큐보다 높다.

Q2. Promise는 비동기로 동작하는데 async await은 Promise 기반임에도 불구하고 동기적으로 실행될 수 있는 이유가 궁금합니다.

제너레이터를 사용하여 비동기를 동기적으로 처리한다.

Medium

Q3. async await은 동기적으로 실행되기 때문에 아래와 같이 서로 연관이 없는 코드가 동작할 때, 모든 코드가 settled될 떄까지 기다린 후에야 다음 코드를 실행하는데, 이런 경우에는 어떻게 하는 것이 좋을까요?

const test = async () => {
  const first = await new Promise((resolve) => setTimeout(() => resolve(1), 3000));
  const second = await new Promise((resolve) => setTimeout(() => resolve(2), 2000));
  const third = await new Promise((resolve) => setTimeout(() => resolve(3), 1000));

  console.log(first, second, third);
};

test(); // 6초 후에 1 2 3

A. Promise.all을 사용한다.

 

피터 질문에 대한 정리

Q1. Promise.race의 동작에 관해, 그리고 어떤 경우에 사용할 수 있는지에 대해 설명해주세요.

  • Promise.race는 Promise.all 메서드와 동일하게 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달 받는다. Promise.race 메서드는 Promise.all 메서드처럼 모든 프로미스가 fulfilled 상태가 되는 것을 기다리는 것이 아니라 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환한다.

Medium

Q2. 최상위 레벨의 코드에서 await를 사용할 수 있는 방법이 있을까요?

  • await을 사용하려면 async 함수를 사용해야하기 때문에 없다고 생각한다.
  • but 최근 Top level await을 도입하려는 시도가 있다.

Top-level await

Q3. task queue, microtask queue, animation frames에 대해 설명해주세요

Queue에는 종류가 있다. 바로 Task Queue Microtask Queue Animation frames 이다.

Event Loop는 Microtask Queue → Animation frames → Task Queue 순으로 우선순위를 두어 작업을 가져간다.

JavaScript의 비동기 처리 과정

Q4. 브라우저 렌더링에 영향을 미치는 task에는 어떤 것들이 있는지 궁금합니다.

브라우저의 리플로우와 리페인팅이 실행되는 경우

  • 자바스크립트에 의한 노드 추가 또는 삭제
  • 브라우저 창의 리사이징에 의한 뷰포트 변경
  • HTML 요소의 레이아웃에 변경을 발생시키는 width/height, margin, padding, border, display, position, top/right/bottom/left 등의 스타일 변경

밑의 사이트에서 어떤 CSS 속성이 특정 브라우저의 엔진에서 리렌더링과 리플로우를 발생시키는지 확인할 수 있다.

CSS Triggers

Q5. 시각적 효과에 setTimeout이나 setInterval 대신에 requestAnimationFrame을 권장하는 이유가 궁금합니다.

  • setTimeout이나 setInterval은 프레임을 신경쓰지 않고 동작한다.

자바스크립트 애니메이션 - requestAnimationFrame 활용하기

기타 인사이트

const myPromise = Promise.resolve('Promise !');
const func1 = () => {
   myPromise.then(res => console.log(res + ' 1'));
   setTimeout(() => console.log('Timeout 1!'), 0);
   console.log('Last line 1!');
}

const func2 = async () => {
   console.log(await myPromise + ' 2');
   setTimeout(() => console.log('Timeout 2!'), 0);
   console.log('Last line 2!');
}func1();
func2();// Output:
// Last Line 1! Promise 1! Promise 2! Last Line 2! Timeout 1! Timeout 2!

 

from 카일

  1. func1 - myPromise ⇒ micro task queue
  2. func1 - setTimeout ⇒ macro task queue
  3. console.log('Last line 1!')
  4. func2 - myPromise ⇒ micro task queue (후순위 로직 await)
  5. 스크립팅 문 종료 (한 개의 macro task 종료)
  6. microtask 처리 (func1 myPromise, func2 myPromise)
  7. func2 - setTimeout ⇒ macro task queue
  8. console.log('Last line 2!')
  9. macrotask 처리 (func1 setTimeout, func2 setTimeout)

 

기타 자료

이벤트 루프와 매크로·마이크로태스크

JavaScript Visualized: ⭐️🎀 Promises & Async/Await

[Node.js] await vs return vs return await: 비동기 이해하기

// 비동기 함수 (1초 기다린 후 'yay' or '에러')
async function waitFunction() {
  await new Promise((resolve, reject) => setTimeout(()=>{
    console.log('promise')
    resolve()}, 1000));
  return 'yay';
}

// 1. 그냥 실행
async function foo1() {
  try {
    waitFunction();
  }
  catch (e) {
    return 'caught';
  }
}

// 2. return 실행
async function foo2() {
  try {
    return waitFunction();
  }
  catch (e) {
    return 'caught';
  }
}

(async () => {
    console.log('foo1', await foo1())
    console.log('foo3', await foo2())
})()

//

'스터디 > 하브루타 스터디' 카테고리의 다른 글

6. This  (3) 2021.05.02
5. 변수와 데이터  (0) 2021.04.25
4. 함수  (0) 2021.04.18
2. CSS selector  (0) 2021.04.04
1. 이벤트  (0) 2021.04.04