비동기적으로 동작한다
= 데이터가 들어오거나, 어떤 이벤트가 일어날 때 까지 계산을 멈추고 대기하는 일이 잦다.
웹 브라우저의 자바스크립트 프로그램은 이벤트 주도적이다.
프로그램이 실제로 무엇인가 실행하기 전에 사용자가 무언가 클릭하고 탭하여 이벤트를 발생시켜야한다.
자바스크립트에서 가장 기본적인 비동기 프로그래밍은 콜백을 통해 이루어진다.
콜백을 전달받은 함수는 어떤 조건을 만족하거나 어떤 (비동기) 이벤트가 발생하면 콜백함수를 호출한다.
일정 시간이 지나면 코드를 실행하는 단순한 비동기 프로그래밍
setTimeout()
함수로 타이머를 설정할 수 있다.
setTimeout(checkForUpdates, 60000);
클라이언트 사이드 자바스크립트는 이벤트 주도적이다.
미리 지정된 계산을 수행하기 보다, 사용자가 무엇을 하길 기다렸다가 그 행동에 반응한다.
이벤트 주도 자바스크립트 프로그램
addEventListender
을 통해 콜백을 등록함.브라우저에서 실행되는 JS 는 다음 처럼 웹 서버에서 데이터를 가져올 수 있다.
callback
: 콜백을 인자로 받는다. 첫 번째 인자는 에러를, 두 번째 인자는 성공 결과를 받는다.request
: 백엔드의 API 에게 HTTP 요청을 보낸다.onload
: 응답을 받았을 때 호출할 콜백을 등록한다.onerror
: 네트워크 에러가 발생했을 때 호출할 다른 콜백을 등록한다.function getNumber(callback){
let request = new XMLHttpRequest();
request.open("GET", URL);
request.send();
request.onload = function() {
if(request.status === 200){
let currentNumber = parseFloat(request.responseText);
callback(null, currentNumber);
} else {
callback(request.statusText, null);
}
};
request.onerror = request.ontimeout = function(e) {
callback(e.type, null);
}
}
getNumber
함수에서 값을 사용하는 법
getNumber
가 현재 번호를 동기적으로 반환할 수 없다.getNumber
를 호출하는 호출자는 API 결과나 에러를 사용하는 콜백 함수를 전달한다.프로미스란...
[콜백 기반 프로그래밍의 문제점]
[프로미스가 콜백 기반 프로그래밍의 문제점을 해결한 방법]
[프로미스 작업의 특징]
setTimeout
을 대신할 수 있지만, setInterval
처럼 콜백을 반복적으로 호출하는 프로미스는 대신할 수 없다.프로미스를 반환하는 유틸리티 함수를 어떻게 사용할까?
JSON 형식인 HTTP 응답 바디를 분석하고 프로미스를 반환하는 getJSON
함수를 사용해보자.
getJSON(url).then(jsonData => {
// JSON 값을 분석하면 비동기적으로 호출될 콜백 함수
});
getJSON
: URL에 비동기 HTTP 요청을 보내고 응답을 대기하면서 프로미스 객체를 반환함then()
: 프로미스 객체의 then 인스턴스 메서드. 콜백 함수를 then 메서드에 전달하면, HTTP 응답을 받아 JSON 으로 분석하고 그 결과값을 then 에게 전달된 함수에게 전달한다.클라이언트 사이드 자바스크립트에서 이벤트 핸들러를 등록할 때 사용하는 addEventListener()
과 유사한 콜백 등록 메서드이다.
then
메서드를 여러 번 호출하면 각 콜백은 비동기 작업이 완료될 때 호출된다.then
에 등록된 각 함수는 단 한번만 호출된다.then
을 호출할 때 비동기 작업이 이미 완료된 상태더라도, then
에 전달된 콜백은 비동기적으로 호출된다. (태스크 큐에 저장되어 이벤트 루프에 의해 호출된다는 뜻인 것 같음)getJSON
이 반환하는 프라미스 객체를 변수에 할당하지 않고, 바로 then
메서드를 이어붙이는 형태를 주로 사용한다.function displayUserProfile(profile) { /* ... */ }
// 이 코드는 영어 문장과 거의 비슷하게 읽힌다.
getJSON("/api/user/profile").then(displayUserProfile);
then
메서드에 두 번째 에러 핸들링 함수를 전달해 에러 처리getJSON("/api/user/profile").then(displayUserProfile, handleProfileError);
[동기 작업]
[프로미스 기반 비동기 작업]
then
의 두 번째 인자인 함수에게 예외를 전달한다.displayUserProfile
에게 전달된다.handleProfileError
에게 전달된다.catch
메서드일반적으로 then
에게 에러 핸들러 까지 전달하진 않는다.
then
의 두 번째 인수로 전달된 에러 핸들러는 displayUserProfile
에서 발생할 에러를 캐치하지 못한다.catch
는 getJSON
에서 발생한 에러 뿐 아니라 displayUserProfile
에서 발생할 에러까지 캐치한다.catch
는 then
메서드를 호출하면서 첫 번째 인자로 null 을 전달하는 형태를 짧게 줄인 것이다.
// 다음 두 문는 완전히 동일하다.
getJSON("/api/user/profile").then(displayUserProfile).then(null, handleProfileError);
getJSON("/api/user/profile").then(displayUserProfile).catch(handleProfileError);
대기 Pending | 완료 settled |
---|---|
이행 fullfilled | |
거부 rejected |
비동기 작업 시퀀스를 then
으로 체인으로 이어서 콜백헬을 방지한다.
fetch(URL) // 1. HTTP 요청을 보낸다
.then(res => res.json()) // 2. 응답 JSON 바디를 가져온다.
.then(doc => render(doc)) // 3. JSON 분석이 끝나면 문서를 사용자에게 표시한다.
.then(rendered => { // 4. 문서 렌더링이 끝나면
cachedInDatabase(rendered); // 로컬 데이터베이스에 캐시한다.
})
.catch(err => handle(err)); // 5. 에러 핸들링
fetch("/api/user/profile").then(res => {
// 프라미스가 해석되면 상태와 헤더가 존재한다.
if(res.ok &&
res.headers.get("Content-Type") == "application/json"){
// 아직 응답 바디를 받지 못했다.
}
}
});
XMLHttpRequest 객체로 HTTP 요청을 예전 방법 대신, 새로운 프로미스 기반 Feat API 를 사용하자.
fetch()
함수 하나뿐이다.fetch()
는 URL 을 받고 프라미스를 반환한다. 프로미스를 반환하기 때문에 then
후속 처리 메서드를 사용할 수 있다.fetch()
가 반환하는 프라미스가 이행(fullfilled)되면, 프라미스는 then()
메서드에 전달한 콜백에 응답 객체를 전달한다.text()
와 json()
메서드도 가지고 있다.text()
와 json()
메서드도 프라미스를 반환한다.물론 다음과 같이 fetch
와 json()
메서드를 사용해 HTTP 응답 바디를 가져올 순 있지만, 프라미스를 콜백처럼 중첩해서 사용하는 것은 프라미스 설계 목적에 부합하지 않는다.
// Bad
fetch("/api/user/profile").then(res => {
res.json().then(profile => {
// json 으로 분석된 바디를 요청한다.
// json() 메서드 또한 프로미스를 반환하기 때문에 then 메서드를 사용할 수 있다.
// 하지만 이렇게 프라미스를 중첩해서 사용하면 설계 목적과 부합하지 않는다.
displayUserProfile(profile);
});
});
});
프로미스는 다음처럼 연속적인 체인으로 사용해야 한다.
fetch("/api/user/profile")
.then(res => res.json())
.then(profile => {
displayUserProfile(profile);
});
// 인자를 제외하고 메서드 호출만 작성하면 다음과 같다.
fetch().then().then()
위 처럼 표현식 하나에 메서드를 하나 이상 호출하는 것을 메서드 체인이라고 부른다.
fetch()
함수는 프로미스 객체를 반환.then()
은 반환된 프로미스 객체의 메서드(then)를 호출함.then()
메서드는 프로미스 객체를 반환함.then()
도 반환된 프로미스 객체의 메서드(then)를 호출함[프로미스 객체와 콜백]
then()
메서드를 호출할 때 마다 새 프로미스 객체를 반환하며 하나의 프로미스에 하나의 콜백이 등록된다.then()
에 전달된 함수가 완료되기 전에는 이행(fullfilled) 되지 않는다.fetch(URL) // 작업 1. 프로미스 1을 반환
.then(callback1) // 작업 2. 프로미스 2를 반환
.then(callback2); // 작업 3. 프로미스 3을 반환
fetch()
호출하면 fetch 는 URL 로 HTTP GET 요청을 보내는 작업 1을 수행하고 프로미스 1을 반환한다. .then
에서는 반환된 프로미스1의 then()
메서드를 호출하며, 프로미스1이 이행(fullfilled) 됐을 때 호출 할 callback1 함수를 전달했다. then()
메서드는 callback1 을 어딘가에 저장하고 프로미스 2를 반환한다. 작업 2는 callback1이 호출될 때 시작한다..then
에서는 반환된 프로미스2의 then()
메서드를 호출하며, 프로미스2이 이행(fullfilled) 됐을 때 호출 할 callback2 함수를 전달했다. then()
메서드는 callback2 을 어딘가에 저장하고 프로미스 3를 반환한다. 작업 3는 callback2이 호출될 때 시작한다.위 세 단계는 표현식을 처음 실행할 때 모두 동기적으로 일어난다.
1 단계에서 보낸 HTTP 요청이 인터넷을 통해 전송되는 동안 비동기적으로 일시 중지한다.
fetch()
호출의 비동기적 부분은 HTTP 상태와 헤더를 감싼 응답 객체를 값으로 프라미스1 을 이행(fullfilled) 한다.