1. 동기와 비동기(Synchronous and Asynchronous)
자바스크립트를 배우다 보면 마주치는 장애물 중 하나는 바로 비동기와 동기라는 개념이다. 이번 포스팅에서는 동기와 비동기 개념을 이해하고 비동기처리가 왜 필요한지 그리고 그 방법에대해서 알아보자.
자바스크립트는 기본적으로 맨 윗줄부터 차례로 코드가 실행된다. 예를들어 이런 코드가 있다고 생각해보자.
colsole.log("1")
colsole.log("2")
colsole.log("3")
//1,2,3이 순서대로 출력된다.
당연히 위에서부터 코드가 실행되고 콘솔에는 1,2,3 순서로 값이 출력될 것이다. 이것이 바로 동기적으로 코드실행이다. 하나의 작업이 마무리 될때까지 기다렸다가 다음 작업이 순서대로 일어난다.
‘비동기적’ 이란 개념은 동기적의 반대말이다. 즉, 순서를 보장하지 않는다는 말이 된다. 코드로 살펴보자
function one (){
setTimeout(()=>{
console.log("1");
},1000)
}
one();
console.log(2);
//2가먼저 출력되고 1이 출력된다.
‘one’라는 함수를 선언했다. 이 함수가 하는 일은 1000ms(1초)뒤에 1이라는 값을 콘솔에 출력하는 역할을 한다. 즉 바로 실행되는것이 아니라 지연(딜레이)가 있다는 뜻이다. one함수의 호출은 console.log(2)보다 먼저 되었다. 위에서 살펴봤던 동기적인 상황이라면 1초가 지난 후 ‘1’이 출력되고 이어서 ‘2’가 출력될 것이다.
하지만 콘솔에서는 2가 먼저 그다음 1초뒤 1이 나타난다. 직접 브라우저 콘솔에 찍어보면 다음과 같이 출력되는 것을 볼 수 있다.
왜 이런 현상이 발생하는 것일까?
자바스크립트는 기본적으로 멀티쓰레드를 지원하지 않는다. 때문에 동시에 여러 작업을 수행할 수 없다. 하지만 위와 같은 상황에서는 마치 멀티쓰레드를 통해 두 작업이 동시에 일어나는 것처럼 보인다. 사실 이것은 자바스크립트의 독특한 작동방식에 원인이 있다.
자바스크립트는 자체적으로 응답에 시간이 걸리는 작업들을 미뤄놓고 다른 작업을 먼저 수행하는 메커니즘이 내장되어있다. 자바스크립트 함수들은 호출 순서대로 콜스택(call stack)에 차곡차곡 쌓인다. 하지만 몇 가지 비동기적 처리를 필요로 하는 함수들은 바로 호출되어 바로 처리되는것이 아니라 잠깐 다른 곳으로 이동된다. 이 함수들이 이동한곳이 바로 콜백큐(callback que)라는 영역인데 비동기 함수들은 여기서 잠시 대기했다가 콜스택이 비워지게 되면 차례로 하나씩 수행된다.
즉, 시간이 소요될 것 같은 함수들을 따로 보관하는 장소에 두었다가 급한일을 먼저 처리한 후 하나씩 처리하는 것이다.
2. 비동기 작업이 필요한 이유
나는 Java를 먼저 접했기 때문에 자바스크립트의 비동기적인 처리방식에 적응하는게 어려웠다. java는 멀티쓰레드를 지원하긴 하지만 기본적으로 java는 호출된 메서드가 처리되기 전에는 다음 구문이 실행되지 않는다. 때문에 흐름을 파악하게 그리 어렵지 않다. 하지만 자바스크립트는 비동기적 처리와 호이스팅이슈 등 순서를 한눈에 파악하기 어렵게 하는 요소들이 많아서 순차적인 흐름을 파악하기가 쉽지 않았다.
하지만 이렇게 개발자를 헷갈리게 함에도 불구하고 자바스크립트에서 비동기 처리는 중요한위치를 차지한다. 그 이유는 바로 빠른 처리를 할 수 있도록 한다는 점이다. 자바스크립트의 가장 중요한 역할 중 하나는 서버에 요청을 하고 응답을 받아 화면에 출력하는 것이라고 할 수 있다. 수 많은 웹사이트에 요청을 날리는 것이 다반사인데 요청에는 항상 딜레이가 있다. 요청을 한다고 바로 응답을 주는 것이 아니기 때문이다.
비동기적인 처리방식은 이런 환경에서 빛을 발한다. 서버에 요청을 하고 응답을 기다리는 동안 잠시 다른 일을 먼저 처리할 수 있도록 하면 마치 동시에 여러일을 하는 것과 비슷한 역할을 하여 효율을 증대시키기 때문이다. 이런 이유로 비동기적인 처리방식은 자바스크립트에서 널리 사용되게 되었다.
3. 비동기적 처리에서 순차적인 작업을 해야할 때
비동기적인 처리를 하는 함수가 수행된 후에 순차적으로 다음 함수가 수행되어야 하는 상황이 있다고 생각해보자. 위에서 본것 처럼 setTimeout()함수는 비동기적 처리가 되는 함수이다. 때문에 호출 순서에 상관 없이 순위가 뒤로 밀리게 되어있다. 그렇다면 setTimeout은 항상 마지막에 수행될 것이다.
이런 문제를 해결하기 위한 방법이 두 가지 있다. 바로 콜백함수를 이용하는 방식과 Promise를 사용하는 방식이다.
1) 콜백함수를 통한 처리
'콜백함수'란 매개변수나 반환값으로 넘길 수 있는 함수를 말한다. Java에서는 메서드의 반환값을 매개변수로 사용할 수는 있으나 메서드 자체를 매개변수나 리턴값으로 넘길 수는 없다. 하지만 javascript에서는 가능하다. 매개변수로 함수를 넘길 수 있다는 점은 순차적인 처리를 할 수 있다는 말이 된다. 코드로 살펴보자
function one(two){
setTimeout(()=>{
console.log(1);
two();
},1000)
}
function two(){
console.log(2);
}
one(two);
비동기적으로 처리되었던 위의 예시를 약간 변경해보았다. 'two'라는 함수를 추가로 선언하여 'one'함수의 매개인자로 넣어주었다. setTimeout()이 실행되며 1초 후 '1'이 콘솔에 출력 된 후에 'two'가 호출되면서 '2'를 콘솔에 출력한다.
콘솔에 출력해보면 1,2 가 순차적으로 출력되는 것을 볼 수 있다. 순서상 나중에 수행되어야 하는 함수를 매개변수로 받아 함수 안에서 호출함으로써 순서를 조작할 수 있게 되었다.
하지만 여러단계의 순서가 있다면 어떻게 될까?
예를 들어 5단계의 순서가 있는 비동기작업이 있다고 상상해보자. 만약 콜백함수로 이것들을 처리한다면 위와같은 작업을 통해 4번의 콜백함수 연결을 통해 순서를 만들어줘야 한다. 생각만해도 번거로운 일이 아닐 수 없다.
이렇게 콜백함수가 줄줄이 연결되는 것을 '콜백체이닝(callback chaining)'이라고 하는데 심한경우 위 그림처럼 생김새가 괴상해진다.
2) Promise를 통한 처리
콜백함수 외에도 Promise를 통해 순차적으로 작업을 수행할 수 있다. 콜백체이닝과 마찬가지로 Promise도 체이닝을 이룬다. 다만 형태가 좀 더 직관적이고 요청이 정상적으로 수행되었을 때와 요청이 거절되었을 때 상황을 따로 관리한다는 점에 차이가 있다.
코드로 Promise의 형태를 살펴보자.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
});
처음 보면 형태가 복잡하게 생겼다고 느낄 수 있다. 하지만 중요한 핵심 개념은 Promise가 생성될 때 수행되는 콜백함수의 매개변수인 resolve를 통해 반환 된 값이 .then에서 사용되고 이것이 또 다시 다음 .then으로 이어진다는 것이다. 즉 반환값을 내보내고 .then으로 받아서 다시 사용하고 반환하는 순차적 고리가 Promise의 핵심이다.
resolve는 요청이 정상적으로 수행 되었을 때 반환되고 reject는 요청이 거절되었을 때 반환된다. 이처럼 상태에 따라 다른 값을 반환한다는 것이 Promise의 특징 중 하나이다.
Promise는 다른 포스팅에서 좀 더 자세하게 다룰 예정이다. 이 포스팅에서는 비동기적으로 순차적인 작업을 해야할 때 사용하는 방법으로 Promise가 있구나 정도만 알면 된다.