비동기

HanSungUk·2022년 5월 29일
0

Javascript

목록 보기
15/16
post-thumbnail

비동기

현재 코드스테이츠 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

학습목표

  • 어떤 경우에 중첩된 콜백(callback)이 발생하는지 이해할 수 있다.
  • 중첩된 콜백(callback)의 단점, Promise의 장점을 이해할 수 있다.
  • async / await 키워드에 대해 이해하고, 작동 원리를 이해할 수 있다.
  • callback, Promise, async/await 구현 방법을 이해한다.
  • Promise 실행 함수가 가지고 있는 두 개의 매개변수 resolvereject를 활용할 수 있다.
  • new Promise()를 통해 생성한 Promise 인스턴스가 사용할 수 있는 메서드의 용도를 이해한다.
  • Promise의 세 가지 상태는 각각 무엇인지 설명할 수 있다.
  • async/ await키워드와 함께 실행되는 함수는 어떤 타입이어야 하는지 이해한다.
  • await키워드를 사용할 경우 어떤 값이 리턴되는지 설명할 수 있다.

1. 동기와 비동기 처리

  • 들어가기 전

    제어권 : 제어권은 자신(함수)의 코드를 실행할 권리 같은 것입니다. 호출된 함수(B함수)는 return을 통해서 호출한 함수(A함수)에게 제어권을 넘겨줍니다.
    결과값을 기다린다는 것 : A함수에서 B함수를 호출했을 때, A함수가 B함수의 결과값을 기다리느냐의 여부를 의미한다.

  • blocking vs non-blocking
    호출된 함수(B함수)가 호출한 함수(A함수)에게 제어권을 바로 넘겨주냐 안넘겨주냐의 차이를 가지고 있습니다.

blocking : A함수가 B함수를 호출할 때, B함수에게 제어권을 넘겨줍니다. A 함수는 제어권을 넘겨줬기 때문에 함수 실행을 잠시 멈춥니다.
B함수는 실행이 끝나면 자신을 호출한 A함수에게 제어권을 돌려주고 A함수가 다시 실행됩니다.
non-blocking : A함수가 B함수를 호출할 때, B함수는 A함수로부터 받은 제어권을 바로 A함수에게 넘겨줍니다. 따라서 A함수는 B함수를 호출한 이후에도 자신의 코드를 계속 실행시킵니다.

  • 동기(Synchronous) vs 비동기(Asynchronous)
    호출하는 함수(A함수)가 호출된 함수(B함수)의 작업 완료 여부를 신경쓰느냐 안쓰느냐의 차이를 갖고 있습니다.

동기(Synchronous) : 호출하는 함수(A함수)가 호출된 함수(B함수)의 작업 완료 후 리턴을 기다리거나, 바로 리턴 받더라도 미완료 상태라면 작업 완료 여부를 스스로 계속 확인하면서 신경씁니다.

function waitSync(ms){
	var start = Date.now();
  	var now = start;
  	while(now - start < ms){
    	now = Date.now();
    }
} // 현재 시각과 시작 시각을 비교하여 ms 범위 내에서 무한 루프를 도는 blocking 함수입니다. 

function orderCoffeeSync(coffee){
	console.log(coffee + '가 접수되었습니다.')
  	waitSync(2000);
  	return coffee;
}

function drink(person, coffee) {
	console.log(person + '는' + coffee + '를 마십니다.');
}

let customer = [{
	name: 'Steve',
  	request: '카페라떼'
},{
	name: 'John',
  	request: '아메리카노'
}
];

// 동기(synchronous)
customers.forEach(function(customer){
	let coffee = orderCoffeeSync(customer, request);
  	drink(customer.name, coffee);
});
// 카페라떼가 접수되었습니다.
// Steve는 카페라떼를 마십니다.
// 아메리카노가 접수되었습니다.
// John는 아메리카노를 마십니다.

비동기(Asynchronous) : A함수가 B함수를 호출할 때 콜백 함수를 함께 전달해서, 함수 B의 작업이 완료되면 함께 보낸 콜백 함수가 실행됩니다.
즉, A함수는 B함수를 호출한 후로 함수 B의 작업 완료 여부에는 신경쓰지 않습니다. 콜백 함수가 작업 완료 여부를 확인합니다.

