[JavaScript] Promise, async, await = 삼박자 얼쑤!

hyeonbin·2023년 4월 26일
0

JS 계란반 스터디

목록 보기
6/12
post-thumbnail
🎶🥁 Promise, async, await 삼박자를 타기 전에,
동기와 비동기, 콜백 함수와 콜백 지옥을 간단하게 이해하고 가자!

📃 Promise, async, await

📍 동기 & 비동기

동기 (Synchronous) - 순차적

  • 요청과 결과가 동시에 일어난다.
  ✅ 장점 : 설계가 매우 간단하고, 직관적이다.
  ❎ 단점 : 결과가 주어질 때까지 아무것도 못하고 대기해야 한다.
  
  
  예) 이름: 김삼박
  
  삼박이가 청소기를 돌리고(12시) → 
  밥을 하고(13시) → 
  설거지를 하고(14시) → 
  빨래를 한다(15시)

비동기 (Asynchronous) - 비순차적

  • 요청과 결과가 동시에 일어나지 않는다.
  ✅ 장점 : 결과가 주어지는데 시간이 걸리더라도, 
  그 시간 동안 작업을 할 수 있어 자원을 효율적으로 사용할 수 있다.
  ❎ 단점 : 동기보다 복잡하다.


  예) 이름: 김삼박
  
  삼박이가 청소기를 돌리면서(12시) → 
  밥을 하고(12시) → 
  설거지를 하고(12시) → 
  밀린 빨래를 하고(12시)


setTimeout 함수를 통해 비동기를 알아보자!


setTimeout 코드 설명
  • 첫번째 코드에서 setTimeout 함수의 대기 시간은 0, 두번째 코드에서는 1000으로 설정되어 있다.
  • 대기 시간이 0이어도 setTimeout 함수에 전달된 콜백 함수는 다른 코드가 모두 실행된 다음 비동기적으로 실행된다.
  • setTimeout의 대기 시간이 0이라고 해서 즉시 실행되는 것이 아니라, 이벤트 루프의 다음 틱에서 실행되게 된다.

=> 따라서 첫번째와 두번째 실행 결과는 약간의 시간 차이가 있을 뿐, 같은 순서로 실행된다.


근데 만약 동기 함수라면?

  • setTimeout 함수를 이용해 비동기 함수를 실행하는 것 이나라, 동기 함수로 실행한다면 실행 결과는 실행 순서대로 일어날 것이다.
  const one = '1';
  const two = '2';
  const three = '3';
  const four = '4';

  console.log(one);
  console.log(two);
  console.log(three);
  console.log(four);

  // 실행결과
  1
  2
  3
  4



📍 콜백 함수 & 콜백 지옥

콜백 함수란?

  • 나중에 호출되는 함수! 다른 함수가 실행을 끝낸 뒤 실행 ( 콜백 ) 되는 함수다.

  • 즉 함수를 등록한 뒤, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수다.

  • 콜백 함수는 매개변수로 함수 객체를 전달해 호출 함수 내에서 매개변수 함수를 실행하는 것이다.


콜백 함수를 왜? 사용할까?

  • 자바스크립는 비동기 프로그래밍을 위해 만들어진 언어기에 한 명령에 대한 응답을 기다리는 것보다 다음 명령을 수행하기 위해 콜백 함수를 사용한다.

  • 자바스크립트 V8 엔진은 싱글 스레드이기에 동시에 여러 개의 명령을 수행할 수 없다. 그래서 한 명령의 응답이 오래 걸리면 속도가 매우 느려지게 된다.

  • 그렇기에 비동기적 프로그래밍 방식으로 콜백 함수를 사용하면, 자바스크립트 엔진이 브라우저나 Node.js에서 제공하는 Web API에게 작업을 맡기고, 다음 명령들을 수행해 속도를 빠르게 할 수 있다.


콜백 함수 사용 원칙

1. 익명 함수 사용

  • 보통 콜백 함수는 호출 함수에 일회용을 사용하는 경우가 많기에 코드의 간결성을 위해 이름이 없는 익명 함수를 사용한다.

  • 함수 내부에서 매개변수를 통해 실행되기에 이름을 붙이지 않아도 된다. 또한 함수 이름의 충돌 방지를 위한 이유도 있다.

  foo("개발자", function (job) { // 함수의 이름이 없는 익명 함수
      console.log(job); 
  });

2. 화살표 함수 모양의 콜백

  • 익명 함수로 코드의 간결성을 얻을 수 있지만, 한단계 더 간결성을 얻기 위해 자바스크립트의 화살표 함수를 통해 익명 화살표 함수 형태로 사용할 수 있다.

  • 익명 화살표 함수 형태는 자바스크립트 프로그래밍을 하면서 자주 접하게 되는 형태이다.

  function foo(callback) {
    const job = "developer";
    callback(job); // 콜백 함수 호출
  }

  // 익명 화살표 콜백 함수
  foo((job) => {
    console.log("My Job is " + job);
  }); // My Job is developer

