Skip to content
This page has been auto-translated and may contain errors.View in English

비동기 JavaScript

docs.scrimba.com

어떤 것들은 즉시 일어납니다: 두 수를 더하기, 변수 읽기, 페이지의 텍스트 변경하기. 다른 것들은 시간이 걸립니다. 타이머가 3초 동안 카운트다운됩니다. 서버에 요청이 네트워크를 통해 이동하고 잠시 후에 돌아옵니다. JavaScript가 각각을 기다리며 멈춘다면, 전체 페이지가 얼어붙을 것입니다: 클릭 없음, 스크롤 없음, 아무것도 없음, 느린 작업이 끝날 때까지. 하지만 그렇게 작동하지 않습니다. 이 장은 JavaScript가 어떻게 느린 작업을 시작하고, 페이지를 반응 가능하게 유지하고, 결과가 준비되었을 때 다시 돌아오는지에 관한 것입니다.

비동기가 필요한 이유

대부분의 JavaScript는 위에서 아래로 실행되며, 한 줄이 끝나야 다음 줄이 시작됩니다. 빠른 작업에는 괜찮습니다. 하지만 어떤 작업은 실제 시간이 걸리고, JavaScript는 그것을 기다리면서 앉아있지 않습니다. 느린 작업을 시작하고, 다음 줄로 이동하며, 완료되었을 때 다시 돌아옵니다. 이것이 **비동기**가 의미하는 바입니다: 순서대로 일어나지 않음, 지금 아님, 하지만 나중에, 결과가 준비되었을 때.

가장 간단한 예는 타이머입니다. setTimeout은 지연 후에 코드를 실행합니다:

js
console.log("시작");
setTimeout(() => {
  console.log("3초 후");
}, 3000);
console.log("끝");
// 시작
// 끝
// 3초 후

순서를 주목하세요. "끝""3초 후"보다 먼저 출력되지만, 코드에서는 setTimeout 후에 나옵니다. JavaScript는 그 줄에서 3초를 기다리지 않았습니다. 타이머를 설정하고, 계속 진행했으며, 시간이 되었을 때 지연된 코드를 실행했습니다.

JavaScript는 위에서 아래로 한 번에 한 가지씩 실행합니다. 이 모델은 시간이 걸리는 작업까지는 잘 작동합니다: 타이머, 네트워크 요청, 파일 읽기. 언어가 이들에 대해 블로킹된다면, 다른 것이 실행될 수 없기 때문에 페이지가 기다리는 동안 얼어붙을 것입니다. 비동기 코드는 해결책입니다: 느린 작업을 시작하고, JavaScript는 나머지 코드를 계속 실행하며, 결과가 도착했을 때 나중에 처리됩니다.

setTimeout은 가장 명확한 첫 번째 예입니다. 함수와 밀리초 단위의 지연을 주면, 그 지연 후에 함수를 실행합니다:

js
console.log("시작");
setTimeout(() => {
  console.log("3초 후");
}, 3000);
console.log("끝");
// 시작
// 끝
// 3초 후

setTimeout에 전달하는 함수는 콜백입니다: 나중에 호출되기를 원하는 코드, 지금 아닙니다. JavaScript는 타이머를 등록하고, 바로 "끝"으로 이동하며, 지연이 지나고 현재 코드가 완료된 후에만 콜백을 실행합니다. 그 "지금 마치고, 나중에 돌아오기" 패턴이 비동기의 전체 개념이며, 이 장의 다른 모든 것들이 이를 기반으로 합니다.

JavaScript는 단일 스레드입니다: 하나의 호출 스택, 한 번에 하나의 실행. 바로 이 제약이 비동기가 중요한 이유입니다. 네트워크 요청에 대한 블로킹 대기는 단일 스레드를 멈추게 하고, 그것과 함께 페이지의 모든 클릭, 애니메이션, 렌더링이 멈춥니다. 따라서 느린 작업은 인라인으로 처리되지 않습니다. 런타임(브라우저 또는 Node)에 넘겨지며, 런타임이 당신의 스레드 밖에서 대기하고 결과가 준비되었을 때 당신의 코드를 재개하도록 예약합니다. 비동기 코드는 그 전달입니다: 아직 없는 결과로 무엇을 할지 설명합니다.

