Introducing asynchronous JavaScript

번역이 완료되지 않았습니다. Please help translate this article from English

이 문서에선 JavaScript의 동기식 처리와 관련된 문제를 간략하게 요약하고, 앞으로 접하게 될 다른 비동기 기술들을 살펴보며, 어떻게 우리에게 도움이 될 수 있는지 살펴봅니다.

Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective: To gain familiarity with what asynchronous JavaScript is, how it differs from synchronous JavaScript, and what use cases it has.

Synchronous JavaScript

asynchronous JavaScript가 무엇인지 이해하려면, 우리는 synchronous JavaScript가 무엇인지 알아야 합니다. 이 문서에선 이전 문서에서 본 정보의 일부를 요약하겠습니다.

이전의 학습 모듈에서 살펴본 많은 기능들은 동기식 입니다. — 약간의 코드를 실행하면, 브라우저가 할 수 있는 한 빠르게 결과를 보여줍니다. 다음 예제를 살펴볼까요 (see it live here, and see the source):

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

이 블럭에서 코드는 위에서 아래로 차례대로 실행됩니다. :

  1. DOM에 미리 정의된 <button> element 를 참조합니다.
  2. click 이벤트 리스너를 만들어 버튼이 클릭됐을 때 아래 기능을 차례로 실행합니다. :
    1. alert() 메시지가 나타납니다.
    2. 메시지가 사라지면 <p> element를 만듭니다.
    3. 그리고 text content를 만듭니다.
    4. 마지막으로 docuent body에 문장을 추가합니다.

각 작업이 처리되는 동안 렌더링은 일시 중지됩니다. 앞에서 말한 문서와 같이, JavaScript 는 single threaded이기 때문입니다. 한 번에 한 작업만, 하나의 main thread에서 처리될 수 있습니다. 그리고 다른 작업은 앞선 작업이 끝나야 수행됩니다.

따라서 앞의 예제는 사용자가 OK 버튼을 누를 때까지 문장이 나타나지 않습니다. :

Note: It is important to remember that alert(), while being very useful for demonstrating a synchronous blocking operation, is terrible for use in real world applications.

Asynchronous JavaScript

앞서 설명된 이유들 (e.g. related to blocking) 때문에 많은 웹 API기능은 현재 비동기 코드를 사용하여 실행되고 있습니다. 특히 외부 디바이스에서 어떤 종류의 리소스에 액세스 하거나 가져오는 기능들에 많이 사용합니다. 예를 들어 네트워크에서 파일을 가져오거나, 데이터베이스에 접속해 특정 데이터를 가져오는 일, 웹 캠에서 비디오 스트림에 엑세스 하거나, 디스플레이를 VR 헤드셋으로 브로드캐스팅 하는것 입니다.

동기적 코드를 사용하여 작업을 처리하는데 왜 이렇게 어려울까요? 다음 예제를 살펴보겠습니다. 서버에서 이미지를 가져오면 네트워크 환경, 다운로드 속도 등의 영향을 받아 이미지를 즉시 확인할 수 없습니다. 이 말은 아래 코드가 (pseudocode) 실행되지 않는다는 의미 입니다. :

let response = fetch('myImage.png');
let blob = response.blob();
// display your image blob in the UI somehow

왜냐하면 앞서 설명했듯이 이미지를 다운로드 받는데 얼마나 걸릴지 모르기 때문입니다. 그래서 두 번째 줄을 실행하면 에러가 발생할 것 입니다. (이미지의 크키가 아주 작다면 에러가 발생하지 않을 수도 있습니다. 반대로 이미지의 크기가 크면 매번 발생할 것 입니다.) 왜냐하면 response 가 아직 제공되지 않았기 때문입니다. 따라서 개발자는 response 가 반환되기 전 까지 기다리게 처리를 해야합니다.

JavaScript에서 볼 수 있는 비동기 스타일은 두 가지 유형이 있습니다, 예전 방식인 callbacks 그리고 새로운 방식인 promise-style 코드 입니다. 이제부터 차례대로 살펴보겠습니다.

