28. Node.js 입문주차 - 동기 & 비동기 (항해 12일차)

코이그·2023년 4월 14일

항해99

목록 보기
27/54

동기(Sync) vs. 비동기(Async)

  • 동기: 실행된 코드의 결과가 나올 때까지 대기하는 것. (놀이기구의 탑승/하차: 한번에 모두가 탑승/하차)
  • 비동기: 실행된 순서와 상관 없이 결과가 나오는 것. (식당의 입/퇴장: 사람마다 입/퇴장이 다름)

Blocking Model vs. Non-Blocking Model

  • Blocking model: 실행 제어권을 다른 곳에 넘기지 않으므로 다른 작업을 수행하지 못하고 현재 실행이 종료될 때까지 대기한다. (동기)
  • Non-blocking model: 실행 제어권을 다른 곳에 넘겨 다음 코드가 실행될 수 있다. (비동기)

JavaScript는 비동기와 non-blocking 방식이기 때문에 현재 실행 중인 코드가 끝나지 않아도 다음 코드를 실행한다.

async function main() {
  function first() {
    // 1
    console.log("first");
  }
  // 1초 뒤에 first 호출
  setTimeout(first, 1000);
  // 2
  console.log("middle");
  // 3
  console.log("last");
}

main();

이 코드를 실행했을 때 동기적 언어라면 출력 순서가 1초 후 1 -> 2 -> 3이 되지만
자바스크립트는 async, non-blocking이기 때문에 1에서 대기하지 않고 2 -> 3, 그리고 1초 뒤에 1이 출력된다.

Promise

JavaScript에서 비동기적 처리를 동기적으로 처리할 수 있게 돕는 객체이다.

3가지 상태

  • 대기(pending): 이행/거부되지 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료된 상태.
  • 거부(rejected): 연산이 실패한 상태.

동작 원리

  1. 새로운 Promise 객체를 생성하고 그 안에서 executor 함수가 자동으로 실행된다. (상태: 대기)
  2. 객체 내부에서 resolve 또는 reject가 호출되면 해당 promise의 상태가 바뀐다.
    2-1. resolve: (상태: 성공) 객체의 then 메소드가 호출된다.
    2-2. reject: (상태: 실패) 객체의 catch 메소드가 호출된다.

예시

resolve-then

async function main() {
  // 1
  const timerPromise = new Promise((resolve, reject) => {
    // 이곳에 정의된 함수가 executor
    // 1초 후 setTimeout 내부 코드 실행
    setTimeout(() => {
      console.log("First");
      resolve("Resolve!"); // resolve 호출 시 매개변수 전달 가능
    }, 1000);
  });

  // 이 시점에서 timerPromise는 Fulfilled Promise라고 부를 수 있다.

  // 2
  // resolve에서 전달한 매개변수를 받을 수 있음
  timerPromise.then((data) => {
    console.log("Middle");
    console.log("Last");
    console.log(data);
  });

  // 출력결과:
  // (1초 후)First
  // Middle
  // Last
  // Resolve!
}

main();
  1. timerPromise라는 Promise 객체 생성. (상태: 대기)
  2. timerPromise 객체 내부에서 reslove()가 실행이 되면 상태가 이행으로 바뀌고 then 메소드가 실행됨.

reject-catch

async function promiseCatch() {
  // 1
  const errorPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("First");
      // 위의 예제와 똑같은데 resolve 대신에 reject 호출
      reject("Error!!");
    }, 1000);
  });

  // errorPromise 객체 내부에서 reject 또는 resolve가 실행되었을 때 상태가 바뀌는데, resolve일 때는 then 메소드, reject일 때는 catch 메소드가 실행됨
  errorPromise
    .then(() => {
      console.log("Middle");
      console.log("Last");
    })
    .catch((error) => {
      console.log("에러 발생!", error);
    });
}