3. 함수 이름 전달

  • 콜백 함수가 일회용이 아닌 여러 호출 함수에 재활용으로 자주 이용될 경우, 별도로 함수를 정의하고 함수의 이름만 호출 함수의 인자에 전달하는 식으로 사용 가능하다.

  • 여러가지 함수 형태를 다양하게 전달할 수 있다.

  function bar(job) {
    console.log('My Job is ' + job);
  }

  function foo(callback) {
    const job = 'developer';
    callback(job); // 콜백 함수 호출
  }

  function foo2(callback) {
    const job2 = 'designer';
    callback(job2); // 콜백 함수 호출
  }

  // 콜백 함수의 이름만 인자로 전달
  foo(bar); // My Job is developer
  foo2(bar); // My Job is designer

콜백 함수 활용

1. 이벤트 리스너로 사용

  • addEventListner는 특정 이벤트가 발생했을 때 콜백 함수를 실행하는 메서드이다.

  • click과 같은 이벤트를 처리하기 위해 등록하는 이벤트 리스너로 콜백함수가 쓰인다.

  • 버튼을 click하면 연관된 스크립트 실행을 콜백 함수로 등록하는 형태다.

  // 버튼 요소를 선택
  const button = document.getElementById('button');

  // 버튼에 클릭 이벤트 리스너를 추가
  button.addEventListener('click', function () {
    // 콜백 함수
    console.log('Button clicked!');
  });

2. 고차함수에 사용

  • 자바스크립트에서 for문 보다 더 자주 사용되는 반복문이 forEach 메서드다.

  • 이 역시 forEach 메서드의 입력값으로 콜백 함수를 전달하는 형태다.

  // 예시 : 배열의 각 요소를 두 배로 곱해서 새로운 배열을 생성하는 콜백 함수
  let numbers = [1, 2, 3, 4, 5]; // 배열 선언
  let doubled = []; // 빈 배열 선언

  // numbers 배열의 각 요소에 대해 콜백 함수 실행
  numbers.forEach(function (num) {
    doubled.push(num * 2); // 콜백 함수로 각 요소를 두 배로 곱해서 doubled 배열에 추가
  });

  console.log(doubled); // [ 2, 4, 6, 8, 10 ]

3. Ajax 결과값을 받을 때

  • 서버와 데이터를 주고받을 때 사용하는 fetch 메서드의 서버 요청의 결과값을 처리하기 위해 콜백 함수가 사용된다.
  // fetch 메서드를 사용하여 서버로부터 JSON 데이터를 받아오고 콜백 함수로 화면에 출력
  fetch('https://jsonplaceholder.typicode.com/users')
    .then(function (response) {
      // fetch 메서드가 성공하면 콜백 함수로 response 인자를 받음
      return response.json(); // response 객체의 json 메서드를 호출하여 JSON 데이터를 반환
    })
    .then(function (data) {
      // json 메서드가 성공하면 콜백 함수로 data 인자를 받음
      console.log(data);
    });

4. 타이머 실행 함수

  • setTimeout , setInterval 같은 타이머 함수에서 일정 시간마다 스크립트를 실행하는 용도로 콜백 함수를 이용한다.
  // 2000 밀리초(2초) 후에 콜백 함수 실행
  setTimeout(function () {
    console.log("Time is up!"); // 콜백 함수로 콘솔에 메시지 출력
  }, 3000);

5. 애니메이션 완료

  • jQuery에서 제공하는 animation 메서드들은 애니메이션이 끝난 후에 실행할 콜백 함수를 인자로 받는다.
  // jQuery의 slideUp 메서드를 사용하여 #box 요소를 숨기고 콜백 함수로 콘솔에 메시지 출력
  $("#box").slideUp(1000, function () {
    console.log("Animation completed!"); // 콜백 함수로 콘솔에 메시지 출력
  });

콜백 지옥이란?

  • 콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들정도로 깊어지는 현상이다.

  • 주로 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하는데, 가독성이 떨어지고 코드를 수정하기 어려워진다.

  • 비동기적인 작업을 수행하기 위해 콜백 함수를 익명 함수로 전달하는 과정에서 생기는 콜백 지옥을 Promise, async/await, Generator 등을 사용해 방지할 수 있다.



📍 Promise

  • 비동기 프로그래밍을 할 수 있게 해주며 위에서 언급된 콜백 지옥을 어느 정도 탈출할 수 있게 만들어준다.

  • Promise ( 약속하다 )Pending ( 대기중 ) 을 거쳐 콜백 함수를 다시 부르곘다는 의미다.


Promise 만들기

const promise = new Promise((resolve, reject) => {
	// 처리 내용
})