Async callbacks

Async callbacks은 백그라운드에서 코드 실행을 시작할 함수를 호출할 때 인수로 지정된 함수입니다. 백그라운드 코드 실행이 끝나면 callback 함수를 호출하여 작업이 완료됐음을 알리거나, 다음 작업을 실행하게 할 수 있습니다. callbacks을 사용하는 것은 지금은 약간 구식이지만, 여전히 다른 곳에서 사용이 되고있음을 확인할 수 있습니다.

Async callback 의 예시는 addEventListener() 'click' 옆의 두 번째 매개변수 입니다. :

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

첫 번째 매개 변수는 이벤트 리스너 유형이며, 두 번째 매개 변수는 이벤트가 실행 될 때 호출되는 콜백 함수입니다.

callback 함수를 다른 함수의 인수로 전달할 때, 함수의 참조를 인수로 전달할 뿐이지 즉시 실행되지 않고, 함수의 body에서 “called back”됩니다. 정의된 함수는 때가 되면 callback 함수를 실행하는 역할을 합니다.

XMLHttpRequest API (run it live, and see the source)를 불러오는 예제를 통해 callback 함수를 쉽게 사용해봅시다. :

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = type;

  xhr.onload = function() {
    callback(xhr.response);
  };

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

displayImage()함수는 Object URL로 전달되는 Blob을 전달받아, URL이 나탸내는 이미지를 만들어 <body>에 그립니다. 그러나, 우리는 loadAsset() 함수를 만들고 "url", "type" 그리고 "callback"을 매개변수로 받습니다. XMLHttpRequest (줄여서 "XHR") 를 사용하여 전달받은 URL에서 리소스를 가져온 다음 callback으로 응답을 전달하여 작업을 수행합니다. 이 경우 callback은 callback 함수로 넘어가기 전, 리소스 다운로드를 완료하기 위해 XHR 요청이 진행되는 동안 대기합니다. (onload 이벤트 핸들러 사용)

Callbacks은 다재다능 합니다. 함수가 실행되는 순서, 함수간에 어떤 데이터가 전달되는지를 제어할 수 있습니다. 또한 상황에 따라 다른 함수로 데이터를 전달할 수 있습니다. 따라서 응답받은 데이터에 따라 (processJSON(), displayText()등) 어떤 작업을 수행할지 지정할 수 있습니다.

모든 callback이 비동기인 것은 아니라는 것에 유의하세요 예를 들어 Array.prototype.forEach() 를 사용하여 배열의 항목을 탐색할 때가 있습니다. (see it live, and the source):

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

이 예제에선 그리스 신들의 배열을 탐색하여 인덱스 넘버와 그 값을 콘솔에 출력합니다. forEach() 매개변수는 callback 함수이며, callback 함수는 배열 이름과 인덱스 총 두 개의 매개변수가 있습니다. 그러나, 여기선 비동기로 처리되지 않고 즉시 실행됩니다..

Promises

Promises 모던 Web APIs에서 보게 될 새로운 코드 스타일 입니다. 좋은 예는 fetch() API 입니다. fetch()는 XMLHttpRequest보다 좀 더 현대적인 버전 입니다. 아래 Fetching data from the server 예제에서 빠르게 살펴볼까요?  :

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

Note: You can find the finished version on GitHub (see the source here, and also see it running live).

fetch() 는 단일 매개변수만 전달받습니다. — 네트워크에서 가지고 오고 싶은 리소스의 URL — 그리고 promise로 응답합니다. promise 는 비동기 작업이 성공 했는지 혹은 실패했는지를 나타내는 하나의 오브젝트 입니다. 즉 성공/실패 의 분기점이 되는 중간의 상태라고 표현할 수 있죠. 왜 promise라는 이름이 붙었는지 잠깐 살펴보자면.. "내가 할수 있는 한 빨리 너의 요청의 응답을 가지고 돌아간다고 약속(promise)할게" 라는 브라우저의 표현방식 이어서 그렇습니다.

