Jieunny의 블로그

S2) Unit 3. [JS] 비동기(Callback, Promise, Async/Await) 본문

CodeStates/learning contents

S2) Unit 3. [JS] 비동기(Callback, Promise, Async/Await)

Jieunny 2023. 1. 17. 12:00

✔️ Blocking : 하나의 작업이 끝날 때까지 이어지는 작업을 막는 것

✔️ 동기적 : 시작 시점과 완료 시점이 같은 상황

📌 non-blocking 하고 비동기적으로 작동하는 것이 효율적

 

 

📣 자바스크립트의 동기 & 비동기

✔️ 동기 : 특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것

✔️ 비동기 : 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것

 

✔️ JS의 작동원리 

JS는 싱글 스레드 기반 언어(스레드가 하나라서 동시에 작업이 불가능하다 -> 동기적으로 작동한다)
자바스크립트 런타임에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있다.

 

📣 비동기 JS

✔️ 타이머 관련 API

 

1️⃣ setTimeout(callback, millisecond)

➰ 일정 시간 후에 함수를 실행한다.

➰ 매개변수 : 실행 할 콜백함수, 콜백 함수 실행 전 기다려야 할 시간(밀리초)

➰ 임의의 타이머 ID를 리턴

setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);
// 123

➰ console.log는 함수 호출 후 1초 후에 실행된다.

 

2️⃣ clearTimeout(timerId) 

➰ setTimeout 타이머를 종료한다.

➰ 매개변수 : 타이머 ID

➰ 리턴 X

const timer = setTimeout(function () {
  console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨.

 

3️⃣ setInterval(callback, millisecond)

➰ 일정 시간의 간격을 가지고 함수를 반복적으로 실행한다.

➰ 매개변수 : 실행 할 콜백 함수, 함수를 반복적으로 실행시키기 위한 시간 간격(밀리초)

➰ 임의의 타이머 ID를 리턴

setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
// 345

console.log 는 10초마다 실행된다.

 

4️⃣ clearInterval(timerId)

setInterval 타이머를 종료한다.

매개변수 : 타이머 ID

리턴 X

const timer = setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨.

 

🚨 항상 개발자가 제어할 수 있는 코드를 작성하자

 

 

📣 비동기 코드의 순서를 제어할 수 있는 방법

1️⃣ Callback()

✔️ 비동기 코드의 순서를 제어할 수 있는 방법 (비동기를 동기화 할 수 있다)

const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {});
    });
  });
};

// A B C

➰ callback()이 없다면 무작위로 시간을 설정했기 때문에 매번 다른 결과가 나올 것이다.

➰ 랜덤한 시간이 지나면 setTimeout() 이 실행되고, 전달받은 문자열을 출력한 뒤 callback() 인자로 전달받은 함수(다음 문자열 찍는 함수)를 실행하게 된다.

🚨 이렇게 하면 비동기가 아니라 동기적으로 작동하는 게 아닌가?

더보기

콘솔에 찍는 순서로만 보면 동기적으로 보일 수 있지만, 콘솔이 아닌 어떤 작업을 하는 코드 였다면 작업을 종료한 뒤 행동(콘솔로 찍는 것)만 순차적으로 하게 되는 것이므로

비동기 적으로 작동하는 게 맞다.

🚨 Callbak Hell : callback 함수를 사용하면 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡하고 가독성이 낮아지는 단점 -> 이를 방지하기 위해 Promise 가 사용되기 시작했다.

 

 

2️⃣ Promise

✔️ class 이므로 new 키워드를 통해 Promise 객체를 생성한다.

✔️ 비동기 처리를 진행할 콜백 함수(excutor)를 전달받는데, 이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.

✔️ 객체가 생성되면 excutor는 자동으로 실행되고, 코드가 정상적으로 실행되면 resolve 함수를, 에러가 발생하면 reject 함수를 호출한다.

let promise = new Promise((resolve, reject) => {
	// 1. 정상적으로 처리되는 경우
	// resolve의 인자에 값을 전달할 수도 있습니다.
	resolve(value);

	// 2. 에러가 발생하는 경우
	// reject의 인자에 에러메세지를 전달할 수도 있습니다.
	reject(error);
});

 

✔️ Promise 객체의 내부 프로퍼티

➰ Promise 객체는 state, result 내부 프로퍼티를 갖는다.

➰ 직접 접근은 할 수 없고 .then, .catch, .finally 메서드를 사용해야 접근 할 수 있다. 

 