promise.then(
	// resolve가 호출되면 then이 실행
)
.catch(
	// reject가 호출되면 catch가 실행
)
.finally(
	// 콜백 작업을 마치고 무조건 실행되는 finally (생략 가능)
)

Promise 만들기
  • 비동기 작업 성공시 resolve를 호출하고, 비동기 작업 실패시 reject를 호출하도록 구현한다.
  • 그런 다음, then()을 사용해 resolve가 호출됐을 때의 상황을 처리하고, catch()를 사용해 reject가 호출됐을 때의 상황을 처리한다. finally()는 프로미스가 성공 또는 실패와 관계없이 항상 실행되어야 하는 코드를 작성할 때 사용된다.

Promise 예제

  • flag가 true면 resolve를 호출하고, false면 reject를 호출한다.
  • resolvereject는 각각 호출되어 인수로 문자열을 전달한다.
  const flag = true;
  const promise = new Promise(((resolve, reject) => {
      if (flag) {
          resolve('resolve가 되었음')
      }
      else {
          reject('reject가 되었음')
      }
  }))

  promise.then((resolveMessage) => {
      console.log(resolveMessage)
  })
  .catch((errorMessage) => {
      console.log(errorMessage)
  })

  // 결과 : resolve가 되었음


Promise 체이닝

1. Promise 객체로 비동기 처리 연결하기

  • then ( ) 과 catch ( ) 뒤에는 또다른 then ( ) 과 catch ( ) 를 연결함으로써 비동기 처리를 연결할 수 있다.
  const flag = true
  const promise = new Promise(((resolve, reject) => {
      if (flag) {
          resolve('resolve가 되었음')
      }
      else {
          reject('reject가 되었음')
      }
  }))

  promise.then((resolveMessage) => {
      console.log(resolveMessage)

      return new Promise(((resolve, reject) => {
          if (flag) {
              resolve('resolve가 되었음2')
          }
          else {
              reject('reject가 되었음2')
          }
      }))
  })
  .then((resolveMessage2)=> {
      console.log(resolveMessage2)
  })
  .catch((errorMessage) => {
      console.log(errorMessage)
  })

  // 결과 : resolve가 되었음
  // 결과 : resolve가 되었음2

2. 실무에서 있을 법한 Promise 연결 사례

// 실제 웹 서비스에서 있을 법한 사용자 로그인 인증 로직에 프로미스를 여러 개 연결함
getData(userInfo)
  .then(parseValue)
  .then(auth)
  .then(diaplay);
// 페이지에 입력된 사용자 정보를 받아와 파싱, 인증 등 작업을 거치는 코드를 나타냄
// userInfo = 사용자 정보가 담긴 객체
// parseValue, auth, display =  각각 프로미스를 반환해주는 함수라고 가정
const userInfo = {
  id: 'test@abc.com',
  pw: '****'
};

function parseValue() {
  return new Promise({
    // ...
  });
}
function auth() {
  return new Promise({
    // ...
  });
}
function display() {
  return new Promise({
    // ...
  });
}

Promise 3가지 상태

Promise states란 프로미스의 처리 과정을 의미한다.
new Promise ( ) 로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖는다.

1. Pending (대기) - 비동기 처리 로직이 아직 완료되지 않은 상태

// new Promise() 메서드를 호출하면 대기 상태가 됨
new Promise()
 
// new Promise() 메서드를호출 시 콜백 함수를 선언할 수 있고, 
//콜백 함수의 인자는 resolve, reject가 됨
new Promise(function(resolve, reject) {
	//...
})

2. Fulfilled (이행) - 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태

