동기 Synchronous 프로그래밍에서 작업은 차례로 실행되며 이전 작업이 끝날 때까지 중단할 수 없다. 모든 작업은 이전 작업의 실핼이 완료될 때까지 기다려야만 한다.
반면, 비동기 Asynchronous 프로그래밍에서는 임의의 순서로 똔느 동시에 작업이 실행될 수 있다.
자바스크립트는 런타임(Node.js)에서 싱글 스레드로 작동한다. 싱글 스레드로 동작 한다는 것은 한번에 하나의 작업만 처리한다는 뜻이다. 그럼 동기 프로그램으로 동작한다면 실행이 완료될 때까지는 매우 긴 시간이 걸리게되고 매우 비효율적으로 동작하게 될 것이다.
어떻게 효율적으로 처리해야 할 까?
비동기 처리를 하면된다. 비동기처리는 이전 작업이 끝나지 않아도 다른 작업을 수행 할 수 있게 해준다.
비동기로 처리하기 위해서는 Callback, Promise, asyn, await 방법을 사용하면 자바스크립트에서 비동기로 처리가 가능하다.
비동기는 현재 코드의 실행 결과를 받지 않고 이후 코드를 수행하는 기법이다. 컴퓨팅 자원을 효율적으로 사용하는 기법이긴 하자민 정확한 순서를 지켜 수행해야 하는지를 고려해서 처리해야한다.
비동기 코드를 순서대로 싱행하는 가장 일반적인 방안으로 콜백이 있다. 콜백은 실행 가능한 함수를 인자로 전달하여, 특정 상황이 발생할 때 호출되게 하는 방식이다.
콜백은 현실 세계에서도 발견할 수 있는데, 커피숍에 가서 점원에게 커피를 먼저 주문하고 다른 것을 하고 있으면, 커피 제조가 끝난 후에 손님을 호출(callback)하는 상황으로 콜백을 볼 수 있다.
콜백 함수를 작성해 보자.
const DB = []
// 회원 가입 API 함수
// 콜백이 3중으로 중첩된 함수
function register(user) {
return saveDB(user, function(user){
return sendEmail(user, function(user){
return getResult(user);
});
});
}
// DB에 저장후 콜백 실행
function saveDB(user, callback){
DB.push(user);
console.log(`save ${user.name} to DB`);
return callback(user);
}
// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
console.log(`email to ${user.email}`);
return callback(user);
}
// 결과를 반환하는 함수
function getResult(user){
return `success register ${user.name}`;
}
const result = register({ email:"andy@test.com", password: "1234", name: "andy"});
console.log(result);
3단계로 회원 가입 API를 실행하는 1. register함수는 2.saveDB, 3.sendEamil, 4. getResult 함수를 각각 차례로 호출하여 콜백을 사용했다.
여기서 보장하는 것은 함수의 실행 순서이다.
결과

예상한 순서대로 잘 작동하는 것을 볼 수 있따. 하지만 매우 간단한 코드 인데도 콜백을 사용하니 코드가 매우 복잡해보여 가독성이 금방 매우 떨어지는 것을 볼 수 있다.
콜백을 3번만 사용해도 tab이 3번을 사용해야한다. 좀 더 복잡하고 무거운 프로그램을 만들어야 한다면? 10단, 20단 콜백문을 볼 수 있을지도 모른다.
그렇다면 지금은 짧아서 괜찮더라도 점점 알아보기가 힘든 상황이 된다. 심지어 콜백에 에러가 발생한다면? 찾기는 매우 힘들 것이다. 생각도 하기 싫다.
이러한 문제를 해결하기 위해서 Promise 객체가 2015년 ES6 버전에서 등장했다.
Promise객체는 자바스크립트에서 비동기 실행을 동기화하는 구문으로 사용한다. Promise의 뜻인 약속을 떠올리면 프로미스 개념을 이해하기 좀 쉽다.
현실 세계에서 약속은 미래에 어떤 것을 할 거라고 정하는 것이다. 약속은 이행, 거절, 대기 세가지 상태를 가질 수 있다.
자바스크립트에서 '이 코드는 어느 시점에서 실행할 거야'라고 약속하는 객체로 Promise를 사용한다.
Promise는 각각 이행, 거절, 대기 세가지 상태를 가질 수 있다. Promise는 객체로 new 연산자로 인스턴스 생성이 가능하다.

Promise 객체가 생성되면 대기 (pending)상태가 된다. resolve()함수가 실행되면 이행으로 변경되고
실패해 reject()함수가 실행되면 거절로 변한다.
const DB = []
// DB에 저장후 콜백 실행
function saveDB(user, callback){
const oldDBSize = DB.length;
DB.push(user);
console.log(`save ${user.name} to DB`);
return new Promise((resolve, reject)=>{ // 콜백 대신 Promise 객체 반환
if (DB.length > oldDBSize){
resolve(user) // 성공 시 유저 정보 반환
} else {
reject(new Error("Save DB Error!")); // 실패 시 에러처리
}
});
}
// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
console.log(`email to ${user.email}`);
return new Promise((resolve)=>{ // Promise 객체 반환, 실패 처리 없음
resolve(user);
});
}
// 결과를 반환하는 함수
function getResult(user){
return new Promise((resolve, reject)=>{
resolve(`success register ${user.name}`);
});
}
function registerByPromise(user){
//비동기 호출이지만 순서를 지켜서 실행
const result = svaeDB(user).then(sendEmail).then(getResult);
// 아직 완료되지 않아서 지연(pending)상태이다.
console.log(result);
return result
}
const myUser = { email:"andy@test.com", password: "1234", name: "andy"};
const result = registerByPromise(myuser);
result.then(console.log);
결과