이 개념에 익숙해 지기 위해서는 연습이 필요합니다.; 마치 Schrödinger's cat(슈뢰딩거의 고양이) 처럼 느껴질 수 있습니다. 위의 예제에서도 발생 가능한 결과 중 아직 아무것도 발생하지 않았기 때문에, 미래에 어떤 시점에서 fetch()작업이 끝났을때 어떤 작업을 수행 시키기 위해 두 가지 작업이 필요합니다. 예제에선 fetch() 마지막에 세 개의 코드 블럭이 더 있는데 이를 살펴보겠습니다. :

  • 두 개의 then() 블럭: 두 함수 모두 이전 작업이 성공했을때 수행할 작업을 나타내는 callback 함수 입니다. 그리고 각 callback함수는 인수로 이전 작업의 성공 결과를 전달받습니다. 따라서 성공했을때의 코드를 callback 함수 안에 작성하면 됩니다. 각 .then() 블럭은 서로 다른 promise를 반환합니다. 이 말은 .then() 을 여러개 사용하여 연쇄적으로 작업을 수행하게 할 수 있음을 말합니다. 따라서 여러 비동기 작업을 차례대로 수행할 수 있습니다.
  • catch() 블럭은 .then() 이 하나라도 실패하면 동작합니다. — 이는 동기 방식의 try...catch 블럭과 유사합니다. error 오브젝트는 catch()블럭 안에서 사용할 수 있으며, 발생한 오류를 보고하는 용도로 사용할 수 있습니다. 그러나 try...catch 는 나중에 배울 async/await에서는 동작하지만, promise와 함께 동작할 수 없음을 알아두세요.

Note: You'll learn a lot more about promises later on in the module, so don't worry if you don't understand them fully yet.

The event queue

promise와 같은 비동기 작업은 event queue에 들어갑니다. 그리고 main thread가 끝난 후 실행되어 후속 JavaScript 코드가 차단되는것을 방지합니다. queued 작업은 가능한 빨리 완료되어 JavaScript 환경으로 결과를 반환해줍니다.

Promises vs callbacks

Promises 는 old-style callbacks과 유사한 점이 있습니다. 기본적으로 callback을 함수로 전달하는 것이 아닌 callback함수를 장착하고 있는 returned된 오브젝트 입니다.

그러나 Promise는 비동기 작업을 처리함에 있어서 old-style callbacks 보다 더 많은 이점을 가지고 있습니다. :

  • 여러 개의 연쇄 비동기 작업을 할 때 앞서 본 예제처럼 .then() 을 사용하여 결과값을 다음 작업으로 넘길 수 있습니다. callbacks으로 이를 사용하려면 더 어렵습니다. 또한 "파멸의 피라미드" (callback hell로 잘 알려진)을 종종 마주칠 수 있습니다.
  • Promise callbacks은 event queue에 배치되는 엄격한 순서로 호출됩니다.
  • 에러 처리가 더 간결해집니다. — 모든 에러를 코드 블럭의 마지막 부분에 있는 단 한개의 .catch() 블럭으로 처리할 수 있습니다. 이 방법은 피라미드의 각 단계에서 에러를 핸들링하는 것 보다 더 간단합니다..
  • 구식 callback은 써드파티 라이브러리에 전달될 때 함수가 어떻게 실행되어야 하는 방법을 상실하는 반면 Promise는 그렇지 않습니다.

The nature of asynchronous code

코드 실행 순서를 완전히 알지 못할 때 발생하는 현상과 비동기 코드를 동기 코드처럼 취급하려고 하는 문제를 살펴보면서 비동기 코드의 특성을 더 살펴봅시다 아래 예제는 이전에 봤던 예제와 유사합니다. (see it live, and the source). 한가지 다른 점은 코드가 실행순서를 보여주기 위해 console.log() 를 추가했습니다.

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');

브라우저는 코드를 실행하기 시작할 것이고, 맨 처음 나타난 (Starting) 글씨가 써진 console.log() 후 image 변수를 만들 것 입니다.

