노드에서는 동기와 블로킹이 유사하고 비동기와 논 블로킹이 유사하다.
Node.js에서는 대부분의 메서드들이 비동기 방식으로 처리한다.
하지만 Javascript가 기본적으로 단일 스레드 (Single thread)이기 때문에 한 번에 한 작업만 수행한다.
What is Single Thread ?
: 프로세스 내 하나의 쓰레드가 하나의 요청만을 수행하는 것을 말한다.
즉, 들어온 요청이 돌아가고 있을 때 다른 요청을 함께 수행할 수 없다 !
Node.js에서는 싱글 스레드 논블로킹 모델을 적용해서 사용.
따라서 하나의 Thread이지만 이 Thread를 이용해 비동기 처리를 하지 않고,
논블로킹 I/O 작업을 통해 동시에 들어온 많은 요청을 비동기적으로 처리할 수 있다.
논블로킹 I/O 는 Event-driven (이벤트 기반) 으로 동작 가능하다.
Event-driven는 또 뭐여 ?
: 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미한다.
이를 도와주는 기능이 총 3가지가 있다 !
어떤 이벤트 발생 시, 특정 시간이 지난 뒤 시스템에서 호출하는 함수이고 다른 함수의 인자로 사용한다.
//* Callback Function
console.log("Ready ...");
setTimeout((): void => {
console.log("Set ..."); //3초 뒤에 출력
}, 3000);
console.log("Go !");
//* 출력
//Ready ...
//Go !
//Set ...
이처럼 콜백 함수를 통해 비동기 처리를 할 수 있다.
하지만 ! 이런 callback function을 이용한 비동기 처리는, 콜백 지옥을 만들어낸당 ㅎㅎㅎ..
이를 해결하기 위해 ES2015부터는 Promise 를 사용한다.
Promise에는 총 3단계의 상태가 존재한다.
const condition : boolean = false; // true면 resolve, false면 reject
//* 최초 생성 시점
const promise = new Promise((resolve, reject) => {
if (condition) {
resolve("우와 Promise다 !");
} else {
reject(new Error("비동기 처리 도중 실패!"));
}
});
/*
다른 코드 들어갈 수 있다
!
!
!
*/
//* 비동기 처리 성공(then), 비동기 처리 실패(catch)
//resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.
promise
.then((resolveData): void => console.log(resolveData))
.catch((error): void => console.log(error)); //현재 condition 의 값이 false 이므로, error("비동기 처리 도중 실패!")를 출력
new Promise로 프로미스를 생성할 수 있으며, 그 내부에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는다.
프로미스 내부에서 resolve가 호출되면 -> then이 실행 / reject가 호출되면 catch가 실행
finally 부분은 성공/실패 여부와 상관없이 실행
resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.
resolve('성공')이 호출되면 then의 message가 '성공'이 됨.
reject('실패')가 호출되면 catch의 error가 '실패'가 됨.
위 코드에서 condition 변수를 true로 바꿔보면 catch에서 message가 로깅된다.
즉, 프로미스를 쉽게 설명하자면, 실행은 바로 하되 결과값은 나중에 받는 객체이다.
결과값은 실행이 완료된 후 then이나 catch 메서드를 통해 받는다.
여러 개의 promise 를 연결해서 사용할 수 있다.
앞서 확인했던 <Promise>.then()과 <Promise>.catch()를 이용하면 된다.
아침에 일어나서 어렵게.. 어렵게.. 양치를 하는 나를 Promise Chaining을 이용해 만들어보자
//* 아침에 어렵게,, 어렵게,, 일어나는 나를 표현한 함수
const me = (callback: () => void, time: number) => {
setTimeout(callback, time);
};
//* 기상
const wakeUp = (): Promise<string> => { //Promise 객체를 반환하는 함수
return new Promise((resolve, reject) => {
me(() => {
console.log("[현재] 일어남");
resolve("일어남");
}, 1000);
});
};
//* 화장실 감
const goBathRoom = (now: string): Promise<string> => {
return new Promise((resolve, reject) => {
me(() => {
console.log("[현재] 화장실로 이동함");
resolve(`${now} -> 화장실로 이동함`);
}, 1000);
});
};
//* 칫솔과 치약을 준비함
const ready = (now: string) : Promise<string> => {
return new Promise((resolve, reject) => {
me(() => {
console.log("[현재] 칫솔과 치약을 준비함");
resolve(`${now} -> 칫솔과 치약을 준비함`)
}, 1000);
});
};
//* 양치함
const startChikaChika = (now: string) : Promise<string> => {
return new Promise((resolve, reject) => {
me(() => {
console.log("[현재] 양치함");
resolve(`${now} -> 양치함`)
}, 1000);
});
};
//* 나 자신한테 칭찬함
const goodjob = (now: string) : Promise<string> => {
return new Promise((resolve, reject) => {
me(() => {
console.log("[현재] 나 자신에게 칭찬중");
resolve(`${now} -> 칭찬중`)
}, 1000);
});
};
wakeUp() //resolve가 chaining 되어서 화살표가 이어져서 출력된다. (now 값에 문자열들이 추가되고, 추가되고,,,)
.then((now) => goBathRoom(now))
.then((now) => ready(now))
.then((now) => startChikaChika(now))
.then((now) => goodjob(now))
.then((now) => console.log(`\n${now}`)); //출력값: 일어남 -> 화장실로 이동함 -> 칫솔과 치약을 준비함 -> 양치함 -> 칭찬중