➕ State, Result

✔️ State
➰ 기본 상태는 pending(대기) 이다.
➰ 콜백 함수가 성공적으로 작동했다면 fulfilled(이행) 로 변경되고, 에러가 발생했다면 rejected(거부) 된다.

✔️ Result
➰ 처음에는 undefined 이다.
➰ 콜백 함수가 성공적으로 작동해서 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변한다.

 

➕ Then, Catch, Finally 

✔️ 모두 Promise 객체를 반환한다.
✔️ Then
➰ executor 코드들이 정상적으로 처리되면 resolve 함수를 호출하고, .then으로 접근 가능하다.
➰ .then 안에서 리턴한 값이 Promise라면 result를 다음 .then의 콜백 함수의 인자로 받아오고, Promise가 아니라면 리턴한 값을 현재 .then의 콜백 함수의 인자로 받아올 수 있다.
let promise = new Promise((resolve, reject) => {
	resolve("성공");
});

promise.then(value => {
	console.log(value);
	// "성공"
})​


✔️ Catch
➰ executor에 작성한 코드에서 에러가 발생했을 경우 reject 함수를 호출하고, .catch 메서드로 접근할 수 있다.
let promise = new Promise(function(resolve, reject) {
	reject(new Error("에러"))
});

promise.catch(error => {
	console.log(error);
	// Error: 에러
})​

 

✔️ Finally
➰ executor에 작성한 코드의 성공, 에러 여부와 상관없이, .finally로 접근할 수 있다.
let promise = new Promise(function(resolve, reject) {
	resolve("성공");
});

promise
.then(value => {
	console.log(value);
	// "성공" executor가 성공했을 경우 실행된다.
})
.catch(error => {
	console.log(error);
})	// executor에서 에러가 발생했을 경우 실행된다.

.finally(() => {
	console.log("성공이든 실패든 작동!");
	// "성공이든 실패든 작동!"
})​

 

 

2️⃣−1️⃣ Promise chaining

 

✔️ 비동기 작업을 순차적으로 진행해야 하는 경우 사용

✔️ .then, .catch, .finally 메서드는 모두 Promise를 반환하기 때문에

     성공할 경우 .then을 통해 계속 연결할 수 있고, 에러가 발생하면 .catch로 처리가 가능하다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = () => {
  printString('A')
    .then(() => {	// 'A'를 출력하는 게 성공하면
      return printString('B');	// 'B'를 출력하고, 
    })
    .then(() => {	//	'B'를 출력하는 게 성공하면
      return printString('C');	//'C'를 출력한다.
    });
};

printAll();

console.log(
  `아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
);

//아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있습니다!
//A
//B
//C
let promise = new Promise(function (resolve, reject) {
  resolve('성공');
  // reject("실패");
});

promise
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
  .finally(() => {
    console.log('성공이든 실패든 작동!');
  });
 
//성공
//성공
//성공
//성공이든 실패든 작동!

 

2️⃣−2️⃣ Promise.all()

✔️ 여러 개의 비동기 작업을 동시에 처리하고 싶을때 사용한다.

✔️ 인자로는 배열을 받고, 해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 성공하면 결과를 배열에 저장해 새로운 Promise를 반환 해준다.

const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));

// promise.all
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // ['1초', '2초', '3초']
  .catch((err) => console.log(err));
  
  
  // 하나라도 에러가 발생했을 경우
  Promise.all([
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
])
	.then((value) => console.log(value))
  .catch((err) => console.log(err));
	// Error: 에러1

➰ 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 나머지 Promise의 state와 상관없이 즉시 종료된다.

 

2️⃣−3️⃣ Promise Hell

✔️ callback 함수와 마찬가지로 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell 이 발생할 수 있다.

 

3️⃣ Async / Await

✔️ ES8 부터 제공하는 키워드로, 복잡한 Promise 코드를 간결하게 작성할 수 있다.

✔️ 함수 앞에 async 키워드를 사용하고, async 함수 내에서만 await 키워드를 사용하면 된다.

✔️ await 키워드가 작성된 코드가 동작하고 나서, 다음 순서의 코드가 동작하게 된다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = async () => {
  await printString('A');
  await printString('B');
  await printString('C');
};

printAll();

console.log(
  `Async/Await을 통해 Promise를 간결한 코드로 작성할 수 있게 되었습니다.`
);