JS는 비동기적으로 처리되는 언어이다. 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 처리한다는 뜻이다.
console.log('Hello');
setTimeout(function() {
console.log('Bye');
}, 3000);
console.log('Hello Again');
JS가 비동기적으로 처리되는 걸 모른다면
hello => 3초 후 Bye, => Hello Again 의 순서대로 출력될 것이라고 예상할 것이다.
실제론 hello => Hello Again => 3초 후 Bye 이다.
비동기 처리는 굉장한 장점이라고 생각된다. 일상 생활에서도 빗대어 생각해보면 전자레인지에 밥을 데워놓고 가만히 앉아서 기다릴게 아니라 반찬을 꺼내놓으면 좋지 않은가! 허나 종속적인 함수가 있다면 문제가 발생할 수 있다.
const makeName = (lastName, firstName) => {
let fullName;
setTimeout(() => {
fullName = lastName + firstName;
}, 3000);
return fullName;
};
const introduce = (fullName) => {
console.log(`안녕하세요. 제 이름은 ${fullName}입니다.`);
};
const fullName = makeName("홍", "길동");
introduce(fullName);
이름을 만드는 makeName 함수를 의도적으로 setTimeout을 이용해 시간이 걸리게 만들었다.(비동기 작업이 되었다.) 그럼 introduce가 먼저 실행이 될테고 "안녕하세요. 제 이름은 undefined입니다." 라는 출력값이 나오게 된다. 이런 비동기 처리에서 발생되는 문제를 해결할 수 있는 것이 콜백함수이다.
콜백함수는 함수 안에서 실행되는 또 다른 함수로, 파라미터로 전달되는 함수를 말한다. '다 끝나고 실행이 될때 불러줘~!' 라는 의미의 콜백이다.
function introduce(lastName, firstName, callback) {
let fullName = lastName + firstName;
callback(fullName);
}
introduce("홍", "길동", (name) => {
console.log(`안녕하세요. 제 이름은 ${name}입니다.`);
});
이제 알맞게 출력된다. callback은 언뜻 보기엔 단순한 변수같지만 함수이다.(TS의 필요성을 이렇게..) 함수를 실제 사용하는 부분에선 화살표 함수를 넣어서 사용했다.
function introduce (lastName, firstName, callback) {
var fullName = lastName + firstName;
callback(fullName);
}
function say_hello (name) {
console.log("안녕하세요 제 이름은 " + name + "입니다");
}
function say_bye (name) {
console.log("지금까지 " + name + "이었습니다. 안녕히계세요");
}
introduce("홍", "길동", say_hello);
// 결과 -> 안녕하세요 제 이름은 홍길동입니다
introduce("홍", "길동", say_bye);
// 결과 -> 지금까지 홍길동이었습니다. 안녕히계세요
콜백의 유용함을 보여주는 코드이다. introduce에서 만든 fullName을 이용해 콜백으로 넣는 함수만 바꿔 다른 동작을 쉽게 할 수 있다.
콜백 지옥은 콜백함수의 호출이 반복되어 가독성을 떨어트리는 코드를 말한다.
function add(x, callback) {
let sum = x + x;
console.log(sum);
callback(sum);
}
add(2, (result) => {
add(result, (result) => {
add(result, (result) => {
console.log("finish");
});
});
});
// 4, 8, 16, finish
여기서 프로미스의 등장이다.
프로미스는 JS 비동기 처리에 사용되는 객체이다. new 연산자를 사용하고 콜백으로 resolve, reject 각각 성공, 실패했을 때 실행할 콜백을 받는다. (콜백의 인자로 콜백을 받는다!)
function add(x) {
let sum = x + x;
console.log(sum);
return new Promise((resolve, reject) => {
resolve(sum);
});
}
add(2)
.then((result) => add(result))
.then((result) => add(result))
.catch((err) => console.log(err))
.finally(() => {
console.log("끝");
});
// 4, 8, 16, 끝
콜백 지옥을 탈출했다. 이제 then 지옥인가? 하지만 이건 이해하기 어려움을 주는 수준은 아니다!
catch는 오류 즉 reject가 실행되거나 다른 오류가 발생하면 실행되고 finally는 마지막에 실행된다.
function add(x) {
let sum = x + x;
console.log(sum);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(sum);
}, 2000);
});
}
add(2)
.then((result) => add(result))
.then((result) => add(result))
.catch((err) => console.log(err))
.finally(() => {
console.log("끝");
});
setTimeout을 추가했다. 2초 간격으로 같은 값이 출력된다.
함수를 나눠서 예제를 들어보겠다.
const f1 = () => {
return new Promise((res, rej) => {
setTimeout(() => {
res("1번 주문 완료");
}, 1000);
});
};
const f2 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("2번 주문 완료");
// rej("xxx");
}, 3000);
});
};
const f3 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("3번 주문 완료");
}, 2000);
});
};
f1()
.then((res) => f2(res))
.then((res) => f3(res))
.then((res) => console.log(res))
.catch(console.log)
.finally(() => {
console.log("끝");
});
각 초간격으로 잘 나온다. 만약 중간에 f2에서 reject를 실행하게 되면 3번 주문은 실행되지 않고 바로 '끝'이 출력된다.
현재 f1, f2, f3의 작업에는 1+3+2초 즉 6초가 걸린다. 세개를 동시에 실행하면 3초면 끝날 것이다. 이럴 때 사용할 수 있는게 Promise.all이다
... 함수는 동일
Promise.all([f1(), f2(), f3()]).then((res) => {
console.log(res);
}); // [ '1번 주문 완료', '2번 주문 완료', '3번 주문 완료' ]
실행하고 3초뒤에 배열 형태로 [ '1번 주문 완료', '2번 주문 완료', '3번 주문 완료' ] 이렇게 출력이 된다. 만약 reject가 하나라도 껴있다면 어떤 데이터도 얻을 수 없다.
... 함수는 동일
Promise.race([f1(), f2(), f3()]).then((res) => {
console.log(res);
}); // 1번 주문 완료
Promise.race 말그대로 race이다 가장 먼저 끝나는 f1의 출력 값만 나온다.
then 지옥까진 아니지만 그래도 더욱 가독성이 좋게 수정할 수 있다. 바로 async & await, 함수에 async를 붙이고 내부에서 promise를 반환하는 값 앞에 await을 붙인다. 해당 프로미스가 끝날때를 기다린다는 의미이다. async가 있어야 await을 사용할 수 있다.
function add(x) {
let sum = x + x;
return new Promise((res, rej) => {
setTimeout(() => {
res(sum);
// rej(new Error("err..."));
}, 1000);
});
}
const threeTimesAdd = () => {
try {
let result1 = await add(2);
console.log(result1);
let result2 = await add(result1);
console.log(result2);
let result3 = await add(result2);
console.log(result3);
} catch (err) {
console.log(err);
} finally {
console.log("끝");
}
};
threeTimesAdd();
async의 기능이 한가지 더있다 함수를 promise를 반환하는 함수로 바꿔주는 것이다. 따라서..
async function add(x) {
let sum = x + x;
// throw new Error("err...");
return sum;
}
const threeTimesAdd = async () => {
try {
let result1 = await add(2);
console.log(result1);
let result2 = await add(result1);
console.log(result2);
let result3 = await add(result2);
console.log(result3);
} catch (err) {
console.log(err);
} finally {
console.log("끝");
}
};
threeTimesAdd();
이렇게도 작성이 가능하다. 에러는 throw 키워드로 발생시킨다.
JS는 비동기 처리를 지원하는 언어인데 이로 인해 통신에 문제가 생길 수 있다. 이를 콜백을 통해 해결했지만 콜백지옥이라는 가독성이 나쁜 패턴이 발생했고 그를 해결하기 위해 Promise가 탄생했다. 라는 이야기~~~ 이걸 이렇게 이해하기까지 많은 시간이 걸렸다..