function waitAsync(callback, ms){
	setTimeout(callback, ms); 
  // 특정 시간 이후에 callback 함수가 실행되게끔하는 브라우저 내장 기능입니다.
}  

function drink(person, coffee) {
	console.log(person + '는' + coffee + '를 마십니다.');
}

let customer = [{
	name: 'Steve',
  	request: '카페라떼'
},{
	name: 'John',
  	request: '아메리카노'
}
];

function orderCoffeeAsync(menu, callback){
	console.log(menu + '가 접수되었습니다.');
  	waitAsync(function(){
    	callback(menu);
    }, 4000)
}

// 비동기(asynchronous)
customers.forEach(function(customer){
	orderCoffeeAsync(customer.request, function(coffee){
    	drink(customer.name, coffee);
    });
});
// 아메리카노가 접수되었습니다.
// 아메리카노가 접수되었습니다.
// Steve는 카페라떼를 마십니다.
// John는 아메리카노를 마십니다.
  • 비동기 함수 전달 패턴
  1. 콜백함수 패턴
let request = 'caffelatte';
orderCoffeeAsync(request, function(response){
	// response -> 주문한 커피 결과
  drink(response);
});
  1. 이벤트 등록 패턴
let request = 'caffelatte';
orderCoffeeAsync(request).onready = function(response){
	// response -> 주문한 커피 결과
  drink(response);
});
  • 비동기의 주요 사례
    • DOM Element의 이벤트 핸들러
      • 마우스, 키보드 입력(click, keydown 등)
      • 페이지 로딩(DOMContentLoaded 등)
    • 타이머
      • 타이머 API(setTimeout 등)
      • 애니메이션 API(requestAnimationFrame)
    • 서버에 자원 요청 및 응답
      - fetch API
      • AJAX(XHR)
  • 타이머 관련 API
    setTimeout(callback, millisecond): 일정 시간 후에 함수를 실행
    • 매개변수(parameter): 실행할 콜백 함수, 콜백 함수 실행 전 기다려야 할 시간(밀리초)
    • return 값: 임의의 타이머 ID
setTimeout(function(){
	console.log('1초 후 실행');
}, 1000);
// 123

clearTimeout(timerId): setTimeout 타이머를 종료

  • 매개변수(parameter): 타이머 ID
  • return 값: 없음
setTimeout(function(){
	console.log('1초 후 실행');
}, 1000);
clearTimeout(timer);
// setTimeout이 종료됨

setInterval(callback, millisecond): 일정 시간의 간격을 가지고 함수를 반복적으로 실행

  • 매개변수(parameter): 실행할 콜백 함수, 반복적으로 함수를 실행시키기 위한 시간 간격(밀리초)
  • return 값: 임의의 타이머 ID
setInterval(function(){
	console.log('1초마다 실행');
}, 1000);
// 345

clearInterval(timerId): setInterval 타이머를 종료

  • 매개변수: 타이머 ID
  • return: 없음
