웹 서비스를 포함한 모든 응용 소프트웨어 서비스는 개발자를 위한 것이 아니다. 클라이언트를 위해 개발되어야 한다. 따라서 어떠한 서비스를 개발할 때 클라이언트가 쾌적함을 느낄 수 있는 환경을 제공하는 것이 개발자의 역할이다.
Javascript는 예로부터 사용되던 웹브라우저와 연동되는 Interpreter 프로그래밍 언어이다. 어떠한 웹 사이트에서 독자들에게 제공하는 기능이 많이 없다면 동기적으로 스크립트를 실행시켜도 무방할 것이다.
그러나 우리는 웹브라우저에서 텍스트를 입력한다. 버튼을 클릭한다. 데이터를 새로고침한다. 이 일련의 행동들, 전부 컴퓨터 입장에서는 예측할 수 없는 작동들이리라. 이 상황을 우리는 비동기적이라고 한다.
이 비동기적인 행동에 대해서 대비할 필요가 생겼다. 왜냐? 당신은 새로고침 버튼을 한 번 눌렀을 때 브라우저가 10초 동안 (응답 없음) 을 띄우다가 다시 돌아오는 꼴을 보지 못한다.
버튼을 누르면 브라우저가 파일을 로드한다. I/O 작업을 시작한다. 기다려야 한다.
버튼을 누르면 HTTP Request 네트워크 패킷을 서버에 전송한다. Response가 오기까지 기다려야 한다.
많은 데이터를 처리해야 한다. 연산이 느린 컴퓨터를 사용 중에 있다. 기다려야 한다.
위 원인들이 대표적인, 웹브라우저를 hang 상태에 놓이게 하는 주범들이다.
컴퓨터공학의 천재들은 이를 callback 함수를 이용하여 처리하는 기전을 선보였다.
가령, work() 이라는 함수가 실행되는데 10초 걸린다고 가정하자. 이 때 이 함수는 내부적으로 worker 스레드를 하나 만들어서 웹브라우저 메인 스레드가 hang에 걸리지 않도록 한다. 그리고 그 이후에 work() 함수의 후속 조치를 하는 statements들을 새로 정의된 callback 함수에 넣어서 일을 이어나가는 것이다.
function callback(result){
...do something with result...
}
function work(WORKNUM, callback){
...create a thread. if thread ends, execute callback function...
}
비동기적 행위를 하는 함수가 callback 함수를 받는 위 예시를 볼 수 있다. (완벽하진 않다.)
위 기전을 좀 더 암묵적이고 개발자 친화적으로 만든 체계가 promise가 되겠다.
ECMAScript 2015 (ES6라고도 불린다.) 에서 도입된 Promise 객체는 비동기적인 작업을 체계적으로 처리할 수 있도록 만들어졌다.
Promise의 상태는 세 가지다.
Promise 내부에서 resolve()를 실행하면 Fulfilled 상태가 되고, reject()를 실행하면 Rejected 상태가 된다.
const asynjob = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("lol");
},3000);
});
asynjob.then((result)=>{console.log(result);})
.catch((error)=>{console.error(error);});
위 예제는 setTimeout이라는, 비동기 함수를 사용한다. 3000ms 뒤에 setTimeout 내의 함수가 실행되므로 이는 비동기 함수다. (3초를 기다릴 수는 없지 않는가.)
new 키워드를 이용하여 Promise 객체를 생성하는데, 암묵적인 parameter로 resolve 함수와 reject 함수를 받는 내부 비동기 함수를 람다 함수로 넣어줬다.
이제 asynjob의 이름을 가진 Promise가 비동기 작업을 수행할 것이다. 이 때 이 Promise가 Fulfilled 상태에 들어가면 어떤 callback 함수를 호출할 지 결정하려면 .then() 을 이용하며, Rejected 상태에 들어가면 어떤 error handling 함수를 호출할 지 결정하려면 .catch() 를 사용하게 된다. (이들은 cascading이 가능하다.)
위 예제의 경우는 3초 뒤에 lol이라는 문자열을 console에 출력하게 될 것이다.
나온다.
이제는 굳이 then()과 catch()를 쓰지 않아도 된다. ECMAScript 2017에서 Promise를 활용하는 새로운 방법이 도입되었기 때문이다.
키워드는 async-await 뿐이다.
예시로 설명을 하자.
async function getSomeInfo(){
const result = await getDataFromRemoteDB();
console.log(result);
}
함수 선언자 function 앞에 async 함수를 붙여서 이 함수가 비동기 작업을 한다는 것을 선언한다. 그러면 getSomeInfo() 함수는 자동적으로 Promise를 return하는 함수가 된다.
getDataFromRemoteDB() 함수는 비동기 작업을 하는 함수이므로 Promise를 return하는 함수이다. 그렇다면 우리는 await 키워드를 사용할 수 있다. (즉, await Promise객체)
본래대로, 동기적으로 위 코드가 작동했다면 console.log의 결과는 Pending 상태를 가진 Promise 객체가 될 것이었다.
그러나 우리는 await 키워드를 사용하여 해당 Promise가 fulfilled 될 때까지 기다린다. 이후 await 아래에 있는 코드를 실행한다. 이거 완전 동기적으로 작동하는 모습이다. 음? 이러면 결국 기다리게 되는 것이 아닌가?
괜찮다. 결국 우리는 async로 선언된 함수 안에서 일을 하고 있다. 즉, 또 다시 Promise를 만드는 것이므로 메인 쓰레드와는 별개의 쓰레드에서 작업을 하게 된다.
여기서 중요한 점은, 따라서 await를 사용하기 위해선 async 지시자가 무조건 필요하다는 것이다. 이는 많은 IDE에서 오류를 띄워 미연에 방지된다.
이제 then을 사용하지 않아도 된다. 콜백 함수를 안 만들어도 된다.
catch는 어떻게 할까? reject된 Promise를 handling 하는 방법? 고전적이지만 효과적인 try-catch를 이용하면 편하다.
최근 어떤 서비스를 봐도 인터넷을 사용하지 않는 서비스는 존재하지 않는다. 많은 팀들은 서비스 로직을 구현하고 클라이언트에 전달하는 백엔드 서버를 구현해야 한다. 수 많은 비동기 작업이 있을 것이다. Javascript 언어를 기반으로 하는 Node의 express 프레임워크를 사용한다면 많이 써먹을 수 있을 것이다.