// 콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행 상태가 됨
new Promise(function(resolve, reject) {
  resolve();
});
// 이행 상태가 되면 아래와 같이 then()을 이용해 처리 결과 값을 받을 수 있음
function getData() {
  return new Promise(function(resolve, reject) {
    var data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
  console.log(resolvedData); // 100
});

3. Rejected (실패) - 비동기 처리가 실패하거나 오류가 발생한 상태

// new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있는데, 
// reject를 아래와 같이 호출하면 실패 상태가 됨
new Promise(function(resolve, reject) {
  reject();
});
// 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있음
function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

Promise 처리의 흐름


Promise 에러 처리 방법

실제 서비스를 구현하다 보면 네트워크 연결, 서버 문제 등 오류가 발생할 수 있다. 따라서 에러 처리 방법은 2가지가 있는데, 모두 프로미스의 reject ( ) 메서드가 호출되어 실패 상태가 된 경우에 실행된다.

1. then ( )의 두번째 인자로 에러 처리하는 방법

  getData().then( 
    handleSuccess,
    handleError
   )

2. catch ( )를 이용하는 방법

  • 프로미스 에러 처리는 가급적 catch ( ) 를 이용하는 것이 효율적이다.

  • why ? 모든 비동기 작업에서 발생하는 오류를 중앙 집중화해 처리할 수 있다.

  • why ? 프로미스 체인 안에서 발생하는 오류를 처리하는데, 체인의 어느 위치에서든 오류가 발생하더라도 catch ( ) 블록으로 이동한다. 따라서 비동기 작업 중 하나라도 실패하면 즉시 오류 처리가 가능해 프로미스 체인이 중단되지 않고 지속될 수 있다.

  getData().then().catch();



📍 async / await

async / await란?

  • 비동기식 코드를 동기식으로 표현하여 간단하게 나타낸다.

  • 기존의 비동기 처리 방식인 Callback 함수의 단점을 보완하기 위해 Promise를 사용했지만, 코드가 장황하다는 단점이 있다. 그래서 이러한 단점을 해결하기 위해 ES8에서 도입된 비동기 처리 방식의 가장 최신 문법이다.

  • async / awaitPromise 객체를 반환한다. ( .then 사용 가능 )

  // 프로미스를 반환하고, then()으로 받게 되는 인자 result는 바로 return 값을 의미
  async function myAsync(){
    return 'async';
  }

  myAsync().then((result) => {
    console.log(result); // 결과 : async
  })

async / await 기본 문법

  • 함수 앞에 async 라는 예약어를 붙인다.

  • 함수 내부 로직 중 HTTP 통신을 하는 비동기 처리 코드 앞에 await 를 붙인다.

  • 비동기 처리 메서드가 꼭 프로미스 객체를 반환해야 await가 의도한 대로 동작한다.
    ( 일반적으로 await의 대상이 되는 비동기 처리 코드는 Axios 등 프로미스를 반환하는 API 호출 함수다. )

  async function 함수명() {
    await 비동기_처리_메서드_명()
  }

1. async 함수

  • async 는 function 앞에 위치하고 해당 함수는 항상 프로미스를 반환한다.

  • 프로미스가 아닌 값을 반환하더라도 이행 상태의 프로미스 ( resolved promise )로 값을 감싸 이행된 프로미스가 반환되도록 한다.

  • async 함수는 화살표 함수로도 정의가 가능하고, 함수 표현식으로도 정의가 가능하다.

2. await

  • awaitasync 함수 안에서만 동작한다.

  • 자바스크립트는 await 키워드를 만나면 프로미스가 처리 ( settled ) 될 때가지 기다리고, 결과는 그 이후 반환된다.

3. async & await 예제

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
 
  //2초동안 기다리게 하고 사과를 리턴하는 메서드
  async function getApple() {
    await delay(2000)
    return '🍎'
  }
 
  //1초동안 기다라게 하고 사과를 리턴하는 메서드
  async function getBanana() {
    await delay(1000)
    return '🍌'
  
  getApple().then(console.log) // 결과 : 🍎
  getBanana().then(console.log) // 결과 : 🍌

async / await 예외 처리

  • try.. catch.. 구문을 사용한다.
  function fetchData() {
      return new Promise((resolve, reject) => {
          setTimeout(() => {
              return resolve('success')
          }, 1000)
      })
  }

  async function loadData() {
      try {
          const result = await fetchData()
          console.log(result)
      } catch(e) {
          console.log(e)    
          }
  }

  loadData()

자세한 코드 설명
  1. fetchData 함수는 비동기 작업을 수행하는 함수로, Promise를 반환한다. Promise 객체를 생성해 콜백 함수로 전달하고, 이 콜백 함수는 resolvereject를 인자로 받는다.
  1. setTimeout 함수를 사용해 1초 후에 resolve를 호출해 Promise가 성공 상태로 이행되도록 한다.
  1. loadData 함수는 async 키워드로 선언되어 비동기 함수로 정의되고, try 블록 안에서 비동기 함수인 fetchDate를 호출해 데이터를 가져오려고 한다. await 키워드를 사용해 fetchData 함수의 결과가 반환할 때까지 기다린다.
  1. fetchData가 성공적으로 실행되어 Promise가 이행되면, 결과 값 'success'를 result 변수에 할당하고, 이후 console.log(result)를 호출해 'success'를 콘솔에 출력한다.
  1. 만약에 fetchData가 실패하면 catch 블록으로 넘어가고, e라는 변수로 에러를 받아 콘솔에 출력한다. 마지막으로 loadData 함수를 호출하고, 이 함수 내에서 fetchData를 비동기적으로 호출하고 결과를 처리한다.

종합적으로 보면 :

fetchData 함수는 1초 후에 Promise를 성공 상태로 이행하고, loadData 함수에서는 fetchData 함수를 호출해 결과 값을 가져오고 출력한다. 만약 fetchData 함수가 실패하면, 에러를 처리해 콘솔에 출력한다.

profile
할 수 있다고 믿는 사람은 결국 그렇게 된다 😄😊

0개의 댓글