setInterval(function(){
	console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨

2. 콜백 함수(Callback function)

먼저 고차함수는 함수를 리턴하는 함수와 함수를 전달인자로 받는 함수를 의미합니다.
이때, 다른 함수(A)의 전달인자(argument)로 넘겨주는 함수(B)를 콜백 함수라고 합니다.
parameter를 넘겨받는 함수(A)는 callback 함수(B)를 필요에 따라 즉시 실행(synchronously)할 수도 있고, 아니면 나중에(asynchronously) 실행할 수도 있습니다.

 function B () {
 	console.log('called at the back!');
 }
 
 function A (callback) {
 	callback(); // callback === B
 }
 
 A(B)
// called at the back!
  • 이벤트에 따른 함수(이벤트 핸들러)사용시 주의 사항
function handleClick() {
	console.log('button click');
};

// 가능
document.querySelector('#btn').onclick = handleClick;
document.querySelector('#btn').onclick = function(){
	handleClick();
}
document.querySelector('#btn').onclick = handleClick.bind()

// 불가능
document.querySelector('#btn').onclick = handelClick();
// 함수 실행을 연결하는 것이 아니라 함수 자체를 연결해야한다.
  • 콜백 없는 비동기 실행(Asynchronous)
// 순서 없는 비동기적 실행이 이뤄집니다.
const printString = (string) => {
	setTimeout(
    	() => {
        	console.log(string)
        },
      	Math.floor(Math.random() * 100) + 1
    )
}

const printAll = () => {
	printString("A")
  	printString("B")
  	printString("C")
}

// B
// A
// C

콜백(callback) 을 비동기에서 사용하는 이유는 순서를 제어하기 위해서 입니다.

// 순서 없는 비동기적 실행이 이뤄집니다.
const printString = (string, callback) => {
	setTimeout(
    	() => {
        	console.log(string)
          	callback()
        },
      	Math.floor(Math.random() * 100) + 1
    )
}

const printAll = () => {
	printString("A", () => {
    	printString("B", () => {
        	printString("C", () => {
            })
        })
    })
}
// A
// B
// C

콜백을 통해 원하는 순서대로 비동기적 실행을 할 수 있지만 위 예제와 같이 콜백의 갯수가 많아짐에 따라 코드의 가독성이 떨어트리는 콜백 지옥을 야기합니다.

일반적으로 콜백지옥을 해결하는 방법에는 PromiseAsync를 사용하는 방법이 있습니다.

3. Promise

promise는 자바스크립트 비동기 처리에 사용되는 객체입니다.
자바스크립트 비동기 처리란 간단히 말해서 '특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성'을 의미합니다.
비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기(then(), catch() 메서드)를 연결할 수 있습니다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속(프로미스)'을 반환합니다.

  • 프로미스 문법
let promise = new Promise(function(resolve, reject){
  	// executor()
});	

new Promise에 전달되는 함수는 executor(실행자, 실행 함수)라고 부릅니다. executor는 new Promise가 만들어질때 자동으로 실행됩니다. executor는 인자로 자바스크립트에서 자체 제공하는 resolvereject 콜백 함수를 받습니다. executor에서는 인수로 넘겨준 콜백 중 하나를 반드시 호출해야합니다.

  • resolve(value)- 주어진 값으로 이행하는 Promise객체를 반환합니다.
  • reject(reason)- 주어진 사유로 거부하는 Promise객체를 반환합니다.

new Promise 생성자가 반환하는 promise객체는 다음과 같은 내부 프로퍼티를 갖습니다

  • state - 처음에는 pending(보류)이었다 resolve가 호출되면 fulfilled, reject가 호출되면 rejected로 변합니다.
  • result - 처음엔 undefined이었다 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변합니다.

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

  1. Pending(대기): 비동기 처리 로직이 아직 완료되지 않은 상태
    참고로 아래 fulfilledrejected 상태의 프로미스는 settled(처리된) 라고 부릅니다.
// 아래와 같이 new Promise() 메서드를 호출하면 대기(Pending)상태가 됩니다.
new Promise();
// new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고,
// 콜백 함수의 인자는 resolve, reject 입니다.
new Promise(function(resolve, reject){
 // ...
})
  1. Fulfilled(이행): 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
    Promise.prototype.then(onFulfilled,[onRejected])은 첫 번째 인자는 프로미스가 이행되었을때 실행되는 함수이고, 여기서 Fulfilled promise(이행된 프로미스 결과값)를 반환합니다.
    두 번째 인자는 프로미스가 실패했을때 실행되는 함수이고, 여기서 Rejected promise(실패한 프로미스 결과값)을 반환합니다.
    하지만 then()메서드는 주로 Fulfilled promise(이행된 프로미스 결과값)을 반환하는데 사용합니다.
    Rejected promise(실패한 프로미스 결과값)을 반환할 때에는 catch() 메서드를 사용합니다.
// 여기서 콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fullfilled)상태가 됩니다.
new Promise(function(resolve, reject){
	resolve();
})
// 그리고 이행상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있습니다.
function getData(){
	return new Promise(function(resolve, reject){
    	let data = 100;
      	resolve(data);
    });
}

// resolve()의 결과 값 data를 resolvedData로 받습니다.
getData().then(function(resolvedData){
	console.log(resolvedData); // 100
});
  1. Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태
    Promise.prototype.catch(onRejected)는 Rejected promise(실패한 프로미스 결과값)을 반환합니다.
// reject를 아래와 같이 호출하면 실패(Rejected) 상태가 됩니다.
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().catch(function(err){
	console.log(err); // Error: Request is failed
})
// 위 코드를 then()으로 표현할 수 있습니다.
getData().then(null, (err) => {
	console.log(err); // Error: Request is failed
})

아래 예제는 콜백 지옥에 빠졌던 위 예제에서 promise를 이용한 예제입니다.

const printString = (string) => {
  // new Promise() 추가
  // setTimeout()을 이용해 Math.floor(Math.random() * 100) + 1후에 resolve()를 호출
	return new Promise((resolve, reject) => {
    	setTimeout(
          ()=>{
        	console.log(string)	
            resolve()
        },
        	Math.floor(Math.random() * 100) + 1
        )
    })
}

const printAll = () => {
	printString("A")
  	.then(()=>{
    	return printString("B")
    })
  	.then(()=>{
    	return printString("C")
    })
}
printAll()
// A
// B
// C

  • promise chaining
const fetchNumber = new Promise((resolve, reject)=>{
	setTimeout(()=> resolve(1), 1000); // 1초 뒤 이행된 promise 반환
});

// then() 메서드는 값을 바로 전달할 수도 있고, promise를 전달해도 됩니다. 
fetchNumber
	.then(num => num*2)
	.then(num => num*3)
	.then(num => {
		return new Promise((resolve, reject)=>{
        	setTimeout(()=> resolve(num-1), 1000)
        });
})
.then(num => console.log(num))

3.1 promise 메서드

  • Promise.resolve(value)
    • 구문 : Promise.resolve(value)
    • value : Promise에 의해 결정되는 인수.
    • 반환값 : 주어진 값으로 이행된 Promise.
const promise1 = Promise.resolve(123);

promise1.then((value) => {
  console.log(value);
  // expected output: 123
});

Promise.resolve("Success").then(function(value) {
  console.log(value); // "Success"
  • Promise.prototype.then()
    • 구문 : p.then(onFulfilled, onRejected)
    • onFulfilled : Promise가 이행될 때 호출되는 function으로, 이행 값(fulfillment value) 하나를 인수로 받습니다.
    • onRejected : Promise가 실패될 때 호출되는 function으로, 실패 이유(rejection reason) 하나를 인수로 받습니다.
    • 반환값 : Promise가 이행되거나 실패했을때, 각각에 해당하는 핸들러 함수(onFulfilled, onRejected)가 비동기 적으로 실행됩니다. then()과 아래의 catch()메서드는 무조건 프로미스를 반환하기 때문에 체이닝이 가능합니다.
p.then(function(value) {
  // 이행
}, function(reason) {
  // 거부
});
const promise1 = new Promise((resolve, reject) => {
  resolve('Success!');
});

promise1.then((value) => {
  console.log(value);
  // expected output: "Success!"
});
  • Promise.reject()

    • 구문 : Promise.reject(reason)
    • reason : Promise가 실패한 이유.
    • 반환값 : 실패한 Promise를 반환합니다.
Promise.reject(new Error("fail")).then(function(error) {
 // Promise가 실패했으므로 onFulfilled는 호출되지 않음
}, function(error) {
  console.log(error); // Stacktrace
});
  • Promise.prototype.catch()
    • 구문 : p.catch(onRejected)
    • onRejected: Promise가 실패될 때 호출되는 function으로, 실패 이유(rejection reason)하나를 인수로 받습니다.
p.catch(function(reason) {
   // rejection
});
  • Promise.all()
    이 메서드는 여러 프로미스의 결과를 집계할때 유용합니다.
    • 구문 : Promise.all(iterable);
    • iterable: Array와 같이 순환 가능한(iterable) 객체
    • 반환값: 프로미스를 반환합니다.
      1. 반환한 프로미스의 이행 결과 값은 (프로미스가 아닌 값 포함한)매개변수로 주어진 순회 가능한 객체(iterable)에 포함된 모든 값을 담은 배열입니다. 반환하는 프로미스의 이행 값은 매개변수로 주어진 프로미스의 순서와 일치하며, 완료 순서에 영향을 받지 않습니다.
      2. 주어진 프로미스 중 하나라도 실패하면 다른 프로미스의 이행 여부에 상관없이 실패 이유를 반환합니다.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

4. async & await

자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법으로 기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와줍니다.

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

먼저 함수의 앞에 async라는 예약어를 붙이면 해당 함수는 항상 프로미스를 반환합니다. 프로미스가 아닌 값을 반환하더라도 이행 상태의 프로미스(fulfilled promise)로 값을 감싸 이행된 프로미스가 반환되도록 합니다.
그리고 나서 비동기 처리 메서드 코드 앞에 await를 붙입니다. 여기서 awaitasync함수 안에서만 동작합니다.

function fetchItems(){
	return new Promise(function(resolve, reject){
    	let items = [1,2,3];
     	resolve(items)
    }); 
}

async function logItems() {
	let resultItems = await fetchItems();
  	console.log(resultItems); // [1,2,3]
}

fetchItems()함수를 실행하면 프로미스가 이행(Resolved)되며 결과 값은 items 배열이 됩니다.

logItems()함수를 실행하면 fetchItems()함수의 결과 값인 items배열이 resultItems변수에 담깁니다.

자바스크립트가 await를 만나면 프로미스가 처리될 때까지 함수 실행을 기다리고 프로미스가 처리되면 조건에 따라 아래와 같은 동작이 이어집니다.

  1. 에러 발생 - 예외가 생성됨
  2. 에러 미발생 - 프로미스 객체의 result 값을 반환

프로미스가 처리되길 기다리는 동안 자바스크립트 엔진은 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에 CPU 리소스가 낭비되지 않습니다. 또한 await를 사용하는 이유는 직관적이기 때문입니다.

마무리

Q. Promise 실행 함수가 가지고 있는 두 개의 파라미터 resolvereject는 각각 무엇을 의미하나요?
A. 비동기 작업이 성공한 경우 resolve(value)를 호출하고, 실패한 경우 reject(error)를 호출합니다.

Q. resolve, reject 함수에는 전달 인자를 넘길 수 있습니다. 이때 넘기는 전달인자는 어떻게 사용할 수 있나요?
A. then()catch() 등 promise 메서드를 통해 사용할 수 있습니다.

Q. new Promise()를 통해 생성한 Promise 인스턴스에는 어떤 메서드가 존재하나요? 각각은 어떤 용도인가요?
AA. 대표적인 메서드는 then(), catch()로서 프로미스를 리턴하기 위해 사용합니다.

Q. Promise.prototype.then 메서드는 무엇을 리턴하나요?
AA. then() 자체는 프로미스를 리턴하지만 result 값을 갖기 위해서는 return을 해줘야합니다. then()은 프로미스의 return 값을 받기 때문입니다.
return을 안해주면 result 값은 undefined가 나옵니다. 프로미스를 리턴하기 때문에 체이닝은 가능합니다.

Q. Promise.prototype.catch 메서드는 무엇을 리턴하나요?
AA. 프로미스를 리턴합니다.

Q. Promise의 세 가지 상태는 각각 무엇이며, 어떤 의미를 가지나요?
AA. pending, fufilled, rejected

Q. await 키워드 다음에 등장하는 함수 실행은 어떤 타입을 리턴할 경우에만 의미가 있나요?
AA. 프로미스가 있어야 의미가 있습니다.

Q. await 키워드를 사용할 경우, 어떤 값이 리턴되나요?
AA. 프로미스 결과값이 리턴됩니다. 따라서 then()처럼 체이닝을 하지 않아도 간단하게 결과값들을 얻을 수 있습니다.

Q. promise.all의 전달인자는 어떤 형태인가요?
A. Array와 같이 순환 가능한(iterable) 객체

Q. promise.all을 사용할 경우에 then메서드의 매개변수는 어떠한 형태인가요?
A. Promise.all에서 반환한 프로미스의 이행 값은 매개변수로 주어진 순환 가능한 객체(iterable)에 포함된 모든 값을 담은 배열 형태 입니다.
따라서 then 메서드의 매개변수는 배열 형태를 갖습니다.

Q. promise.all에 두 개의 Promise 요청이 전달되고, 만일 그중 하나가 rejected 상태가 되는 경우, then메서드, catch메서드 중 어떤 메서드를 따라갈까요?
A. Promise.all()은 배열 내 요소 중 어느 하나라도 거부하면 즉시 거부하기 때문에 catch 메서드를 따라갑니다.

0개의 댓글