동기 vs. 비동기

요청을 순차적으로 처리하는 것을 동기적(synchronous)이라고 표현한다. 먼저 들어온 요청 A를 처리하는 동안 그 이후에 들어온 요청 B를 막고(blocking), 처리가 완료되면 B을 처리하기 때문에 B의 시작 시점과 A의 완료 시점이 같은 상황을 말하는 것이다.
이에 반해, 비동기(asynchronous)는 요청이 들어오는 것을 막지 않고(non-blocking) 처리가 완료된 요청은 바로 완료한다. 이는 먼저 들어온 요청 A의 완료 시점과 이후의 요청 B의 시작 시점이 다르다. 이 때, 처리하는 시점은 다르더라도 일단 후에 들어온 요청을 막지 않는 다는 것이 핵심이다.

이를 통한 대표적인 이점은 작업 효율이다. 만약 4개의 task를 동기적으로 작업한다면, 왼쪽의 그래프처럼 모든 task가 순차적으로 진행되고 많은 시간이 소요된다. 하지만 비동기적으로 작업하게 된다면 병렬적으로 작업이 수행되기 때문에 시간 단축이 가능하다.
JavaScript의 비동기적 실행(Asynchronous execution)이라는 개념은 웹 개발에서 유용한데, 특히 아래 작업은 비동기적으로 작동되는게 효율적이다.
- 백그라운드 실행, 로딩 창 등의 작업
- 인터넷에서 서버로 요청을 보내고, 응답을 기다리는 작업
- 큰 용량의 파일을 로딩하는 작업
비동기의 주요 사례
- DOM Element의 EventHandler
- 마우스, 키보드 입력 (click, keydown 등)
- 페이지 로딩(DOMContentLoaded 등)
- 타이머
- 타이머 API (setTimeout 등)
- 애니메이션 API (requestAnimationFrame)
- 서버에 자원 요청 및 응답
- fetch API
- AJAX (XHR)
비동기를 순차적으로 구현하는 세 가지 방법
비동기는 순차적으로 작동하는 것이 아니기 때문에 병렬적으로 요청을 처리하는 등의 이점이 있다고 했는데, 비동기를 순차적으로 구현하는 이유는 무엇일까. 예를 들어 이전 작업의 결과물을 받아 다음 작업을 진행하는 기능을 구현한다 했을 때 각각의 기능이 동시에 요청이 된다면, 즉 비동기적으로 작업이 된다면 문제가 발생할 수 있지 않을까?
비동기는 개발자가 원할 때(특정 작업이 수행 완료되거나, 브라우저 내에서 이벤트가 발생했을 때 등) 기능을 동작시키거나 서버에 요청하는 등 동작 시점과 순서를 원하는대로 제어할 수 있다는 점에서도 의미가 있다.
CallBack
이전에 사용했던 고차 함수 처럼 함수를 인자로 받아 작동시킨다면 순차적으로 처리할 수 있다.
const test1 = (func) => {
// 선행작업할 코드를 작성하고 함수를 인자로 받아 마지막에 실행
console.log(1);
func();
}
const test2 = () => console.log(2);
test1(test2);
// 1
// 2
하지만 단순한 코드가 아니라 기능이 많아지고 중첩해서 연쇄적으로 호출해야하는 복잡한 코드는 계속되는 들여쓰기 때문에 콜백 지옥(Callback Hell)이라 불리는 가독성이 떨어지고 난해한 코드가 작성되기 때문에 현재는 콜백을 이용해서 구현하는 것은 지양된다.

