자바스크립트 비동기(Asynchronous) 처리를 간단히 정리해보자.
자바스크립트는 기본적으로 동기 방식(Synchronous)이며 싱글쓰레드이다. 동기라 함은, 기존의 코드가 끝나야 그 다음 코드로 넘어간다는 뜻이다.
동기 방식의 문제점은 어떤 실행문이 데이터베이스에서 데이터를 불러오는 등의 이유로 시간이 오래 걸릴 경우 나머지 코드를 실행하지 못하고 막히게 된다는 것이다. 즉 나머지 코드를 블록한다.
비동기 방식은 지금 실행하고 나중에 끝낸다고 생각하면 된다. 즉 실행문을 수행은 하되 나머지 코드를 블록하지 않는다.
자바스크립트는 싱글 쓰레드이지만, async 함수를 사용하면 기존의 쓰레드가 아닌 브라우저의 다른 곳에서 실행하고 callback 함수를 보낸다. 그래서 쓰레드는 나머지 코드를 실행할 수 있고, async 함수가 끝나면 callback 함수를 부른다.
쉽게 setTimeout()
을 예시로 알아보자.
아래의 경우 출력문이 어떻게 될까?
console.log(1);
console.log(2);
setTimeout(() => {
console.log('timeout!');
}, 3000);
console.log(3);
console.log(4);
출력문 1?
1
2
timeout!
3
4
출력문 2?
1
2
3
4
timeout!
답은 2번이다. setTimeout
은 비동기이기 때문이다.
앞서 동기 방식의 문제를 설명할 때 데이터베이스에서 데이터를 불러오는 등
이라는 말을 했다. 클라이언트, 즉 브라우저는 주로 HTTP Request로 서버에서 데이터를 불러온다. 그리고 보통 JSON 형식의 데이터를 서버로부터 응답받는다. JSON은 데이터 교환 포맷으로, 풀어쓰면 JavaScript Object Notation 이다.
이전에는 HTTP Request를 보낼 때 XMLHttpRequest
를 사용했다. 그리고 state의 변화에 따라 데이터를 받았는지 유무를 판단하고 callback을 호출했다.
const getUserInfo = (callback) => {
const request = new XMLHttpRequest();
request.addEventListener('readystatechange', () => {
if(request.readyState === 4 && request.status === 200) {
callback();
}
});
request.open('GET', API_URL);
request.send();
}
getUserInfo((data) => {
console.log(data);
});
이 방법은 요즘 잘 사용하지 않고(요즘은 주로 fetch 사용함), 성공했을 경우와 실패했을 경우 callback 함수가 다르면 각기 다른 함수를 파라미터로 작성해야 하는 등 코드가 복잡하다.
이 쯤에서 Promise에 대해 짚고 넘어가자.
Promise는 비동기 연산의 최종 완료 또는 실패와 그 결과 값을 나타내는 객체이다.
Promise는 두 가지 결과 response
와 reject
를 가지고 있다. 성공적으로 완료 되었을 때는 response
를, 실패했을 때는 reject
이다.
let myPromise = new Promise((resolve, reject) => {
if (succeed) {
resolve(value);
} else {
reject();
}
});
myPromise.then((value) => {
// 성공
}, (err) => {
// 실패
})
위처럼 .then()
을 사용해서 첫번째 인자로는 성공했을 때 실행하는 함수, 두번째 인자로는 실패했을 때 실행하는 함수를 작성할 수는 있지만, 보통 이렇게 안한다.
Promise를 반환 받은 후, 성공했을 때는 .then()
을 사용해서 value 값을 받고, 실패한 경우 .catch()
를 사용해서 error 값을 받는다.
myPromise
.then(value => console.log(value))
.catch(err => console.log(err))
이제 fetch
에 대해 알아보자. Fetch는 XMLHttpRequest보다 더 간결한 코드를 작성할 수 있다. fetch
는 항상 Promise를 반환하고, 에러가 있을 시 error를, 성공시에는 Response 객체를 반환한다.
테스트를 하기 위해 JSONPlaceholder의 가상 API를 사용해보자.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => console.log(response))
status
프로퍼티를 이용해서 네트워크 상태를 확인할 수 있다. 그런데 데이터는 어디있을까? 우리가 받으려는 데이터는 아래와 같다.
이렇듯 json 데이터를 받으려면 json 메소드를 사용하면 된다.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
위에 .then을 붙여서 쓴 것을 눈치챘을 것이다. 이를 프로미스 체이닝(Promise chaining)이라고 한다. 설명을 위해 어떤 데이터를 순서대로 받아야 한다고 가정해보자. 프로미스에서 예시로 사용했던 myPromise를 편의상 사용하겠다.
myPromise(1).then(data => {
console.log(data);
myPromise(2).then(data => {
console.log(data);
myPromise(3).then(data => {
console.log(data);
})
})
})
암담하다. 이런식으로 접근하는 사람은 없길 바란다.
첫번째 데이터를 받고 그 다음으로 두번째, 세번째 이어나갈수야 있다. 하지만 코드가 복잡하지 않은가? 추가될 수록 점점 더 복잡하다. 그래서 저런식으로 사용하지 않는다. fetch를 사용한 것 처럼 then()
을 연결해서 사용한다.
그럼 fetch
예시에서 프로미스 체이닝이 어떻게 가능할까?
그 이유는 response.json()
이 Promise 객체를 리턴하기 때문이다. 앞에서 Promise 객체 값을 받기 위해서는 then
을 사용할 수 있다고 배웠다. 그래서 체이닝이 가능한 것이다.
마지막으로 Async와 Await에 대해서 알아보자.
Async 함수는 비동기 코드를 하나의 함수로 분할한 후 내부에서 await를 사용해서 프로미스 연결을 훨씬 읽기 쉽고 논리적인 방법으로 사용하게 해준다.
예시를 살펴보자.
const getTodo = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
return data;
}
getTodo()
.then(data => console.log(data));
이렇듯 비동기 방식의 함수들을 async 함수인 getTodo() 하나의 함수로 분할할 수 있다. 참고로 Async 함수는 항상 Promise를 리턴한다.
await
에서는 뒤에 있는 함수가 resolve 될 때 까지 기다렸다가 값을 변수에 저장하는 것이다.
여기서 의문이 들 수도 있다. 앞에서 비동기는 다음 코드 실행을 막지 않는다고 하지 않았나? 그러면 await
사용 시 함수가 resolve 될 때 까지 기다린다는 소리는 무슨 소리일까?
이는 Async 함수 내에서 기다린다는 뜻이지, 자바스크립트 쓰레드를 블록한다는 이야기가 아니다. 마지막 예시를 들어보자.
const getTodo = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
return data;
}
console.log(1);
console.log(2);
getTodo()
.then(data => console.log(data));
console.log(3);
console.log(4);
출력값은?
1
2
3
4
{userId: 1, id: 1, title: 'delectus aut autem', completed: false}
이다.