각각의 함수에 있던 콜백 함수를 Promise 객체로 바꿨다. Promise는 객체이므로 new 생성자로 생성할 수 있고 그 안에 resolve, reject 함수가 있다.
성공하면 resolve를 실패하면 reject를 실행시킨다. 또한 Promise는 then(promise) 메서드가 있어서 비동기 호출이지만 Promise1.then(Promise2).then(Promise3) 이런 식으로 순서대로 호출할 수 있다.
then은 Promise 객체에만 사용할 수 있다.
then의 사용법
then(onFulfilled)
then(onFulfilled, onRejected)
then(
value=>{/*Fulfilled handler*/},
reason=>{/*rejection handler*/}
)
then은 성공했을 때, catch는 주로 실패했을 때 사용하지만
then의 파라미터를 잘 이요하면 2번째 파라미터에 실패시의 처리를 해줄 수 있다.
동시에 여러 Promise 객체를 호출해 결괏값을 받고 싶을 때는 어떻게 해야할까??
Promise.all([Promise1, Promise2, ...])처럼 사용하면 된다.
그러면 나열된 순서 상관없이 동시에 실행할 수 있다. 결과 값은 배열로 반환된다.
// 위 코드 그대로...
const myUser = { email:...}
allResult = Promise.all([saveDB(myUser), sendEmail(myUser), getResult(myUser)]);
allResult.then(console.log);
결과

이와 같은 배열의 결과 값을 얻을 수 있다.
saveDB() 함수의 로직을 약간 변경해서 에러를 발생시켜보자.
const DB = []
// DB에 저장후 콜백 실행
function saveDB(user, callback){
const oldDBSize = DB.length+1; // 이 부분 수정하여 에러발생
DB.push(user);
console.log(`save ${user.name} to DB`);
return new Promise((resolve, reject)=>{ // 콜백 대신 Promise 객체 반환
if (DB.length > oldDBSize){
resolve(user) // 성공 시 유저 정보 반환
} else {
reject(new Error("Save DB Error!")); // 실패 시 에러처리
}
});
}
// 이메일 발송 로그만 남기는 코드 실행 후 콜백
function sendEmail(user, callback){
console.log(`email to ${user.email}`);
return new Promise((resolve)=>{ // Promise 객체 반환, 실패 처리 없음
resolve(user);
});
}
// 결과를 반환하는 함수
function getResult(user){
return new Promise((resolve, reject)=>{
resolve(`success register ${user.name}`);
});
}
function registerByPromise(user){
//비동기 호출이지만 순서를 지켜서 실행
const result = svaeDB(user).then(sendEmail).then(getResult);
// 아직 완료되지 않아서 지연(pending)상태이다.
console.log(result);
return result
}
const myUser = { email:"andy@test.com", password: "1234", name: "andy"};
const result = registerByPromise(myuser);
result.then(console.log);
DB.length보다 oldDBSize의 크기가 커야한다고 명시했는데, 일부러 oldDBSize를 더 크게 해주면 Promise 로직이 실패하게 된다.

function registerByPromise(user){
//비동기 호출이지만 순서를 지켜서 실행
const result = saveDB(user)
.then(sendEmail)
.then(getResult)
.catch(error=>new Error(error));
// 아직 완료되지 않아서 지연(pending)상태이다.
console.log(result);
return result
}
위와 같이 수정해주면

좀 다르고 보기 쉬운 에러를 얻을 수 있다.
finally 메서드도 봐보자.
finally 메서드는 비동기 처리의 성공, 실패 여부와 상관 없이 마지막에 실행해주는 메서드다.
function registerByPromise(user){
//비동기 호출이지만 순서를 지켜서 실행
const result = saveDB(user)
.then(sendEmail)
.then(getResult)
.catch(error=>new Error(error))
.finally(()=> console.log("완료"));
// 아직 완료되지 않아서 지연(pending)상태이다.
console.log(result);
return result
}
위와 같이 수정해주면

완료가 잘 찍혀있는 것을 확인할 수 있다.
자바스크립트에서 비동기 처리를 하는 데 사용하는 Promise 객체는 콜백보다는 확실히 편리하다.
하지만 then()과 catch() 함수를 연결하는 체이닝 방식을 사용하기가 만만하지는 않다. 거기에 더 복잡한 로직을 추가하고 예외 처리까지 해야되는 상호아이라면 더욱 힘들어진다.
이상적으로 깔끔한 코드를 작성할 수는 있겠지만 그런 코드는 실전에서는 보기가 힘들다.
실전은 더욱 복잡하고 까다로운 로직을 수행해야하는 경우가 많기 때문이다.
Promise는 콜백보다는 깔끔하지만 잘못 사용될 수 있는 여지가 남아있따.
이러한 문제를 해결하기 위해서 async, await이 만들어 졌다!!
다음에 알아보자.