callback - promise - async/await

김명성·2022년 4월 3일
0

자바스크립트

목록 보기
1/26

어느 정도 API를 사용하다 보니, 정형화 된 틀에 맞추어 무의식적인(무지성) async/await을 사용하게 되었다.
지금은 괜찮지만 Javascript 엔진의 동기/비동기 처리 방식에 대한 이해 없이
무의식적으로
(무지성) 비동기 처리 함수를 사용 하다가는 이후 작동 원리를 설명해야 할 때 과연 내가 당당히 말할 수 있을까라는 의구심이 들었고, Web API 호출뿐만 아니라 async,await을 응용할 수 있는 레벨로 끌어 올리고 싶어 Post를 작성하게 되었다.


callback / promise / async/await을 이해하기 위해 알아두어야 할 점.

1. 자바스크립트의 실행 컨텍스트 : 코드 평가와 코드 실행이 어떻게 이루어지는지
2. 비동기 프로그래밍: 동기/비동기 처리 방식


● 1. 간단히 알아보는 자바스크립트의 실행 컨텍스트

  • 자바스크립트 실행 컨텍스트는 4가지의 소스 코드를 갖고 있다.
    (4가지 소스코드는 전역 / 함수 / eval / 모듈 코드이지만 몰라도 오늘의 주제를 이해하는데에는 지장이 없다.)
  • 모든 소스 코드는 소스 코드의 평가소스 코드의 실행 과정으로 나누어 처리된다.

● 소스 코드의 평가 과정

  • 1. 먼저 실행 컨텍스트를 생성한다.
  • 2. 변수,함수 등의 선언문을 실행한다. (평가 과정에서는 값 할당이 이루어지지 않는다.)
값 할당이 이루어지지 않는다는 것은?
var x = 1;

위의 코드에서 할당 연산자와 그 값인 = 1은 소스코드의 평가 과정에서 평가되는 것이 아닌,
코드의 실행 과정에서 할당 된다.
즉 소스코드의 평가 과정에서는 var x만 실행 컨텍스트가 관리하는 스코프( 렉시컬 환경의 환경 레코드 )에 등록된다. 이 때의 값은 undefined이며 이것이 변수 호이스팅이다. const/let으로 선언된 코드는 선언적 환경 레코드로 따로 관리되어 호이스팅이 되지 않는 것처럼 보이게 처리한다.

  • 3. 생성된 식별자를 키로, 실행 컨텍스트가 관리하는 스코프에 등록

● 소스 코드의 실행 과정(런타임)

  • 1. 평가 과정 2번에서 선언된 변수,함수에 값을 할당하고 실행컨텍스트에 등록한다.
  • 2. 소스 코드의 실행은 위에서부터 아래로 순차적으로 실행되는데, 이때 컨트롤이 함수의 호출부를 만나면 코드 실행을 멈추고 코드 실행 순서를 변경하여 그 함수 내부로 진입한다.
  • 3. 함수 내부에서도 위의 소스코드 평가 과정을 진행한다.
  • 4. 소스코드 평가 종료 이후 실행(런타임) 단계에서, 함수 내부에 중첩함수가 존재한다면 다시 실행 과정의 1번부터 진행된다.
  • 5. 함수 내부의 중첩 함수의 실행문에 작성된 내용이 실행되고 종료된다.
  • 6. 상위 함수에 실행할 내용이 없다면 종료된다.
  • 7. 최상위에서 더 실행할 내용이 없다면 종료되고 최상위 실행 컨텍스트도 종료된다.
위 단계에서, 코드평가와 실행을 한 묶음으로 컨텍스트 단위로 나누어보면 전역 코드(최상위), 함수코드, 중첩 함수코드로 나누어 볼 수 있겠다. 또한 중첩 함수코드의 실행 컨텍스트는 제일 늦게 실행되고 제일 먼저 제거된다는 사실도 알 수 있다. 즉, STACK (후입선출) 구조이다.

즉, 실행 컨텍스트는 실행 컨텍스트 스택이라는 자료구조로 관리된다.

