자바스크립트 비동기 처리 - Promise, async, await

jwhan·2022년 3월 23일
0

JavaScript

목록 보기
2/2
post-thumbnail

자바스크립트 비동기(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

앞서 동기 방식의 문제를 설명할 때 데이터베이스에서 데이터를 불러오는 등 이라는 말을 했다. 클라이언트, 즉 브라우저는 주로 HTTP Request로 서버에서 데이터를 불러온다. 그리고 보통 JSON 형식의 데이터를 서버로부터 응답받는다. JSON은 데이터 교환 포맷으로, 풀어쓰면 JavaScript Object Notation 이다.

XMLHttpRequest를 사용하는 방법

이전에는 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는 비동기 연산의 최종 완료 또는 실패와 그 결과 값을 나타내는 객체이다.

Promise는 두 가지 결과 responsereject를 가지고 있다. 성공적으로 완료 되었을 때는 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에 대해 알아보자. 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))

Promise Chaining

위에 .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에 대해서 알아보자.

async

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

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}

이다.


참고
Asynchronous JavaScript by The Net Ninja

0개의 댓글