setTimeout은 최소 경우이며, 모델을 명확하게 노출합니다:

js
console.log("시작");
setTimeout(() => {
  console.log("나중에");
}, 0);
console.log("끝");
// 시작
// 끝
// 나중에

여기서 지연은 0이고, 콜백은 여전히 마지막에 실행됩니다. 그것이 단서입니다: 비동기 콜백은 절대 동기 코드 중간에 실행되지 않습니다. setTimeout은 0밀리초 후에 콜백을 실행하지 않으며, 현재 동기 코드가 완료된 후 콜백을 실행하도록 예약합니다. 그 순서 뒤의 메커니즘은 이벤트 루프이며, 이 장의 마지막 섹션에서 분석합니다. 지금은 규칙을 기억하세요: 동기 코드는 먼저 완료까지 실행되고, 그 다음 대기 중인 비동기 작업입니다.

Juno비동기가 필요한 이유 타이머나 데이터 로딩 같은 일부 작업은 시간이 걸리고, JavaScript는 기다리는 동안 얼어붙지 않습니다. 느린 것을 시작하고, 다음 줄을 계속 실행하며, 준비되었을 때 느린 것으로 돌아옵니다. 이것이 "끝"이 코드에서 타이머가 먼저 나타나도 setTimeout 메시지보다 먼저 출력되는 이유입니다.
Juno비동기가 필요한 이유 비동기는 느린 작업을 지금 시작하고, 나중에 그 결과를 처리하며, 그 사이에 페이지를 반응 가능하게 유지한다는 뜻입니다. setTimeout이 가장 간단한 버전입니다: 콜백을 넘기면 그 콜백은 지연 후에 실행되고, 즉시 아닙니다. 지연된 메시지가 마지막에 출력되는 이유를 알면, 이 장의 나머지는 그 하나의 개념에 대한 변형입니다.
Juno비동기가 필요한 이유 JavaScript는 단일 스레드이므로, 느린 작업에 대한 블로킹은 모든 것을 멈추게 합니다. 대신 대기를 런타임에 넘기고 결과가 도착했을 때 무엇을 할지 설명합니다. setTimeout(fn, 0) 경우가 단서입니다: 콜백은 여전히 동기 코드 후에 실행되는데, 이는 대기 중인 비동기 작업이 현재 코드가 먼저 완료되기를 기다리기 때문입니다.

Promise

setTimeout은 지연에는 괜찮지만, 대부분의 비동기 작업은 당신이 신경 쓰는 값을 생성합니다: 요청한 데이터, 또는 문제가 있으면 오류. 이를 위해 JavaScript는 **Promise**를 사용합니다. Promise는 아직 준비되지 않은 값이며, 이미 예약된 공간을 가지고 있습니다. 음식을 받지 못했지만, 음식이 될 무언가를 가진 영수증 같은 것입니다.

Promise는 두 가지 방법 중 하나로 끝납니다. 작업이 성공하면 값으로 해결되거나, 실패하면 오류로 거부됩니다. .then().catch()로 각 경우에 무엇을 할지 말합니다:

js
loadUser()
  .then((user) => {
    console.log(`${user.name}을(를) 로드했습니다`);
  })
  .catch((error) => {
    console.log("뭔가 잘못되었습니다");
  });

.then()은 Promise가 해결될 때 실행되고, 값을 받습니다. .catch()는 거부되면 실행되고, 오류를 받습니다. 그 안의 코드는 나중에 결과가 도착했을 때 실행됩니다.

콜백은 작동하지만, 나쁘게 쌓입니다. 한 비동기 작업이 다른 것에 의존하고, 그것이 또 다른 것에 의존할 때, 콜백을 콜백 안에 중첩하게 되고, 각 단계마다 더 오른쪽으로 떠납니다. 그 형태에는 이름이 있으며, "멸망의 피라미드"이며, 읽기가 힘들어지고 그 안에서 오류를 처리하기가 더 힘들어집니다. **Promise**가 형태를 해결합니다.