소스코드의 평가와 실행인 실행 컨텍스트 스텍을 깊게 들어가면 전역 실행 컨텍스트의 생성부터 함수 실행 컨텍스트, 각 실행 컨텍스트의 차이점과, 클로저,호이스팅 등 배울 부분이 많다.다만 오늘의 주제를 이해하기 위해서는 위와 같은 과정으로 코드가 순차적으로 실행되며, 그 실행을 주관하는 것은 실행 컨텍스트 스택으로 후입 선출의 구조이다. 정도까지만 이해한다면 주제를 이해하는데 무리가 없을 것이다.

● 1. 간단히 알아보는 자바스크립트의 동기 / 비동기 처리 방식

자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖는다. 하나의 실행 컨텍스트 스택을 갖는다는 것은 실행 컨텍스트 스택 내에서 실행 중인 실행 컨텍스트를 제외한 모든 실행 컨텍스트는 모두 실행 대기 상태라는 것이다. 대기 중인 실행 컨텍스트들은 실행 중인 실행 컨텍스트가 Pop으로 스택에서 제거 될 때 실행되기 시작한다.

자바스크립트 엔진은 싱글 스레드 방식으로 동작하며, 한번에 하나의 테스크만 실행할 수 있기에, 처리에 시간이 걸리는 테스크를 실행하는 경우에는 블로킹이 발생한다. 이와 같은 방식을 동기 처리 라고 한다. 이러한 동기 처리 방식은 실행 순서가 보장된다는 장점이 있지만, 앞의 테스크가 종료할 때까지 블로킹되는 단점이 있다.

이러한 단점을 극복하기 위해 자바스크립트 엔진에서는 비동기적으로 처리할 수 있게 몇가지를 정해놓았고, 비동기 처리는 Event loop와 task queue에 깊은 관계가 있다.

비동기 처리 방식으로 이루어지는 함수들

setTimeout , setInterval, HTTP요청, Event handler
Event handler는 커스텀 이벤트를 디스패치하거나 click,blur,focus와 같은 몇몇 메서드 등을 호출하면 해당 이밴트 핸들러가 task queue를 거치지 않고 동기 처리 방식으로 동작한다.

비동기 작업과 동기 작업이 이루어지는 과정

  1. 프로그램이 실행되면 전역 실행컨텍스트가 실행된다. (전역 코드의 평가 1단계 실행 컨텍스트의 생성 참조)
  2. 메인컨텍스트 실행 이후 가장 먼저 맞딱 뜨리는 함수의 호출부를 Call stack(실행컨텍스트)에 등록된다.
  3. 등록 이후, 함수 내부에 중첩 함수가 있다면 다시 내부함수로 들어가면서 콜스텍에 내부 중첩함수의 실행컨텍스트가 생성된다. (지금까지의 실행 컨텍스트는 삭제 되지 않고 계속 쌓여있다.)
  4. 실행 컨텍스트 내부에서 setTimeout과 같은 비동기 함수를 처리할 때에는 Call stack으로 쌓아두지 않고 Web APIs로 돌려놓는다. setTimeout에 2번째 인자로 등록된 delay time이 종료되면 setTimeout은 사라지고 그 안에 있던 콜백 함수가 callback Queue(또는 task queue,event queue)로 등록되고 Event Loop를 통해 Call stack(실행 컨텍스트)으로 옮겨간다
  5. Event loop은 Call stack을 계속 확인하면서, 전역 실행 컨텍스트만 남아 있을 때 Callback queue에 대기중인 콜백 함수를 Call stack으로 넘겨준다.

callback

function tasckA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}
function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 2000);
}

function taskC(a, cb) {
  setTimeout(() => {
    const res = a / 5;
    cb(res);
  }, 1000);
}

tasckA(4, 5, (a_res) => {
  console.log("A RESULT:", a_res);
  taskB(a_res, (b_res) => {
    console.log("B RESULT:", b_res);
    taskC(b_res, (c_res) => {
      console.log("C RESULT:", c_res);
    });
  });
});
  

promise

비동기 작업이 가질 수 있는 3가지 상태

  • Pending : 대기 상태
  • Fulfilled : 성공 - 해결 resolve
  • Rejected: 실패 - 거부 reject

어떤 함수가 결과값을 Promise 객체로 반환한다는 것은 그 함수가 비동기 작업을 하고 Promise 객체로 반환 받아 사용할 수 있다는 것을 뜻한다.