// 출력 결과:
// (1초 후)First
// 에러 발생! Error!!
  1. timerPromise라는 Promise 객체 생성. (상태: 대기)
  2. timerPromise 객체 내부에서 reject()가 실행이 되면 상태가 실패로 바뀌고 catch 메소드가 실행됨.

resolve

  1. Promise.resolve() 함수 이용
    Promise가 값을 반환하는 경우 반환되는 값은 항상 Promise로 감싸져 있다.
const promise = Promise.resolve('first');
promise.then((value) => { console.log(data); });
// 출력 결과: first
  1. Promise.then()으로 함수형 프로그래밍 체험하기
const firstPromise = Promise.resolve('first');
// console.log의 함수 호출이 아니라 함수 자체를 넘겼기 때문에 resolve로부터 then이 넘겨받은 'first'가 then의 매개변수인 함수의 매개변수로 들어감 
firstPromise.then(console.log);
// 출력 결과: first
  1. Promise.then()으로 함수형 프로그래밍 체험하기2
const countPromise = Promise.resolve(0);
function increment(value) {
  return value + 1;
}
const resultPromise = countPromise
  .then(increment)					// 처음 넘겨받은 인자에 대해서 increment 실행
  .then(increment)					// 한번 더
  .then(increment);					// 한번 더
resultPromise.then(console.log);	// 최종 값 출력
// 출력 결과: 3

비동기 함수

특징

일반적인 함수 혹은 화살표 함수와 아주 비슷하지만 2가지 차이점이 있다.

  1. 비동기 함수의 결과 값은 항상 Promise 객체로 resolve된다.
  2. 비동기 함수 안에서만 await 연산자를 사용할 수 있다.
// 일반 함수
async function func() { return Promise.resolve("data"); }
async function func2() { return "data"; }

// 익명 함수
async function() {}

// 화살표 함수
async () => {}

func();
func2()
출력 결과: Promise { 'data' } (func, func2의 결과값은 똑같다. 자동으로 Promise 객체로 resolve 된다)

왜 비동기 함수를 쓰는가?

위의 Promise.resolve 메소드 실습을 하면서 비동기적 처리 방법을 익혔는데, 3번째 예시같은 경우 then을 여러번 중첩하는 등 콜백 함수를 지나치게 많이 쓰면 콜백 지옥이라고 할 정도로 가독성과 효율이 좋지 않게 된다.

이를 방지하는 것이 비동기 함수의 await 연산자이다. await 연산자를 사용하면 콜백 함수를 여러 번 사용하지 않고 비동기적 처리를 할 수 있기 때문에 코드가 간결해진다.

또한 new Promise(executor)로 Promise 객체를 직접 생성하면 executor가 바로 실행되는데, 비동기 함수를 사용하면 함수를 호출하기 전까지 Promise가 생성되지 않는다.

await

const data = await "value";
  1. value가 Promise인 경우 성공 혹은 실패 상태가 될 때까지 함수의 실행을 중단하고 기다린다.
  2. value가 Promise가 아닌 경우 await를 하지 않고 바로 할당한다.
// 1
function setTimeoutFunc(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(time + "ms가 지났습니다.");
      resolve();
    }, time);
  });
}

async function main() {
  console.log("시작되었습니다.");
  // 2
  await setTimeoutFunc(1000);
  console.log("종료되었습니다.");
}

main();

// 출력 결과: 
// 시작되었습니다.
// 1000ms가 지났습니다.
// 종료되었습니다.
  1. setTimeoutFunc는 Promise 객체를 반환한다.
  2. await 연산자가 없으면 시작->종료->1000ms~ 가 출력이 되는데, await 연산자를 붙여줌으로써 setTimeoutFunc의 반환값(Promise 객체)이 성공/실패 상태가 될 때까지 (resolve/reject가 호출될 때까지) 기다렸다가 아래를 실행하게 된다.
profile
COYG🔴⚪

0개의 댓글