여기서 주목할 값은 Promise의 resolve가 넘겨주는 인수 now 값이다.
이 now 값은 결국 Promise.then() 함수가 불릴 때 사용되는데, 여기서 chaining 이 이뤄지는 것을 볼 수 있다 !
이해를 쉽게 하기 위해 그림을 그려서 나타내보았다.

resolve 말고도 reject에도 체이닝이 그럼 가능할까 ?..
다음 코드를 돌려보자 !
Promise.resolve(true)
.then((response) => {
throw new Error("비동기 처리 중 에러 발생!");
})
.then((response) => {
console.log(response);
return Promise.resolve(true);
})
.catch((error) => {
console.log(error.message); //출력값: "비동기 처리 중 에러 발생!"
});
여러개의 프로미스 체인 중 하나라도 reject되면 바로 마지막에 달린 catch()로 내려가서 에러를 처리한다. 불필요하게 나머지 프로미스까지 차례차례 확인하지 않는다.
async는 ES2017부터 제공되고 알아두면 엄청나게 편리한 기능이다.
Promise가 콜백 지옥을 해결했다고 하지만 then과 catch가 계속 반복되기 때문에 여전히 코드가 장황하다. 이를 async/await 문법으로 프로미스를 사용하여 깔끔하게 줄일 수 있다.
async와 await를 이용하여 함수를 선언하고 표현하는 방법은 다음과 같다
//* async - await
//함수 선언식
async function foo1() {
}
//함수 표현식
const foo2 = async () => {
}
다음 코드로 왜 async/await를 알고 있어야 하는지 살펴보자 !
//* 이전에 치카치카 코드와 비슷한 Promise를 이용한 비동기 처리 코드
// 보기에 복잡해보이는 코드,,
let asyncFunc1 = (something: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`resolved ${something} from func1 ...`);
}, 1000);
});
};
let asyncFunc2 = (something: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`resolved ${something} from func2 ...`);
}, 1500);
});
};
const promiseMain = (): void => {
asyncFunc1("test")
.then((resolveData: string) => {
console.log(resolveData);
return asyncFunc2("testttt")
})
.then((resolveData: string) => {
console.log(resolveData);
});
};
promiseMain();
//resolved test from func1 ...
//resolved testttt from func2 ...
promiseMain 함수를 보자. 정확히 어떤 데이터가 어떤 시점에서 출력되는지 이해하기 어렵다..
이 promiseMain 함수를 async/await로 바꾼다면 ?!?!??!
const main = async (): Promise<void> => {
let result = await asyncFunc1("wow!");
console.log(result);
result = await asyncFunc2("holy moly");
console.log(result);
};
main();
//resolved wow! from func1 ...
//resolved holy moly from func2 ...
위와 같이 더 직관적이고 깔끔하게 쓸 수 있다.
아래 코드와 같이 for문과 async/await문을 같이 써서 프로미스를 순차적으로 실행할 수 있다. for문과 함께 쓰는 것은 노드 10 버전부터 지원하는 ES2018 문법이다.
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
(async () => {
for await (promise of [promise1, promise2]) {
console.log(promise);
}
})();
for await of 문을 이용해서 프로미스 배열을 순회하는 코드이다. async 함수의 반환값은 항상 Promise로 감싸진다.
따라서, 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여 처리할 수 있다.