다음으로 fetch() block 으로 이동하여 코드를 실행하려고 할 것 입니다. fetch() 덕분에 blocking없이 비동기 적으로 코드가 실행되겠죠. 블럭 내에서 promise와 관련된 작업이 끝나면 다음 작업으로 이어질 것 입니다. 그리고 마지막으로 (All done!) 가 적혀있는 console.log() 에서 메시지를 출력할 것 입니다.

fetch() 블럭 작업이 작업을 완전히 끝내고 마지막 .then() 블럭에 도달해야 console.log() 의 (It worked :)) 메시지가 나타납니다. 그래서 메시지의 순서는 코드에 적혀있는 대로 차례대로 나타나는게 아니라 순서가 약간 바뀌어서 나타납니다 :

  • Starting
  • All done!
  • It worked :)

이 예가 어렵다면 아래의 다른 예를 살펴보세요 :

console.log("registering click handler");

button.addEventListener('click', () => {
  console.log("get click");
});

console.log("all done");

이전 예제와 매우 유사한 예제 입니다. — 첫 번째와 세 번째console.log() 메시지는 콘솔창에 바로 출력됩니다. 그러나 두 번째 메시지는 누군가가 버튼을 클릭하기 전엔 콘솔에 표시되지 않죠. 위의 예제의 차이라면 두 번쩨 메시지가 어떻게 잠시 blocking이 되는지 입니다. 첫 예제는 Promise 체이닝 때문에 발생하지만 두 번째 메시지는 클릭 이벤트를 대기하느라고 발생합니다.

less trivial 코드 예제에서 이러한 설정을 문제를 유발할 수 있습니다. — 비동기 코드 블럭에서 반환된 결과를 동기화 코드 블럭에 사용할 수 없습니다. 위의 에시에서 image 변수가 그 예시 입니다. 브라우저가 동기화 코드 블럭을 처리하기 전에 비동기 코드 블럭 작업이 완료됨을 보장할 수 없습니다.

어떤 의미인지 확인을 하려면 our example을 컴퓨터에 복사한 후 마지막 console.log() 를 아래처럼 고쳐보세요:

console.log ('All done! ' + image.src + 'displayed.');

고치고 나면 콘솔창에서 아래와 같은 에러가 뜨는것을 확인할 수 있습니다. :

TypeError: image is undefined; can't access its "src" property

브라우저가 마지막 console.log() 를 처리할 때, fetch() 블럭이 완료되지 않아 image 변수에 결과가 반환되지 않았기 때문입니다.

Note: For security reasons, you can't fetch() files from your local filesystem (or run other such operations locally); to run the above example locally you'll have to run the example through a local webserver.

Active learning: make it all async!

위의 fetch() 예시에서 마지막console.log()도 순서대로 나타나도록 고칠 수 있습니다. 마지막 console.log() 도 비동기로 작동시키면 됩니다. 마지막 .then() 블럭의 마지막에 다시 작성하거나, 새로운 블럭을 만들면 콘솔에 순서대로 나타날 것 입니다. 지금 바로 고쳐보세요!

Note: If you get stuck, you can find an answer here (see it running live also). You can also find a lot more information on promises in our Graceful asynchronous programming with Promises guide, later on in the module.

Conclusion

In its most basic form, JavaScript is a synchronous, blocking, single-threaded language, in which only one operation can be in progress at a time. But web browsers define functions and APIs that allow us to register functions that should not be executed synchronously, and should instead be invoked asynchronously when some kind of event occurs (the passage of time, the user's interaction with the mouse, or the arrival of data over the network, for example). This means that you can let your code do several things at the same time without stopping or blocking your main thread.

Whether we want to run code synchronously or asynchronously will depend on what we're trying to do.

There are times when we want things to load and happen right away. For example when applying some user-defined styles to a webpage you'll want the styles to be applied as soon as possible.

If we're running an operation that takes time however, like querying a database and using the results to populate templates, it is better to push this off the main thread and complete the task asynchronously. Over time, you'll learn when it makes more sense to choose an asynchronous technique over a synchronous one.

In this module