
나는 이 전에는 JavaScript로 ‘prompt-sync’라는 외부 모듈을 이용해서 사용자의 입력을 받아보았다.
이번에는 외부 모듈이 아닌 내부 모듈 ‘readline’을 이용해 볼 생각이다.
const readline = require('readline'); // readline 모듈 불러오기
const rl = readline.createInterface({ // 인터페이스 생성
input: process.stdin, // 표준 입력으로 입력받기
output: process.stdout // 표준 출력으로 출력하기
});
rl.on('line', (line) => { // rl.on('line', (line))으로 한 줄씩 입력을 받아 line 변수에 저장
console.log(line); // 입력 값 출력
rl.close(); // 종료
})
rl.on('close', (close) => { // 종료 시 실행
console.log('Goodbye!');
process.exit(0);
})
> hi // 입력
hi // 입력값 출력
Goodbye! //
‘readline’ 모듈은 이런 방식으로 동작한다.
그래서 나는 다음과 같은 방식으로 입력을 받아 변수에 저장하고, 그 후에 처리를 하려고 하였다.
let input;
rl.on('line', (line) => {
input = line;
rl.close();
}
console.log(input);
undefined
> hi
그런데 결과가 다음과 같이 내 예상과는 다르게 출력되었다!
마치 입력을 먼저 받고 console.log(input); 출력이 실행되는 것이 아닌 출력이 먼저 되고 입력을 다음에 받는 듯이 보였다.
이러한 이유는 무엇일까?
위와 같이 출력되는 이유는 ‘readline’모듈은 비동기 방식을 기본으로 하기 때문이다.
아직 코딩만 조금 해보고 깊이 있는 지식이 없는 나는 동기와 비동기의 말은 들어봤어도 깊이있는 지식은 잘 알지 못하였다.
그에 대해 여기저기 찾아보고, 관련 영상도 찾아보았다.
이 글을 쓰는 데에는 "드림코딩"님의 "비동기 처리 강의" 11~13강이 많은 도움이 되었다!
동기적(synchronous)이라는 것은 '동시에 일어난다', 비동기적(Asynchronous)이라는 것은 '동시에 일어나지 않는다' 라는 말이다.
카페의 진동벨을 예로 들어 설명할 수 있다.
카페에 진동벨이 없고, 손님이 줄을 서서 주문 후 커피를 받고 나오고, 다음 손님이 주문을 하고 커피를 받고 나오고,, 이러한 방식으로 처리가 된다면 이것은 동기적이라고 한다.
카페에 진동벨이 있어 손님의 주문을 받고, 바로 다음 손님의 주문을 받고,, 손님은 진동벨이 울리면 받으러가는, 주문 후 음료가 나오는 사이에도 계속 주문을 받을 수 있는 이러한 방식으로 처리가 된다면 이것은 비동기적이라고 한다.
이러한 점은 웹에서도 중요한 개념이다.
만약 회원이 로그인을 했을 때 그 회원 정보를 받아오는데 과장해서 3초 정도가 걸린다면 그 회원은 3초동안 아무것도 없는 흰 화면만 보게 되는 것이다.
그러나 비동기적으로 처리한다면 그 3초 동안 로딩 화면을 띄워준다던지, 다른 것들을 먼저 띄워주는 등 다음의 부가적인 처리들을 할 수 있게 해준다.
위의 readline 모듈 또한 비동기 방식이기에 사용자의 입력을 받는 동안 console.log(input);이 먼저 실행이 되어 아직 값이 없는 input은 undefined로 출력이 된 것이다.
"콜백"이란 함수에서 다른 함수를 파라미터로 받아 내부에서 그 함수를 호출해 주는 것을 말한다.
이때 함수의 파라미터로 전해지는 함수를 "콜백 함수"라고 한다.
콜백 함수는 태스크가 끝나기 전까지는 실행되지 않는다.
function hiLog() {
console.log("hi");
}
function print(func) {
setTimeout(func, 100);
}
print(hiLog);
console.log("hello");
hello
hi
setTimeout은 함수와 시간(ms)를 받아 그 시간 뒤에 받은 함수를 실행해주는 함수이다.
hiLog가 콜백 함수가 되는 것이고 setTimeout은 그 콜백 함수를 100ms(0.1초) 후에 사용하는 함수인 것이다.
그래서 hello가 먼저 출력되고 0.1초 후에 hi가 출력이 된다.(비동기적)
JavaScript는 기본적으로 동기적이다.
호이스팅(변수와 함수의 선언들이 가장 위로 올라가서 먼저 실행되는 것) 이후 라인을 한 줄 한 줄 읽으면서 실행한다.
콜백 함수는 이러한 JavaScript를 위처럼 비동기적으로 작성할 수 있게 해준다.
Promise는JavaScript에서 콜백 함수 대신 비동기를 간편하게 처리할 수 있는 object이다.
Promise의 상태에는 3가지가 있다.
1. pending: 프로미스가 만들어진 후 수행해야하는 일이 진행 중임
2. fulfilled: 수행이 완료됨
3. rejected: 수행이 어떠한 이유로 실패함
Promise의 주체에는 2가지가 있다.
1. Producer: 원하는 기능을 수행해서 해당하는 데이터를 만들어냄
2. Consumer: 만들어진 데이터를 소비함
const promise = new Promise((resolve, reject) => {
console.log('start!');
setTimeout(() => console.log("end"), 3000);
});
console.log('end?');
start!
end?
end
위와 같이 promise는 그 줄이 실행 된 순간 작업을 시작한다.
이처럼 Promise는 우리가 전달한 executor들이 바로 실행되기 때문에 서버에서 사용할 때는 불필요한 통신을 하지 않도록 조심해야 한다.
// 1. Producer
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result')
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('error')
}, 1000);
});
// 2. Consumer: then, catch, finally
promise1
.then(value => {
console.log(value);
})
.finally(() => {
console.log('end');
})
promise2
.catch(error => {
console.log(error);
})
.finally(() => {
console.log('end');
})
위처럼 Promise의 Producer는 resolve, reject로 fulfilled와 rejected 상태를 다룬다.
또한 Consumer는 resolve일 경우 then, reject일 경우 catch로 받아온 결과를 다룰 수 있다.
finally는 fulfilled, rejected 상관 없이 마지막에 항상 실행된다.
const fetchNumber = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
})
// then은 값을 전달할 수도 있지만 Promise를 다시 전달하기도 함
fetchNumber
.then(num => num * 2)
.then(num => num * 3)
.then(num => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(num -1), 1000);
});
})
.then(num => console.log(num));
5 // 총 2초 소요
then은 값을 전달할 수도 있지만 위와 같이 Promise를 다시 전달해 또 then으로 받아올 수 있다.
const getHen = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve('🐓'), 1000);
});
const getEgg = hen =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${hen} => 🥚`), 1000);
});
const cook = egg =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${egg} => 🍳`), 1000);
})
getHen() // 닭을 resolve
.then(getEgg) // 닭을 받아 계란을 resolve. 이 표현은 .then(hen => getEgg(hen)) 와 같음
.then(cook) // 계란을 받아 계란후라이를 resolve
.then(console.log); // 결과를 출력
🐓 => 🥚 => 🍳
위와 같이 요리 과정을 표시해주는 Promise chainning이 있다.
const getHen = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve('🐓'), 1000);
});
const getEgg = hen =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
});
const cook = egg =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${egg} => 🍳`), 1000);
})
getHen()
.then(getEgg)
.catch(error => {
return `🥖`;
})
.then(cook)
.then(console.log);
🥖 => 🍳
중간에 에러를 뱉는다면 다음과 같이 catch 후 다른 것을 리턴해 이어나갈 수 있다.
async와 await은 Promise를 좀더 깔끔하게 해준다.
하지만 무조건적으로 async와 await을 사용하는 것이 좋은 것은 아니고, Promise를 써야하는 경우도 분명히 있다고 한다.
function fetchUser() {
return new Promise((resolve, reject) => {
resolve('SongJS');
});
}
const user = fetchUser();
user.then(console.log);
console.log(user);
Promise { 'SongJS' } // console.log(user);의 결과(비동기로 먼저 실행됨)
SongJS // user.then(console.log);의 결과
위와 같은 코드를 async로 고쳐보자.
async function fetchUser() {
return 'SongJS';
}
const user = fetchUser();
user.then(console.log);
console.log(user);
Promise { 'SongJS' }
SongJS
결과가 같은 것을 볼 수 있다.
이와 같이 async는 자동으로 Promise를 만들어 준다.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return '🍎';
}
async function getBanana() {
await delay(3000);
return '🍌';
}
/* getBanana() 함수는 아래와 같다
function getBanana() {
return delay(3000)
.then(() => '🍌');
} */
위와 같은 함수들을 이용하기 위해 다음과 같은 함수를 이용할 수 있다.
function pickFruits() {
return getApple().then(apple => {
return getBanana().then(banana => `${apple} + ${banana}`);
});
}
과연 잘 짠 함수일까?
아니다.. 이 함수는 콜백 지옥이 떠오른다.
Promise도 너무 과도한 chainning을 하면 좋지 않다.
아래와 같이 함수를 개선할 수 있다.
async function pickFruits() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
코드가 참 깔끔해졌음을 볼 수 있다.
getApple()의 실행을 기다리고 받아온 후 getBanana()가 실행, 그 후 return한다.
깔끔해졌지만 아직 시간적인 개선은 되지 않았다.
async function pickFruits() {
const applePromise = getApple();
const bananaPormise = getBanana();
const apple = await applePromise;
const banana = await bananaPormise;
return `${apple} + ${banana}`;
Promise 설명했을 당시 Promise는 일단 실행이 되고 비동기적으로 다음 라인으로 차례를 넘겨줌을 알 수 있다.
그래서 일단 await이 아닌 Promise로 getApple과 getBanana를 실행시켜 준 후, await으로 둘 다 받아졌을 때 리턴을 해줄 수 있다.
이러한 방법으로 위의 코드들보다 리턴 시간을 반이나 줄일 수 있다.
function pickAllFruits() {
return Promise.all([getApple(), getBanana()])
.then(fruits => fruits.join(' + '));
}
pickAllFruits().then(console.log);
위의 Promise.all 방법은 실행시키고자 하는 함수들을 배열로 받아 동시에 실행시키고, 모든 값을 배열로 다시 내보내준다.
그 후 받은 배열을 문자열로 join 해주면 위와 같은 결과를 얻을 수 있다.
코드가 참 깔끔해졌다!
// userInput.js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function getUserInput() {
return new Promise((resolve, reject) => {
rl.on('line', (line) => {
resolve(line);
rl.close();
})
})
}
module.exports = {getUserInput};
// main.js
const Input = require('./UserInput');
async function main() {
const input = await Input.getUserInput();
console.log(input);
}
main();
위와 같이 readline 모듈을 동기적으로 사용할 수 있었다.
input이 undefined가 아닌 입력값으로 잘 출력되었다!
잘 만들었는지는 모르겠지만 내가 원하는 대로 작동하긴 했다. ㅎㅎ
조금 시간이 들은 공부였지만 나름 중요한 지식을 배운 것 같다!