Promise는 아직 준비되지 않은 값을 나타내는 객체입니다. 세 가지 상태 중 하나입니다: 대기 중 (여전히 작동 중), 이행됨 (값으로 해결됨), 또는 거부됨 (오류로 실패함). 성공을 위해 .then()과 실패를 위해 .catch()로 핸들러를 연결합니다:

js
loadUser()
  .then((user) => {
    console.log(`${user.name}을(를) 로드했습니다`);
    return loadPosts(user.id);
  })
  .then((posts) => {
    console.log(`${posts.length}개의 게시물을 찾았습니다`);
  })
  .catch((error) => {
    console.log(`실패: ${error.message}`);
  });

두 가지가 이것을 중첩된 콜백보다 더 낫게 만듭니다. 첫째, 평탄합니다: 각 .then()은 Promise를 반환하므로, 중첩하는 대신 직선으로 연결합니다. .then()에서 값을 반환하면 다음 값이 받고; Promise를 반환하면 체인이 대기합니다. 둘째, 마지막에 하나의 .catch()가 체인의 어느 곳이든 실패를 처리하므로, 모든 레벨에서 오류 처리를 작성하는 대신 한 번만 작성합니다.

Promise 이전에는 비동기 결과가 콜백으로 전달되었고, 종속적인 비동기 단계는 콜백을 콜백 안에 중첩하는 것을 의미했습니다. 가독성 비용을 넘어서, 그 패턴에는 통합된 오류 경로가 없습니다: 모든 레벨이 자신의 실패를 처리하고, 하나를 잊으면 오류가 조용히 삼켜집니다. **Promise**는 최종 결과를 일등급 값으로 만들어 이를 전달하고, 연결하고, 한 곳에서 처리할 수 있도록 하여 두 문제를 모두 해결합니다.

Promise는 세 가지 상태 중 하나인 객체입니다: 대기 중, 값으로 이행됨, 또는 이유로 거부됨. 한 번 정산되면 (이행 또는 거부) 다시는 변하지 않습니다. .then(onFulfilled).catch(onRejected)로 반응을 등록하고, 연결이 실제 힘입니다:

js
loadUser()
  .then((user) => loadPosts(user.id)) // Promise를 반환하고, 체인이 기다립니다
  .then((posts) => posts.filter((post) => post.published))
  .then((published) => console.log(`${published.length}개가 발행됨`))
  .catch((error) => console.log(`실패: ${error.message}`));

.then()은 새 Promise를 반환하고, 핸들러의 반환 값이 그것을 결정합니다: 순수한 값을 반환하면 다음 .then()이 그 값을 받고, Promise를 반환하면 체인이 그것을 채택하고 기다립니다. 어디서든 던져진 오류 또는 거부는 나머지 .then() 핸들러를 건너뛰고 다음 .catch()로 점프하므로, 끝의 한 핸들러가 모든 단계에서의 실패를 캡처합니다. 이는 콜백보다 실제 개선이지만, 더 나은 구문일 뿐입니다: 오류가 자동으로 전파되고, 평탄한 체인은 실행되는 순서대로 읽습니다. 실제로는 많은 Promise 체인을 읽게 되지만, async/await로 자신의 비동기 코드 대부분을 작성할 것이며, 이는 다음 섹션이며 정확히 이 메커니즘 위에 앉아 있습니다.

