내가 아직 비동기적 프로그래밍, Promise, Async, Fetch에 대해 이해를 하지 못한 것을 발견하고, 다시 정리를 해야겠다는 필요성을 느껴 다시 작성한다.
비동기적 프로그래밍
자바스크립트에는 매우 일찍부터 비동기적 실행 메커니즘이 존재했지만, 자바스크립트로 만드는 소프트웨어가 점점 복잡해짐에 따라 비동기적 프로그래밍에 필요한 장치들이 추가되었다.
자바스크립트의 비동기적 프로그래밍에는 세 가지의 패러다임이 있다. 처음에는 콜백이 있었고, Promise가 그 다음 그리고 마지막은 Async await이다. (특히 최근 문법은 Async await이 중심이 되는 것으로 보인다. 익숙해져야 한다.)
우리가 비동기적 프로그래밍을 공부해야 하는 이유는 사용자의 행동이 대부분의 경우에 비동기적으로 실행되기 때문이다. 또한 자바스크립트가 기본적으로는 싱글 스레드이기에 (더욱 정확하게 얘기하자면, 사용자에게 주어지는 스레드가 싱글 스레드이기에) 실제로 멀티코어에서처럼 부드럽게 동작하는 소프트웨어를 만들기 위해서는 비동기적 관점에서 바라보고, 비동기적 프로그래밍으로 구성을 해야 한다는 것이다.
그렇다면, 어떤 때에 우리가 비동기적 프로그래밍을 써야할 지 디테일하게 알아야할 것 같다.
먼저 앞서 말한 사용자 입력을 제외하고도 세 가지의 경우를 예시로 들어볼 수 있다.
- Ajax 호출을 비롯한 네트워크 요청
- 파일을 읽고 쓰는 등의 파일시스템 작업
- 의도적으로 시간 지연을 사용하는 기능(알람 등)
Callback
콜백은 자바스크립트에서 가장 오래된 비동기적 메커니즘이다. 콜백은 간단히 말하면 나중에 호출할 함수이다. 콜백 함수 자체에는 특별한 것이 전혀 없다. 일반적인 자바스크립트 함수일 뿐이며, 약간의 특징은 콜백 함수 자체를 인자로 보내거나 객체의 프로퍼티로 사용된다. (가끔 배열에 쓰이기도 한다) 콜백은 보통 익명 함수로 사용한다.
setTimeout을 예시로 들어본다. setTimeout 함수는 콜백의 실행을 지정된 밀리초만큼 지연하는 내장 함수이다.
console.log("Before timeout: " + new Date());
function f() {
console.log("After timeout: " + new Date());
}
setTimeout(f, 3000); // 3초
console.log("I happen after setTimeout!");
console.log("Me too!");
위의 예시를 console창에서 실행하게 되면, 아래와 같은 결과를 얻게 된다.
위의 코드는 이해를 돕기 위해 명시적으로 f라는 함수를 넘겨주었지만, 일반적으로 setTimeout 함수를 사용하는 방법은 아래와 같다.
setTimeout(function(){
console.log("After timeout: "+ new Date());
},3000);
비동기적 실행에서 에러가 자주 발생하는 부분은 스코프와 클로저가 비동기적 실행에 영향을 미치는 부분이다. 함수를 호출하면 항상 클로저가 만들어지는데, 매개변수를 포함한 함수 안에서 만든 변수는 모두 무언가가 자신에게 접근할 수 있는 한 계속 존재하게 된다.
countdown을 만들고자 하는 아래의 예시를 보면,
function countdown() {
let i;
console.log("Countdown");
for (i = 5; i >= 0; i--) {
setTimeout(function () {
console.log(i === 0 ? "GO!" : i);
}, (5 - i) * 1000);
}
}
countdown();
변수 i를 for문 루프 밖에서 선언했기에 for문의 루프가 실행을 마치고 i 의 값이 -1이 된 다음에야 콜백이 실행이 된 것이다.
-1만 6번 호출된 것을 볼 수 있다.
위를 해결하기 위해선 for 문 루프 내부에 변수를 선언하면 된다.
function countdown() {
console.log("Countdown");
for (let i = 5; i >= 0; i--) {
setTimeout(function () {
console.log(i === 0 ? "GO!" : i);
}, (5 - i) * 1000);
}
}
countdown();
오류 우선 콜백
콜백함수를 사용하면 예외 처리가 어려워진다. try catch문을 쓴다고 해도 같은 블록에 포함되어 있는 함수 안에서만 동작하기 때문에 오류를 잡아내기 힘들고, 또한 콜백이 두 번이 호출되거나 아예 호출되지 않는 경우까지 생겼기 때문이다. 이런 상황에 대응하기 위해 등장한 것이 오류 우선 콜백함수이다. 오류 우선 콜백 함수는 가장 먼저 err의 값이 참인지 확인한다. err가 참이라면 파일을 읽는데 문제가 있다는 뜻이므로 콘솔에 오류를 보고하고 즉시 빠져나온다. 여기서 포인트는 꼭 빠져나와야 한다는 것이다.
const fs = require("fs");
const fname = "may_or_may_not_exist.txt";
fs.readFile(fname, function (err, data) {
if (err) return console.error(`error reading file ${fname}: ${err.message}`);
console.log(`${fname} contents: ${data}`);
});
콜백 헬
콜백함수를 사용해서 비동기적으로 실행할 수 있지만, 현실적인 단점이 존재한다. 한 번에 여러가지의 결과를 기다려야 한다면, 콜백을 관리하기가 상당히 어려워진다.
위의 이미지처럼 무한한 콜백함수로 인해 만들어지는 코드를 콜백 헬이라고 한다. 위의 이미지를 보면, 에러 처리가 왜 힘든지 알 수 있을 것이다... 이런 문제를 해결하기 위해 등장한 것이 Promise이다.
Promise
프로미스는 콜백 함수의 단점을 해결하려는 시도 속에서 만들어졌다. 중요한 포인트는 프로미스가 콜백함수를 대체는 것은 아니다!!
프로미스에서도 콜백을 사용하는 것에서 프로미스가 콜백함수를 대체할 수 없다는 확실한 증거를 확인할 수 있다. 프로미스는 콜백함수를 예측 가능한 패턴(성공 혹은 실패)으로 사용할 수 있게 해주며, 프로미스 없이 콜백만 사용했을 때 나타날 수 있는 이상한 오류들과 찾기 힘든 버그를 상당 수 해결해준다.
프로미스는 프로미스 기반 비동기적 함수를 호출하면 Promise 인스턴스를 반환한다. 프로미스는 성공(fullfilled)하거나, 실패(rejected)하거나 단 두가지 뿐이다. 프로미스는 성공 혹은 실패 중 단 한 번만 일어난다. 프로미스가 성공하거나 실패하면 그 프로미스를 결정됐다(settled)고 한다. 성공 혹은 실패가 이뤄지지 않은 경우 대기 상태(Pending)로 있게 된다
또한 프로미스는 객체이기에 어디든 전달할 수 있다. 비동기적 처리를 다른 함수에서 하고 싶다면 프로미스를 넘기기만 하면 된다.
How to make Promise
프로미스는 쉽게 만들 수 있다. resolve(성공)와 reject(실패) 콜백이 있는 함수로 새 Promise 인스턴스를 만들면 된다.
function countdown(seconds) {
return new Promise(function(resolve, reject){
for(let i=seconds; i>=0; i--){
setTimeout(function(){
if(i>0) console.log(i + '...');
else resolve(console.log("GO!"));
}, (seconds-i)*1000);
}
});
}
countdown(5)
위의 코드는 콜백 함수로 작성했던 countdown 함수를 5초를 기준으로 하여 실행되게 한 코드이다.
여기서 resolve와 reject는 함수이다. resolve를 여러 번 호출해도 결과는 첫 번째로 호출한 것만 나오게 된다. 프로미스는 성공 또는 실패를 나타낼 뿐이다.
How to use Promise
위에서 작성한 코드 countdown 함수를 쉽게 사용하는 방법은 then 핸들러를 호출하면 된다.
then 핸들러는 성공 콜백과 에러 콜백 딱 두가지의 경우만 받는다. 즉, 성공 콜백이 실행되거나 에러 콜백이 실행되거나 둘 중 하나만 실행되는 것이다. 또한 프로미스는 catch 핸들러도 지원하기에 핸들러를 둘로 나눠서 쓰는 것도 가능하다.
countdown(5).then(
function(){
console.log("countdown completed successfully");
},
function(err){
console.log("countdown experienced an error: "+ err.message);
}
)
// 위의 경우처럼 then 핸들러를 호출하여 반환된 프로미스 값을 사용하는 방법이 있고,
const p = countdown(5);
p.then(function(){
console.log("countdown completed successfully");
});
p.catch(function(err){
console.log("countdown experienced an error: "+ err.message);
})
// catch 문과 then 문 두가지를 사용할 수도 있다.
에러를 내는 상황을 코드로 작성해보고 결과를 보면, 에러를 만난다고 해서 함수가 멈추지는 않는다. reject나 resolve는 프로미스의 상태를 관리할 뿐이다.
function countdown(seconds) {
return new Promise(function(resolve, reject){
for(let i=seconds; i>=0; i--){
setTimeout(function(){
if(i===13) return reject(new Error("OMG!!!!"))
if(i>0) console.log(i + '...');
else resolve(console.log("GO!"));
}, (seconds-i)*1000);
}
});
}
countdown(15)
위의 경우를 보고 판단할 수 있는 것은 프로미스는 비동기적 작업의 성공 또는 실패 여부를 확정하는 잘 정의된 메커니즘을 제공하지만 진행 상황을 알려주지 않는다.
Promise Chain
프로미스는 또한 체인이라는 매우 편리한 기능을 사용할 수 있는데, 앞의 프로미스가 완료되면 다른 프로미스를 반환하는 함수를 즉시 호출할 수 있다.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
console.log(string)
resolve()
}, Math.floor(Math.random() * 100) + 1
)
})
}
const printAll = () => {
printString("GIVE ME THE MONEY!!!!")
.then(() => {
return printString("Noooo that's not mine!!!")
})
.then(() => {
return printString("Then you have to hide...")
})
}
printAll()
위 함수의 결과 값을 보면 앞의 프로미스가 반환되면 then 핸들러 이후 반환되는 값이 차례대로 나오는 것을 볼 수 있다.
위의 프로미스에서 또 다른 장점은 체인 어디에서든 에러가 생기면 에러가 발생한 곳에서 체인 전체가 멈추고 then 핸들러의 에러 문이 동작한다는 것이다.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
if(string === 100) return reject(new Error("That's not enough"))
console.log(string)
resolve()
}, Math.floor(Math.random() * 100) + 1
)
})
}
const printAll = () => {
printString("GIVE ME THE MONEY!!!!")
.then(() => {
return printString("Noooo that's not mine!!!")
})
.then(() => {
return printString(100)
})
.then(() => {
return printString("Then you have to hide...")
})
}
printAll()
'개발 R.I.P.' 카테고리의 다른 글
7.17 Dev.Feedback (API, POSTMAN) (0) | 2021.07.17 |
---|---|
7.16 Dev.Feedback ( 비동기적 프로그래밍 # B Fetch, Async) (1) | 2021.07.16 |
7.09 Dev.Feedback (Redux) (0) | 2021.07.09 |
7.07 Dev.Feedback (Ajax) (0) | 2021.07.07 |
7.06 Dev.Feedback (프로그래밍 패러다임) (0) | 2021.07.06 |