비동기 JavaScript


어떤 작업은 즉시 일어납니다. 두 숫자를 더하거나, 변수를 읽거나, 페이지의 텍스트를 바꾸는 일 같은 것들입니다. 하지만 어떤 작업은 시간이 걸립니다. 타이머는 3초 동안 카운트다운을 하고, 서버에 보낸 요청은 네트워크를 거쳐 잠시 후에 돌아옵니다. JavaScript가 이런 작업을 만날 때마다 멈춰서 기다린다면, 페이지 전체가 그대로 멈춰버릴 것입니다. 느린 작업이 끝나기 전까지는 클릭도, 스크롤도, 아무것도 할 수 없게 됩니다. 하지만 JavaScript는 그렇게 동작하지 않습니다. 이 장에서는 JavaScript가 시간이 걸리는 작업을 시작하면서도 페이지를 계속 반응하게 유지하고, 결과가 준비되면 다시 돌아와 처리하는 방법을 다룹니다.
비동기가 필요한 이유
대부분의 JavaScript 코드는 위에서 아래로, 한 줄이 끝나야 다음 줄이 시작되는 방식으로 실행됩니다. 빠르게 처리되는 작업이라면 이 방식으로 충분합니다. 하지만 어떤 작업은 실제로 시간이 걸리는데, JavaScript는 그 작업이 끝날 때까지 가만히 앉아서 기다리지 않습니다. 느린 작업을 시작한 뒤 다음 줄로 넘어가고, 그 작업이 끝나면 다시 돌아와 처리합니다. 이것이 바로 **비동기(asynchronous)**라는 말의 의미입니다. 순서대로, 지금 당장 일어나는 것이 아니라 결과가 준비됐을 때 나중에 일어난다는 뜻입니다.
가장 간단한 예가 타이머입니다. setTimeout은 일정 시간이 지난 뒤 코드를 실행합니다.
console.log("Start");
setTimeout(() => {
console.log("3 seconds later");
}, 3000);
console.log("End");
// Start
// End
// 3 seconds later순서를 잘 보세요. "End"가 "3 seconds later"보다 먼저 출력되는데, 코드에서는 setTimeout 다음에 나옵니다. JavaScript는 그 줄에서 3초를 기다리지 않았습니다. 타이머를 설정하고 계속 진행한 뒤, 시간이 다 되면 지연된 코드를 실행한 것입니다.
"End"가 setTimeout 메시지보다 먼저 출력되는 것입니다. 프로미스
setTimeout은 단순히 지연시키는 용도로는 괜찮지만, 대부분의 비동기 작업은 여러분이 실제로 관심 있는 값을 만들어냅니다. 요청한 데이터일 수도 있고, 잘못됐을 때의 에러일 수도 있습니다. 이럴 때 JavaScript는 **프로미스(promise)**를 사용합니다. 프로미스는 아직 준비되지 않은 값을 위한, 자리가 미리 마련된 값입니다. 영수증과 비슷합니다. 음식을 아직 받지는 않았지만, 나중에 음식으로 바뀔 무언가를 손에 쥐고 있는 것입니다.
프로미스는 두 가지 방식 중 하나로 끝납니다. 작업이 성공하면 값과 함께 resolve되고, 실패하면 에러와 함께 reject됩니다. 각 경우에 무엇을 할지는 .then()과 .catch()로 지정합니다.
loadUser()
.then((user) => {
console.log(`Loaded ${user.name}`);
})
.catch((error) => {
console.log("Something went wrong");
});.then()은 프로미스가 resolve될 때 실행되며 그 값을 받습니다. .catch()는 프로미스가 reject될 때 실행되며 에러를 받습니다. 이 안의 코드는 결과가 도착한 뒤에 나중에 실행됩니다.
.then()으로, 에러에 대해서는 .catch()로 무엇을 할지 지정하며, 둘 다 결과가 도착한 뒤 나중에 실행됩니다. async / await
프로미스 체인은 잘 동작하지만, 거의 평범한 위에서 아래로 흐르는 코드처럼 읽히는 더 깔끔한 작성 방식이 있습니다. async와 **await**라는 두 키워드를 사용합니다.
함수를 async로 표시하면 그 안에서 프로미스를 await할 수 있습니다. await는 프로미스가 resolve될 때까지 함수를 일시 정지시켰다가, 그 값을 바로 건네줍니다. .then()이 필요 없습니다.
async function showUser() {
const user = await loadUser();
console.log(`Loaded ${user.name}`);
}위에서 아래로 읽어보세요. 사용자를 가져온 뒤 이름을 출력합니다. await 줄은 loadUser()가 끝나기를 기다렸다가 user를 건네줍니다. 함수는 그 지점에서 일시 정지하지만, 페이지의 나머지 부분은 계속 실행되므로 아무것도 멈추지 않습니다.
에러를 처리하려면 await를 try와 catch로 감싸면 됩니다.
async function showUser() {
try {
const user = await loadUser();
console.log(`Loaded ${user.name}`);
} catch (error) {
console.log("Could not load the user");
}
}프로미스가 실패하면 코드가 멈춰버리는 대신 catch 블록으로 넘어갑니다.
async로 표시하면 그 안에서 프로미스를 await할 수 있습니다. await는 프로미스가 resolve될 때까지 그 함수를 일시 정지시키고, 값을 바로 건네주므로 .then() 없이도 코드가 위에서 아래로 읽힙니다. 실패를 처리하려면 try와 catch로 감싸면 되고, 함수가 기다리는 동안에도 페이지는 계속 실행됩니다. 데이터 가져오기
여러분이 작성할 가장 흔한 비동기 작업은 서버에서 데이터를 불러오는 일입니다. 브라우저는 이를 위해 fetch를 제공합니다. URL을 넘겨주면 응답에 대한 프로미스를 반환합니다.
async function loadUsers() {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);
}await가 두 번 나오는데, 처음에는 이게 이상해 보일 수 있습니다. 첫 번째는 서버가 응답할 때까지 기다립니다. 두 번째는 그 응답의 본문을 읽어서 .json()으로 JavaScript 데이터로 바꿔주는데, 이 과정 역시 비동기입니다. 그러니까 데이터를 가져오는 일은 두 단계입니다. 응답을 받고, 그다음 그것을 읽는 것입니다.
서버는 "찾을 수 없음" 같은 에러로 응답할 수도 있습니다. fetch는 그런 경우를 자동으로 실패로 취급하지 않으므로, response.ok를 직접 확인해야 합니다.
async function loadUsers() {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
console.log("Request failed");
return;
}
const users = await response.json();
console.log(users);
}fetch(url)은 서버 응답에 대한 프로미스를 반환하며, 데이터를 가져오는 일은 두 단계입니다. 응답을 위해 await fetch(...)를 하고, 그 데이터를 읽기 위해 await response.json()을 합니다. "찾을 수 없음" 같은 서버 에러는 그 자체로 실패로 처리되지 않으므로, 본문을 읽기 전에 response.ok를 확인하세요. 두 단계 모두 시간이 걸리기 때문에 await를 사용합니다. 이벤트 루프와 병렬 작업 실행
지금까지 다룬 모든 내용은 하나의 주제로 통합됩니다. JavaScript는 느린 작업을 시작하고, 계속 진행하다가, 나중에 다시 돌아와 그 작업을 처리합니다. 이 절에서는 그 뒤에 있는 메커니즘과, 여러 비동기 작업을 잘 실행하는 패턴을 다룹니다.
JavaScript가 비동기 작업을 어떻게 추적하는지 그림으로 살펴봅시다. 지금 실행 중인 코드는 콜 스택(call stack), 즉 현재 실행 중인 작업들의 목록에 놓입니다. setTimeout을 호출하면 타이머는 카운트다운을 위해 브라우저에게 넘어가므로, 스택에 남아서 무언가를 막고 있지 않습니다. 타이머가 끝나면 그 콜백은 태스크 큐(task queue), 즉 실행을 기다리는 대기열에 들어갑니다.
**이벤트 루프(event loop)**가 이 둘을 연결해주는 부분입니다. 규칙은 하나입니다. 콜 스택에 있는 모든 것을 먼저 실행하고, 스택이 비었을 때만 큐에서 다음 태스크를 가져옵니다. 그래서 지연 시간이 0이어도 setTimeout(fn, 0)은 여전히 다른 코드보다 나중에 실행됩니다. 콜백은 현재 코드가 끝날 때까지 큐 안에서 기다려야 합니다.
console.log("first");
setTimeout(() => console.log("third"), 0);
console.log("second");
// first
// second
// third지연 시간 0은 "지금 당장"을 의미하지 않습니다. "현재 코드가 끝나는 즉시"를 의미합니다.
setTimeout(fn, 0)이 여전히 나머지 코드 뒤에 실행됩니다. 지연 시간 0은 "지금 당장"이 아니라 "현재 코드가 끝나는 즉시"를 의미합니다. 앞으로 비동기가 이어지는 방향
비동기는 느린 작업이 백그라운드에서 일어나는 동안에도 페이지를 살아있게 유지해주는 JavaScript의 부분입니다. 구조는 언제나 같습니다. 시간이 걸리는 작업을 시작하고, 나머지 코드는 계속 실행되게 두고, 결과가 도착하면 처리하는 것입니다. 프로미스는 그 결과를 어디든 전달할 수 있는 값으로 만들어주고, async/await는 그것을 평범한 위에서 아래로 흐르는 코드로 작성할 수 있게 해주며, fetch는 이 모든 것을 가장 많이 사용하게 될 서버에서 데이터를 불러오는 곳입니다.
여기서부터 비동기는 바깥 세계에 반응하는 언어의 다른 부분들과 이어집니다. 가져온 데이터를 DOM을 통해 페이지에 연결하게 될 것이고, 사람들이 페이지에서 하는 행동에 반응해서 비동기 작업을 시작하게 될 텐데, 이것이 바로 이벤트입니다. 돌려받는 결과는 거의 항상 객체이므로, 그것을 읽고 재구성하는 기술이 여기서 배운 모든 것과 짝을 이루는 다음 단계입니다.