function isPositive(number, resolve, reject) {
  setTimeout(() => {
    if (typeof number === "number") {
      //성공 -> resolve
      resolve(number >= 0 ? "양수" : "음수");
    } else {
      //실패 -> reject
      reject("숫자를 입력 해주세요");
    }
  }, 2000);
}

function isPositiveP(number){
  const executor = (resolve,reject) => {
    setTimeout(() => {
      if (typeof number === "number") {
        //성공 -> resolve
        console.log(number);
        resolve(number >= 0 ? "양수" : "음수");
      } else {
        //실패 -> reject
        reject("숫자를 입력 해주세요");
      }
    }, 2000)
  }
  const asyncTask = new Promise(executor);
  return asyncTask;
}
const res = isPositiveP(101);
// then은 result 결과를 반환
res.then((res)=>{
  console.log("작업 성공 :",res)})
  //catch는 reject 결과를 반환 
  .catch((err)=>{
    console.log("Error발생:",err);
  })

// Promise를 사용해도 
function taskA(a, b) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      resolve(res);
    }, 2000);
  });
}

function taskB(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a * 2;
      resolve(res);
    }, 2000);
  });
}
function taskC(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a / 4;
      resolve(res);
    }, 2000);
  });
}

taskA(5, 2)
  .then((a_res) => {
    console.log("A RESULT:", a_res);
    // promise 객체를 반환한다.
    return taskB(a_res);
  })
  .then((b_res) => {
    console.log("B RESULT:", b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log("C RESULT:", c_res);
  });
//then chainning
// promise를 사용하면 콜백 함수와는 다르게 다른 작업을 중간에 끼워 넣을 수 있다.
// 가독성이 좋아지긴 하지만 여전히 chainning 방식이다.
// 이러한 방식은 다음과 같이 수정할 수 있다.


const bPromiseResult = taskA(5, 2).then((a_res) => {
  console.log("A RESULT:", a_res);
  // promise 객체를 반환한다.
  return taskB(a_res);
});

// 프라미스를 사용하면 비동기를 호출하는 코드와 결과를 처리하는 코드를
// 분리하여 작업을 따로 실행할 수 있다.
bPromiseResult
  .then((b_res) => {
    console.log("B RESULT:", b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log("C RESULT:", c_res);
  });

// taskA(3,4,(a_res)=>{
//   console.log("A RESULT:",a_res)
//   taskB(a_res,(b_res)=>{
//     console.log("B RESULT:",b_res);
//     taskC(b_res,(c_res)=>{
//       console.log("C RESULT:",c_res)
//     })
//   })
// })

async / await

function hello() {
  return "hello";
}
// async를 붙여주면 반환값은 Promise 이다. 타입체크 
// Promise를 리턴하고 있다. 즉 promise 객체를 반환한다.
// async를 붙이고 리턴하면, resolve를 return 값으로 갖는다.
async function helloAsync() {
  return "hello async";
}

console.log(hello())

helloAsync().then(res=> console.log(res));

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// async function helloAsync() {
//   return delay(3000).then(() => {
//     return "hello Async";
//   });
// }

// 위 함수를 await을 통해 간결하게 바꿀 수 있다.
async function helloAsync() {
  //await을 붙이므로써 비동기 함수를 동기 함수처럼 동작하게 한다
  
  await delay(3000);
  return "HELLO, ASYNC.";
}

async function main() {
  //await이 붙은 함수가 끝나기 전까지 아래에 있는 함수를 호출하지 않는다.
  await helloAsync().then((res) => console.log(res));
  console.log("All Done");
}
main();

// helloAsync().then((res) => console.log(res));
// API 호출 : 서버로부터 데이터를 요청하거나
//요청한 데이터의 데이터 응답을 받음
// API는 비동기로 호출한다 ( 언제 응답 받을 지 모르기 때문에)
// 이용할 서버는 JSON Placeholder
//https://jsonplaceholder.typicode.com/posts

// fetch도 비동기처리 함수이며 Promise 객체를 반환한다.
async function getData() {
  let response = await fetch("https://jsonplaceholder.typicode.com/posts");
  let jsonResponse = await response.json();
  console.log(jsonResponse);
}

getData();

0개의 댓글