console.log
가 찍히는 것은 에러를 제외하면 5개 밖에 안된다Promise
Promise
는 당장 얻을 수는 없지만 가까운 미래에는 얻을 수 있는 어떤 데이터에 접근하기 위한 방법을 제공한다. 새로운 Promise 객체를 리턴하는 함수를 작성하고 실행 결과에 따라 .then
이나 .catch
메서드를 사용해 순차적으로 처리하게끔 만들 수 있다.
function returnPromise() {
return new Promise((resolve, reject) => { ... } );
}
이 때, new
키워드를 사용해 생성하는 Promise 객체에는 함수를 인자로 받고, 인자로 받는 함수는 resolve
, reject
라는 2개의 함수형 파라미터를 가진다. 이 파라미터들은 함수 인자의 바디 안에서 정상적으로 처리되거나(resolve
), 예외 사항(reject
)에 따라서 조건문등을 활용해 적절히 호출해야 한다. 즉 Promise
는 받아올 데이터나 함수의 정상여부를 가려 그에 따라 어떻게 작동할 것인지 미리 분기점을 정한다고 볼 수 있다.
객체 내에 작성된 resolve, reject에 따라 정상적으로 처리되었다면 .then
을 호출하고, 예외 사항이 발생했다면 .catch
를 호출한다. 이 때 두 메서드는 함수를 전달 받아 동작한다.
// 나눗셈 함수를 Promise로 구현
function devide(numA, numB) {
return new Promise((resolve, reject) => {
// 나누려는 두 번째 수가 0일 경우 계산불가기 때문에 에러처리(reject)
if (numB === 0) reject(new Error("Unable to devide by 0."));
// 그 외의 경우 resolve로 정상적인 계산 진행
else resolve(numA / numB);
});
}
devide(8, 2) // 4
// 정상적으로 계산이 진행된다면 then 메서드 호출
// resolve로 그 결과값을 then의 인자로 전달받아 출력
.then((result) => console.log("성공:", result))
.catch((error) => console.log("실패:", error));
devide(8, 0) // Error: ...
.then((result) => console.log("성공:", result))
// 정상적으로 계산이 안된다면 catch 메서드 호출
// reject로 에러코드 출력
.catch((error) => console.log("실패:", error));
메서드 체이닝
.then
과 .catch
는 또 다른 Promise 객체를 리턴한다. 그 말인 즉슨, .then
과 .catch
로 리턴 받은 Promise에 다시 .then
과 .catch
를 사용하여 접근이 가능하다는 뜻이다.
const sleep = (wait) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello');
}, wait);
});
};
function runPromise() {
sleep(1000)
.then((param) => {
console.log(param); // 'hello'
return "world";
})
.then((param) => {
console.log(param); // 'world'
return sleep(5000);
})
}
위의 sleep
은 Promise 객체를 리턴하는 함수로, 전달인자에 숫자를 입력받아 시간을 지연시킨다. 이후 .then
으로 연결해 전달받은 'hello'
를 매개변수로 사용해 콘솔에 출력하고, 기능 동작을 마치면 'world'
라는 문자열을 리턴한다.
그 아래에 다시 연결된 .then
을 보면 콘솔에 또 다시 출력하는 기능이 동작하는데, 이 때 앞서 리턴했던 'world'
를 전달받아 매개변수로 사용하게 된다. 이처럼 메서드를 연결하여 순차적으로 작동시킬 수 있다.
async
/ await
Promise 객체를 위해 사용하며 .then
과 .catch
를 대체하는 키워드이다. ES7(ES2017)에서 추가되었으며 비동기 코드를 마치 동기 코드처럼 보이게 작성할 수 있다.
async
는 함수 선언 앞에 붙이는 키워드로, async
키워드가 붙은 함수 내부에서만 await
을 사용할 수 있다.
function getNewsAndWeather() {
let result = {}
return fetch(newsURL)
.then((response) => response.json())
.then((data) => {
result.news = data.data
return fetch(weatherURL);
})
.then((response) => response.json())
.then((data) => {
result.weather = data;
return result;
})
}
async function getNewsAndWeatherAsync() {
let result = {};
let news = await fetch(newsURL).then((res) => res.json())
let weather = await fetch(weatherURL).then((res) => res.json())
result.news = news.data;
result.weather = weather;
return result;
}
if (typeof window === 'undefined') {
module.exports = {
getNewsAndWeatherAsync
}
}
코드를 비교해보면, await
키워드가 붙은 변수가 비동기작업인 것을 알 수 있다. 비동기로 처리해야 하는 작업에 await
키워드가 들어가면 처리가 끝날 때 까지 아래의 작업을 시행하지 않는다. .then
에 비해 간결하고 플로우를 직관적으로 확인할 수 있기 때문에 많이 사용하지만, 자칫 비동기작업이 필요한 상황 마저 동기적으로 동작시킬 수 있기 때문에 사용 용도에 따라 적절히 활용해야 한다.
타이머 API
타이머 API
setTimeout(callback, millisecond)
일정 시간 이후에 함수를 실행하며, 콜백함수와 시간(ms)이 매개변수로 들어간다.
setTimeout(function () {
console.log('1초 후 실행');
}, 1000);
clearTimeout(timerId)
setTimeout
을 종료한다. 매개변수에는 타이머의 이름이 들어간다.
const timer = setTimeout(function () {
console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨
setInterval(callback, millisecond)
일정시간의 간격을 가지고 함수를 반복 실행한다. 콜백함수와 시간(ms)이 매개변수로 들어간다.
setInterval(function () {
console.log('1초마다 실행');
}, 1000);
clearInterval(timerId)
setInterval
을 종료한다. 매개변수에는 타이머의 이름이 들어간다.
const timer = setInterval(function () {
console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨
Uploaded by N2T