노드에서는 동기와 블로킹이 유사하고 비동기와 논 블로킹이 유사하다.
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을 붙여 처리할 수 있다.