- 어떤 경우에 중첩된 콜백(callback)이 발생하는지 이해할 수 있다.
- 중첩된 콜백(callback)의 단점, Promise의 장점을 이해할 수 있다.
- async/await 키워드에 대해 이해하고, 작동 원리를 이해할 수 있다.
카페에 가서 주문을 하는데, 앞 사람이 주문한 커피가 나올때까지 나는 주문조차 할 수 없다면 얼마나 불편하고 답답할까?
이렇게 Task A의 종료시점과 Task B의 시작시점이 같은, 즉 앞선 Task가 완전히 끝나기 전에는 다음 Task는 블로킹(blocking)당해 시작조차 할 수 없는, 다르게 말하면 앞선 Task가 완전히 끝나야만 다음 Task가 실행을 시작하는 상황을 '동기적(synchronous)이다.' 라고 한다.
컴퓨터에게도 마찬가지로 이는 답답하고 효율 떨어지는 방법이다. 그래서 Node.js를 만든 개발자가 논 블로킹(non-blocking)하고 비동기적(asynchronous)으로 작동하는 런타임을 개발하게 된다.
이러한 비동기적 실행은 웹개발에서 특히나 유용하게 쓰이는데, 우리가 사용하는 웹에서 다음과 같은 기능들은 비동기적으로 발생해야 한다.
백그라운드에서 무언가가 실행 중이면 다른 건 전혀 못한다던가, A가 로딩중일 경우 B는 전혀 이용할 수 없다던가, 큰 용량의 파일을 로딩하는 사이에는 아무것도 할 수 없다면, 그 웹은 매우 느리고 답답할 것이다.
JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미
동기적 처리보다 훨씬 효율적이다. (당연함)
JavaScript는 싱글 스레드 기반으로 동작하는 언어이기 때문에 동기적으로 작동하지만, JS가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있는 것!
setTimeout(callback, millisecond)
callback
: 실행할 콜백 함수millisecond
: 콜백 함수 실행 전 기다려야 할 시간 (밀리초)setTimeout(function () { console.log('1초 후 실행'); }, 1000); // 123
clearTimeout(timerId)
setTimeout()
에서 리턴값으로 받은 타이머 IDconst timer = setTimeout(function () { console.log('10초 후 실행'); }, 10000); clearTimeout(timer); // setTimeout이 종료됨.
setInterval(callback, millisecond)
callback
: 실행할 콜백 함수millisecond
: 반복적으로 함수를 실행시키기 위한 시간 간격 (밀리초)setInterval(function () { console.log('1초마다 실행'); }, 1000); // 345
clearInterval(timerId)
setInterval()
에서 리턴값으로 받은 타이머 IDconst timer = setInterval(function () { console.log('1초마다 실행'); }, 1000); clearInterval(timer); // setInterval이 종료됨.
비동기 코드는 코드가 작성된 순서대로 작동되는 것이 아니라 동작이 완료되는 순서대로 작동하게 됩니다. 즉, 코드의 순서를 예측할 수 없다.
➡ 개발자는 언제나 예측가능한 코드를 작성하도록 노력해야 함. 따라서, 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 한다.
Callback
함수로 비동기 코드 순서 제어하기비동기로 작동하는 코드를 제어할 수 있는 여러 방법 중 하나는 바로 Callback
함수를 활용하는 방법!' 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', () => {}); }); }); }; printAll(); // A B C가 순차적으로 출력된다.
Callback
함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.
const printString = (string, callback) => { setTimeout(function () { console.log(string); callback(); }, Math.floor(Math.random() * 100) + 1); }; const printAll = () => { printString('A', () => { printString('B', () => { printString('C', () => { printString('D', () => { printString('E', () => { printString('F', () => { printString('G', () => { printString('H', () => { printString('I', () => { printString('J', () => { printString('K', () => { printString('L', () => { printString('M', () => { printString('N', () => { printString('O', () => { printString('P', () => {}); }); }); }); }); }); }); }); }); }); }); }); }); }); }); }); }; printAll();
Promise
가 사용되기 시작!Promise
로 비동기 코드 순서 제어하기Promise
는 class
이다. 즉, new
키워드를 통해 Promise
객체를 생성해서 사용
Promise
는 비동기 처리를 수행할 콜백 함수(executor
)를 인수로 전달받는다.
resolve
, reject
함수를 인수로 전달받음Promise
객체가 생성되면 executor
는 자동으로 실행되고, 작성했던 코드들이 작동한다.
resolve
함수를 호출reject
함수를 호출let promise = new Promise((resolve, reject) => { // 1. 정상적으로 처리되는 경우 // resolve의 인자에 값을 전달할 수도 있습니다. resolve(value); // 2. 에러가 발생하는 경우 // reject의 인자에 에러메세지를 전달할 수도 있습니다. reject(error); });
- 프로미스가 정상 처리된 경우(上)와 에러 발생한 경우(下)의 프로미스 객체
new Promise
가 반환하는 Promise
객체는 state
, result
내부 프로퍼티를 갖는다. 하지만 직접 접근할 수 없고 .then
, .catch
, .finally
의 메서드를 사용해야 접근이 가능하다.
State
pending
(대기)executor
)가 성공적으로 작동했다면 fulfilled
(이행)로 변경이 되고, 에러가 발생했다면 rejected
(거부)로 변경.Result
undefined
executor
)가 성공적으로 작동하여 resolve(value)
가 호출되면 value
로, 에러가 발생하여 reject(error)
가 호출되면 error
로 변경.then
, catch
, finally
executor
에 작성했던 코드들이 정상적으로 처리가 되어 resolve 함수가 호출된 경우에, .then
메서드로 접근 가능
.then
안에서 리턴한 값이 Promise
면 Promise
의 내부 프로퍼티 result
를 다음 .then
의 콜백 함수의 인자로 받아오고, Promise
가 아니라면 리턴한 값을 .then
의 콜백 함수의 인자로 받아올 수 있다. Catch
executor
에 작성했던 코드들이 에러가 발생했을 경우에는 reject
함수를 호출하고 .catch
메서드로 접근할 수 있다.
Finally
executor
에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally
메서드로 접근할 수 있다.
let promise = new Promise(function(resolve, reject) { resolve("성공"); }); promise .then(value => { console.log(value); // "성공" }) .catch(error => { console.log(error); }) .finally(() => { console.log("성공이든 실패든 작동!"); // "성공이든 실패든 작동!" })
비동기 작업을 순차적으로 진행해야 하는 경우에 사용
.then
, .catch
, .finally
의 메서드들은 Promise
를 리턴하기 때문.then
을 통해 연결할 수 있고, 에러가 발생할 경우 .catch
로 처리let promise = new Promise(function(resolve, reject) { resolve('성공'); ... }); 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('성공이든 실패든 작동!'); });
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)); // promiseAll을 사용하지 않은 코드 // 같은 코드가 중복될 뿐만 아니라 총 6초가 걸린다. const result = []; promiseOne() .then(value => { result.push(value); return promiseTwo(); }) .then(value => { result.push(value); return promiseThree(); }) .then(value => { result.push(value); console.log(result); // ['1초', '2초', '3초'] }) // promise.all 사용한 코드 // 이 경우 모든 프로미스가 동시에 실행되기 때문에 총 3초가 걸린다. Promise.all([promiseOne(), promiseTwo(), promiseThree()]) .then((value) => console.log(value)) // ['1초', '2초', '3초'] .catch((err) => console.log(err));
Promise.all()
은 인자로 받는 배열에 있는 Promise
중Promise
의 state
와 상관없이 즉시 종료// 아래의 코드는 에러1이 발생하고 난 뒤로는 더이상 작동하지 않고 종료된다. 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
역시 Callback
함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.
const printString = (string) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(string); }, Math.floor(Math.random() * 100) + 1); }); }; const printAll = () => { printString('A').then((value) => { console.log(value); printString('B').then((value) => { console.log(value); printString('C').then((value) => { console.log(value); printString('D').then((value) => { console.log(value); printString('E').then((value) => { console.log(value); printString('F').then((value) => { console.log(value); }); }); }); }); }); }); };
JavaScript는 ES8에서 async
/await
키워드를 제공하기 시작!
이를 통해 복잡한 Promise
코드를 간결하게 작성할 수 있게 되었다.
async
키워드를 사용하고 async
함수 내에서만 await
키워드를 사용하면 된다.await
키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 된다. // 함수 선언식 async function funcDeclarations() { await 작성하고자 하는 코드 ... } // 함수 표현식 const funcExpression = async function () { await 작성하고자 하는 코드 ... } // 화살표 함수 const ArrowFunc = async () => { await 작성하고자 하는 코드 ... }