JunoPromise Promise는 아직 준비되지 않은 값이며, 나중에 도착하는 결과에 대한 영수증 같은 것입니다. 작업이 성공하면 값으로 해결되거나, 실패하면 오류로 거부됩니다. 값에 대해 .then()으로, 오류에 대해 .catch()로 무엇을 할지 말하고, 둘 다 결과가 도착했을 때 나중에 실행됩니다.
JunoPromise Promise는 아직 올 결과를 나타내고, 중첩된 콜백을 이기는 이유는 평탄한 선으로 .then()을 연결할 수 있기 때문입니다. .then()에서 값을 반환하면 다음 것이 받고, Promise를 반환하면 체인이 기다립니다. 체인의 어디든지 실패를 처리하는 끝의 하나의 .catch()`이므로, 모든 레벨에서 오류 처리를 작성하는 대신 한 번 작성합니다.
JunoPromise Promise는 한 번 정산되고 (이행 또는 거부), 다시는 변하지 않습니다. 각 .then()은 새 Promise를 반환하므로, 값을 반환하면 그것을 전달하고 Promise를 반환하면 체인이 기다리는 동안, 모든 거부는 다음 .catch()로 건너뜁니다. 많은 체인을 읽게 될 것이지만, 대부분의 자신의 비동기를 asyncawait로 작성할 것이며, 이는 정확히 이것 위에 앉아 있습니다.

async / await

Promise 체인은 작동하지만, 보통 위에서 아래로 읽는 코드처럼 읽히는 더 깔끔한 방법이 있습니다. 두 개의 키워드, async와 **await**를 사용합니다.

함수를 async로 표시하고, 그 안에서 Promise를 await할 수 있습니다. await는 Promise가 해결될 때까지 함수를 멈추고, 그 다음 값을 직접 제공하며, .then()이 필요하지 않습니다:

js
async function showUser() {
  const user = await loadUser();
  console.log(`${user.name}을(를) 로드했습니다`);
}

위에서 아래로 읽으세요: 사용자를 가져오고, 이름을 로그합니다. await 줄은 loadUser()가 완료될 때까지 기다리고 user를 제공합니다. 함수가 거기서 멈추지만, 페이지의 나머지는 계속 실행되므로, 아무것도 얼어붙지 않습니다.

오류를 처리하려면, awaittrycatch로 감싸세요:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`${user.name}을(를) 로드했습니다`);
  } catch (error) {
    console.log("사용자를 로드할 수 없습니다");
  }
}

Promise가 실패하면, 코드가 충돌하는 대신 catch 블록으로 점프합니다.

async/await는 비동기 코드를 작성하는 현대적인 방법입니다. Promise 위에 구축되었으므로 밑에서는 아무것도 변하지 않지만, 함수에서 이미 알고 있는 동기 코드처럼 읽히는 비동기 로직을 작성할 수 있습니다.

함수를 async로 표시하면 그 안에서 **await**를 사용할 수 있습니다. await는 Promise를 가지고, 함수를 해결될 때까지 멈추고, 해결된 값으로 평가됩니다:

js
async function showUser() {
  const user = await loadUser();
  const posts = await loadPosts(user.id);
  console.log(`${user.name}은(는) ${posts.length}개의 게시물을 가지고 있습니다`);
}

마지막 섹션의 .then() 체인과 비교하세요: 동일한 작업이지만 평탄하고 선형이며, 값이 다음 줄에서 사용할 수 있는 순수한 const 바인딩에 도착합니다. async 함수는 항상 자체로 Promise를 반환하므로, showUser()를 호출하면 다른 곳에서 await하거나 .then()할 수 있는 Promise를 제공합니다.

사람들이 놓치는 부분은 오류입니다. async 함수 내의 거부된 Promise는 던져지므로, 순수한 try/catch로 캡처합니다:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`${user.name}을(를) 로드했습니다`);
  } catch (error) {
    console.log(`실패: ${error.message}`);
  }
}

동기 오류에 사용하는 동일한 try/catch가 비동기 오류를 처리하며, 이것이 async/await이 이긴 큰 부분입니다.

async/await는 Promise에 대한 구문입니다. async 함수는 항상 Promise를 반환하고, await는 하나를 풉니다: 함수를 일시 중단하고, 스레드를 런타임으로 돌려주며, Promise가 정산될 때 해결된 값으로 재개합니다. 기다리는 동안 아무것도 블로킹되지 않습니다. 함수는 멈추지만, 단일 스레드는 다른 작업을 실행할 자유가 있으며, 이것이 바로 핵심입니다.

보상은 비동기 코드가 콜백과 원본 체인이 비용이 드는 두 가지를 되찾는다는 것입니다: 선형 읽기 순서와 정상 제어 흐름. **await**를 사용하면 동기였던 것처럼 비동기 값 주위에 const, 조건부, 루프를 사용할 수 있습니다:

js
async function publishReport(userId) {
  const user = await loadUser(userId);
  if (!user.active) {
    return null; // 조기 반환이 정상적으로 작동합니다
  }
  const posts = await loadPosts(user.id);
  return posts.filter((post) => post.published);
}

오류 처리는 거부된 대기 Promise가 await에서 던져지기 때문에 try/catch로 축소됩니다:

js
async function publishReport(userId) {
  try {
    const user = await loadUser(userId);
    return await loadPosts(user.id);
  } catch (error) {
    console.log(`보고서 실패: ${error.message}`);
    return [];
  }
}

내재화할 가치가 있는 하나의 미묘함: try 안의 return await이 중요합니다. await 없이 return loadPosts(...)를 작성하면, 함수가 Promise를 반환하고 Promise가 정산되기 전에 try를 종료하므로, 거부가 catch를 벗어납니다. await를 사용하면, 거부가 여전히 try 안에 있는 동안 던져지므로, catch가 그것을 봅니다. 이것은 "내 오류 핸들러가 왜 발동하지 않았는가"라는 버그로 변하는 종류의 세부 사항이며, 다음 섹션에서는 이벤트 루프와 오류 패턴이 더 자세히 다룹니다.

Junoasync와 await 함수를 async로 표시하고 그 안에서 Promise를 await할 수 있습니다. await는 Promise가 해결될 때까지 함수를 멈추고 값을 직접 제공하므로, 코드가 .then() 없이 위에서 아래로 읽습니다. trycatch로 감싸서 실패를 처리하고, 함수가 기다리는 동안 페이지는 계속 실행됩니다.
Junoasync와 awaitasync/await는 더 깔끔한 구문을 가진 Promise입니다: await는 Promise가 해결될 때까지 함수를 멈추고 순수한 const의 값을 제공합니다. async 함수는 항상 Promise를 반환하므로, 다른 곳에서 그것을 await할 수 있습니다. 오류는 던져지므로, 정상 try/catch가 처리하고, 이것이 이 스타일이 이긴 대부분입니다.
Junoasync와 awaitawait는 함수를 일시 중단하고 스레드를 돌려주며, 해결된 값으로 재개하므로, 기다리는 동안 아무것도 블로킹되지 않습니다. 선형 읽기, 정상 제어 흐름, 오류에 대한 try/catch를 얻습니다. try 안의 return await을 주목하세요: await를 떨어뜨리면 거부가 catch가 볼 수 있기 전에 탈출합니다.

데이터 페칭

작성할 가장 일반적인 비동기 작업은 서버에서 데이터를 로드하는 것입니다. 브라우저는 이를 위해 fetch를 제공합니다. URL을 전달하면, 응답에 대한 Promise를 반환합니다:

js
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를 스스로 확인합니다:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    console.log("요청이 실패했습니다");
    return;
  }
  const users = await response.json();
  console.log(users);
}

서버에서 데이터를 로드하는 것은 가장 자주 도달할 비동기 작업이며, 브라우저의 fetch는 그것을 하는 방법입니다. URL을 주면 Response 객체로 해결되는 Promise를 반환합니다:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`요청 실패: ${response.status}`);
  }
  const users = await response.json();
  return users;
}

두 단계 특성이 중요한 부분입니다. 첫 번째 await는 응답 헤더가 도착할 때 해결되고, Response를 제공합니다. 그 응답은 아직 데이터가 아니며, 그것에 대한 핸들입니다. 본문을 읽는 것은 두 번째 비동기 단계입니다: response.json()은 파싱된 데이터로 해결되는 Promise를 반환합니다. 두 await, 두 단계입니다.

모두를 잡는 함정: fetch는 네트워크 실패에서만 거부하며, HTTP 오류 상태에서는 거부하지 않습니다. 404 또는 500도 해결됩니다; 당신은 스스로 response.ok를 확인해야 합니다. response.ok는 200번대 상태 코드에 대해 true이므로, !response.ok의 보호가 나쁜 상태를 실제 오류로 바꾸는 방법입니다. 한 번 던지면, 호출 주위의 try/catch는 네트워크 실패와 나쁜 응답을 한 곳에서 처리합니다.

fetch(url)은 브라우저의 비동기 HTTP 클라이언트이며, Response로 해결되는 Promise를 반환합니다. 설계에는 선두에서 말할 가치가 있는 하나의 날카로운 모서리가 있습니다: Promise는 네트워크 수준 실패, 끊어진 연결 또는 차단된 요청에서만 거부합니다. 404 또는 500 같은 HTTP 오류 상태는 fetch와 관련하여 성공적인 왕복이므로, 정상적으로 해결됩니다. response.ok를 확인하는 것은 선택 사항이 아닙니다.

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`요청 실패: ${response.status}`);
  }
  return await response.json();
}

두 단계 형태는 스트리밍을 반영합니다. 첫 번째 await는 응답 헤더가 도착할 때 정산되며, 본문이 반드시 도착했을 때가 아닙니다. Response는 스트림에 대한 핸들이며, response.json()은 그 스트림을 완성까지 읽고 파싱하는데, 이것이 자체로 비동기이며 자신의 await이 필요한 이유입니다. 함수 내에서 나쁜 상태를 던진 오류로 바꾸면 호출 사이트의 하나의 try/catch가 네트워크 실패, 나쁜 상태, 파싱에 실패하는 JSON, 세 가지 다른 실패 모드를 한 핸들러로 라우트합니다:

js
try {
  const users = await loadUsers();
  render(users);
} catch (error) {
  console.log(`사용자를 로드할 수 없습니다: ${error.message}`);
}

이것이 페치의 전체 실용적인 해부학입니다: 응답에 대한 하나의 Promise, 상태 확인, 본문에 대한 두 번째 Promise, 그리고 한 번에 모두 주위의 하나의 오류 경계. 다음 섹션은 한 번에 이들 중 몇 개가 필요할 때 일어나는 것을 다룹니다.

Juno데이터 페칭fetch(url)은 서버의 응답에 대한 Promise를 반환하고, 데이터 로딩은 두 단계입니다: 응답을 위해 await fetch(...)하고, 그 다음 데이터를 읽기 위해 await response.json()합니다. "찾을 수 없음" 같은 서버 오류는 자체로 실패하지 않으므로, 본문을 읽기 전에 response.ok를 확인하세요. 두 단계 모두 시간이 걸리므로 둘 다 await를 사용합니다.
Juno데이터 페칭fetchResponse에 대한 Promise를 반환하고, response.json()으로 본문을 읽는 것은 두 번째 비동기 단계이므로, 정상적인 페치는 두 개의 await를 가집니다. 함정: fetch는 네트워크 실패에서만 거부하므로, 404 또는 500도 정상적으로 해결됩니다. response.ok를 확인하고 나쁜 상태에서 던지고, 한 try/catch가 모든 것을 다룹니다.
Juno데이터 페칭fetch는 헤더가 도착할 때 해결되고, 본문이 아닐 때이며, 이것이 response.json()이 스트림을 읽는 별도의 대기 단계인 이유입니다. 네트워크 실패에서만 거부하므로, 404가 정상적으로 해결되고 당신은 스스로 response.ok를 확인해야 합니다. 나쁜 상태에서 던지고 호출 사이트의 하나의 try/catch가 네트워크 오류, 나쁜 상태, 파싱 실패를 함께 처리합니다.

이벤트 루프와 병렬로 작업 실행

지금까지 모든 것은 하나의 테마를 가집니다: JavaScript가 느린 작업을 시작하고, 계속 이동하며, 나중에 그것으로 돌아옵니다. 이 섹션은 그 뒤의 메커니즘입니다. 더하기 몇 개의 비동기 작업을 잘 실행하기 위한 패턴입니다.

JavaScript가 비동기 작업을 추적하는 방법의 그림입니다. 실행 중인 코드는 호출 스택에 앉으며, 지금 실행 중인 것의 목록입니다. setTimeout을 호출하면, 타이머는 브라우저에 넘겨져 카운트다운하므로, 아무것도 블로킹하지 않는 스택에 있지 않습니다. 타이머가 완료되면, 그 콜백은 작업 큐로 이동하며, 실행할 준비가 된 코드의 대기선입니다.

**이벤트 루프**는 그들을 연결하는 부분입니다. 하나의 규칙을 가집니다: 먼저 호출 스택의 모든 것을 실행하고, 스택이 비워질 때만 큐에서 다음 작업을 가져옵니다. 이것이 setTimeout(fn, 0)이 지연이 0이어도 다른 코드 후에 실행되는 이유입니다. 콜백은 현재 코드가 완료될 때까지 큐에서 기다려야 합니다:

js
console.log("첫번째");
setTimeout(() => console.log("세번째"), 0);
console.log("두번째");
// 첫번째
// 두번째
// 세번째

0의 지연은 "지금 아니라" "현재 코드가 완료될 때까지"를 의미합니다.

**이벤트 루프**는 이 모든 순서 지정을 작동시키는 엔진입니다. 세 부분: 호출 스택 (지금 실행 중인 동기 코드), 작업 큐 (차례를 기다리는 비동기 콜백), 루프 자체, 하나를 수행합니다: 호출 스택이 비어 있을 때, 큐에서 다음 작업을 가져오고 그것을 실행합니다.

이 하나의 규칙이 setTimeout(fn, 0) 순서를 설명합니다. 지연은 콜백이 큐에 들어가기 전의 최소 대기이지만, 대기 중인 작업은 현재 동기 코드가 스택을 비워질 때까지 실행될 수 없습니다:

js
console.log("첫번째");
setTimeout(() => console.log("세번째"), 0);
console.log("두번째");
// 첫번째
// 두번째
// 세번째

여러 독립적인 비동기 작업이 있을 때, 하나씩 차례로 기다리는 것은 시간을 낭비합니다. 루프의 await은 일반적인 실수입니다:

js
// 느림: 각 요청이 이전 것이 끝날 때까지 기다립니다
const users = [];
for (const id of ids) {
  users.push(await loadUser(id));
}

작업이 서로 의존하지 않으면, Promise.all로 함께 시작하고 한 번에 모두 기다립니다. Promise 배열을 가지고 그들의 결과 배열로 해결되는 Promise를 반환합니다:

js
// 빠름: 모든 요청이 동시에 실행됩니다
const users = await Promise.all(ids.map((id) => loadUser(id)));

독립적인 비동기 작업이 있을 때마다 Promise.all에 도달하고, 각 단계가 정말로 이전 것의 결과가 필요할 때만 루프에서 순차적 await를 유지합니다.

**이벤트 루프**는 단일 스레드 비동기를 가능하게 하는 스케줄러입니다. 호출 스택은 지금 실행 중인 동기 프레임을 보유합니다. 비동기 콜백은 직접 그것으로 푸시하지 않습니다; 큐에서 기다립니다. 루프의 규칙은 간단합니다: 호출 스택이 비어 있을 때, 다음 작업을 디큐하고 완료까지 실행하고, 반복합니다. 작업은 중단되지 않고 실행되므로, 느린 동기 작업은 여전히 모든 것을 블로킹하고, 비동기 여부와 상관없습니다.

그 모델이 setTimeout(fn, 0)의 전체 설명입니다. 지연은 최소 시간 후 콜백을 큐로 예약하지만, 스택이 비워질 때까지는 실행될 수 없으므로, 항상 동기 코드를 따릅니다:

js
console.log("첫번째");
setTimeout(() => console.log("세번째"), 0);
console.log("두번째");
// 첫번째, 두번째, 세번째

머리에 두고 있을 한 가지 개선: Promise 콜백 (.then 핸들러와 await 이후의 모든 것)은 작업 사이에 완전히 비워지는 별도의 더 높은 우선 순위 마이크로태스크 큐에 들어가므로, 해결된 Promise의 연속은 대기 중인 setTimeout 이전에 실행됩니다. 이것을 추론해야 할 일은 드물지만, Promise 코드가 타이머를 이기는 간헐적인 놀라움을 설명합니다.

실용적인 보상은 자신의 작업을 잘 스케줄링하는 것입니다. 독립적인 비동기 작업은 병렬로 실행되어야 하며, 차례가 아닙니다. 루프 안의 await는 그들을 직렬화합니다:

js
// 직렬: 총 시간은 모든 요청의 합입니다
for (const id of ids) {
  results.push(await loadUser(id));
}

// 병렬: 총 시간은 대략 가장 느린 단일 요청입니다
const results = await Promise.all(ids.map((id) => loadUser(id)));

Promise.all은 모든 Promise를 즉시 시작하고 (.map이 모두를 시작함), 모두 정산될 때 해결하고, 하나라도 거부하면 거부합니다. 독립적인 작업에 사용하세요; 순차적 await을 단계가 이전 결과가 필요할 때만 유지하세요. 경쟁 조건에 대한 하나의 주의: 두 비동기 작업이 동일한 상태에 작성하고 당신이 순서를 제어하지 않으면, 마지막으로 완료되는 것이 이기고, 당신이 시작한 마지막 것이 아닐 수 있습니다. 순서가 중요할 때, 첫 번째를 await 한 다음 두 번째를 시작하거나, 각 결과를 키로 만들어 늦은 도착이 더 새로운 것을 덮어쓸 수 없도록 하세요.

Juno이벤트 루프 실행 중인 코드는 호출 스택에 앉으며, 타이머가 완료되면 그 콜백은 작업 큐에서 기다립니다. 이벤트 루프는 먼저 스택의 모든 것을 실행하고, 그 다음 대기 중인 다음 작업을 가져오는데, 이것이 setTimeout(fn, 0)이 나머지 코드 후에 실행되는 이유입니다. 0의 지연은 "지금"이 아니라 "현재 코드가 완료될 때"를 의미합니다.
Juno이벤트 루프 이벤트 루프는 호출 스택을 비워진 상태로 실행하고, 그 다음 큐에서 다음 작업을 가져오며, 이것이 정확히 setTimeout(fn, 0)이 동기 코드가 완료될 때까지 기다리는 이유입니다. 비동기 작업이 서로 의존하지 않으면, 루프에서 하나씩 await 하지 마세요. Promise.all로 함께 시작하고 한 번에 모두 기다리고, 순차적 await을 단계가 마지막 것의 결과가 필요할 때만 유지하세요.
Juno이벤트 루프 루프는 작업을 완료까지 실행하고, 그 다음 것을 실행하므로, 느린 동기 작업은 모든 것을 블로킹하고, setTimeout(fn, 0)은 항상 현재 코드의 뒤에 있습니다. Promise 연속은 작업 사이에 더 높은 우선 순위 마이크로태스크 큐에 비워지는데, 간헐적으로 .then이 타이머를 이기는 것을 설명합니다. 루프에서 await 하는 대신 Promise.all로 독립적인 작업을 실행하고, 두 작업이 동일한 상태를 건드릴 때, 순서를 제어하거나 늦은 완료가 더 새로운 것을 덮어쓰지 않도록 각 결과를 키로 만드세요.

비동기가 여기서 어디로 가는지

비동기는 느린 작업이 백그라운드에서 일어나는 동안 페이지를 살려 두는 JavaScript의 부분입니다. 형태는 항상 동일합니다: 시간이 걸리는 무언가를 시작하고, 나머지 코드가 실행되도록 하며, 결과가 도착했을 때 처리합니다. Promise는 그 결과에 당신이 통과할 수 있는 값을 제공하고, async/await는 순수한 위에서 아래로 코드로 작성할 수 있도록 하며, fetch는 서버에서 데이터를 로드하면서 대부분 사용할 그곳입니다.

여기서부터, 비동기는 외부 세계에 반응하는 언어의 부분과 연결됩니다. 페칭된 데이터를 DOM을 통해 페이지로 연결하고, 당신은 페이지에서 사람들이 하는 것에 대응하여 비동기 작업을 시작할 것이며, 이것은 이벤트입니다. 당신이 되돌려 받는 결과는 거의 항상 객체이므로, 읽고 다시 형성하는 것이 여기의 모든 것과 쌍을 이루는 다음 기술입니다.