[번역] Introducing asynchronous JavaScript

김대연·2020년 1월 5일
0

Translated Docs

목록 보기
1/2

문서를 천천히 읽다가 번역해서 올려두면 나중에 볼 때도 도움이 되고 다른 분들에게도 혹시나 도움이 되지 않을까 해서 올려봅니다. 전문적인 번역은 절대 아니며 의역, 오역, 그리고 오타도 난무합니다. 처음으로 번역을 해본거라 많이 미숙합니다. 더 좋은 의견이 있으시다면 언제든 남겨주세요!

원문 주소: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

이 포스트는 원문의 첫번째 토픽인 "Synchronous JavaScript" 이후부터 시작합니다.


Async callbacks

Async 콜백은 배경으로서 작동하는 한 함수의 인자로 들어가게 되는 함수입니다. (배경 함수라고 지칭하겠습니다.) 만약 배경 함수의 작동이 끝나면 작업이 완료되었거나 어떠한 일이 발생함을 알려주기 위해 콜백 함수를 실행시킵니다. 콜백 함수를 사용하는 것은 조금 예전 방식이지만 몇몇 아직도 사용되는 보편적인 API들에서 사용되는 것을 볼 수 있습니다.

비동기 콜백의 예시로 아래 코드의 addEventListener() 함수의 두번째 매개변수가 있습니다:

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

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

첫번째 매개변수는 addEventListener 가 인식할 이벤트의 타입이며, 두번째 매개변수는 이벤트가 발생하면 실행될 콜백 함수입니다.

콜백 함수를 또 다른 함수의 인자로 넣게 될때, 인자로 들어가는 것은 함수의 참조값입니다: 콜백 함수는 즉시 실행되면 안됩니다. 콜백함수는 배경 함수의 안 어딘가에서 비동기적으로 다시 call-back(다시 불리게) 됩니다. 또한 배경 함수는 적절한 때에 콜백 함수를 종료시켜야 합니다.

콜백을 가지고 있는 함수를 작성하는 것은 어렵지 않습니다.
아래는 XMLHttpRequest API 를 통해 리소스를 로드하는 예시입니다. (실행시켜보세요, 소스도 확인해보세요)

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 함수가 생성되었는데 이 함수는 blob(Binary Large Object, 용량이 큰 멀티미디어 데이터 같은 것들을 다룰 때 사용) 을 object URL 로 만들어 나타내고, 이 URL 을 이미지로 생성하고 document<body> 에 추가(append)합니다. 또한 fetch 해올 url, content type, 그리고 콜백을 매개변수로 받는 loadAsset 함수도 생성합니다. 이 함수 내에서는, 콜백에서 받아 실행할 응답을 XMLHttpRequest(줄여서 “XHR”) 을 이용하여 URL 을 fetch 합니다. 이 경우 콜백 함수는 XHR 실행이 (onload 이벤트 핸들러를 이용하여) 리소스를 다운로드 받는 것을 대기하고 있습니다.

콜백 함수는 정말 다양합니다 - 함수들 사이의 실행 순서나 데이터의 전달을 제어할 수 있도록 해줄 뿐만 아니라, 상황에 따라 다른 함수에 데이터를 전달하도록 해줄 수도 있습니다. 그래서 다운로드된 응답에 processJSON(), displayText() 같은 여러 함수들을 실행시킬 수도 있습니다.

다만 모든 콜백 함수가 비동기적이지는 않으니 주의하세요 - 어떤 것들은 동기적으로 작동합니다. Array.prototype.forEach() 으로 배열의 요소들을 순회하는 것도 하나의 예시입니다. (실행시켜보세요,소스도 확인해보세요)

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

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

이 예시는 그리스 신들의 이름이 담긴 배열을 순회하며 각 요소의 인덱스와 값을 콘솔창에 띄웁니다. 이 때 forEach() 함수의 매개변수는 배열의 이름과 인덱스 이름을 참조하는 두 개의 매개변수를 가진 콜백함수가 되어야 합니다. 하지만 이 콜백함수는 어떤 것을 기다리거나 하지 않고 바로 실행됩니다.


Promises

Promise 는 현대 Web API들에게서 볼 수 있는 새로운 비동기 방식입니다.
좋은 예시로 fetch() API 가 있는데 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: 완성된 버젼을 깃헙에서 확인할 수 있습니다. (실행시켜보세요, 소스도 확인해보세요).

여기서 보면 fetch() 는 한개의 매개변수를 받고 — 네트워크에서 fetch 하려는 리소스의 URL — promise 를 반환합니다. 프로미스는 비동기 작동의 성공 혹은 실패를 나타내는 객체입니다. 프로미스는 이를테면 중간 상태를 나타낸다고 할 수 있습니다. 좀 더 본질적으로는 브라우저가 “내가 답을 구하는 대로 너에게 알려줄 것을 약속한다.” 라고 "promise" 를 통해 말한다고 할 수 있습니다.

이 개념에 익숙해지는데에 연습이 필요할 것입니다; 마치 슈뢰딩거의 고양이처럼 느껴질 수도 있습니다. 어떤 결과(성공 혹은 실패)가 나온것도 아니기 때문에 fetch 는 언젠가 끝날 브라우저의 작업의 결과를 기다리고 있습니다. 그리고 그 후 fetch() 에 연결된 3개의 코드 블럭들을 확인할 수 있습니다.

  • 2개의 then() 블럭들이 있습니다. 두개 다 이전 작업(fetch())이 성공했다면 실행될 콜백 함수를 가지고 있습니다, 그리고 각 콜백 함수들은 성공적인 결과를 입력값으로 받아들여 또 다른 어떠한 작업을 할 수 있도록 합니다. 각 .then() 블럭은 또 다른 promise 를 반환하는데, 이것은 .then() 블럭 끼리 서로 연결해서 비동기적인 작업을 순서에 맞게 실행할 수 있도록 하는 것을 의미합니다.

  • catch() 블럭은 맨 마지막에 존재하는데 .then() 블럭들 중 하나라도 실패한다면 작동합니다. — 동기적 작동 중 try…catch 블럭과 유사합니다. error 객체가 catch() 함수 안에 존재하고 에러가 발생했을 때를 발견하기 위해 사용할 수 있습니다. 하지만 동기적인 try…catch 는 뒤에 배울 async/await 에선 작동하지만 promise 에서는 작동하지 않는 점을 주의하세요.


The event queue

프로미스 같은 비동기적 작업은 event queue 로 넘어가게 되는데, 이것은 프로미스가 뒤이은 자바스크립트 코드들의 실행을 막지 않도록 메인 스레드에서 프로세스를 마친 후 실행됩니다. 순서대로 대기중인 작업들은 최대한 빠르게 실행된 후 자바스크립트 환경으로 돌아가게 됩니다.


Promises versus callbacks

프로미스는 예전 방식인 콜백과 몇가지 유사점들이 있습니다. 이 둘은 콜백을 함수에 전달하기보단 콜백 함수에 연결할 객체입니다.

하지만, 프로미스는 특히 비동기적 작업을 위해 만들어졌고, 콜백 방식보다 몇가지 장점이 있습니다.

  • 복수의 비동기적 작업을 .then() 함수를 여러 개 사용하여 서로의 결과값을 다음 함수의 입력값으로 넘겨주며 연결할 수 있습니다. 이러한 방식은 콜백으로 구현하기 훨씬 힘들며, 대개 지저분한 “절망의 피라미드” (혹은 콜백헬) 로 나타납니다.

  • 프로미스의 콜백 함수들은 엄격하게 event queue 의 순서를 따라 실행됩니다.

  • 에러 핸들링이 훨씬 뛰어납니다 — 각 “피라미드” 층마다 개별적으로 에러를 관리하기 보다, 모든 에러들을 마지막에 위차하는 .catch() 블럭 하나로 관리합니다.

  • 제3의 라이브러리에 콜백 함수가 전달되면 함수의 실행에 대한 제어를 잃는 콜백 방식과 다르게 프로미스 방식은 제어를 잃지 않습니다.


The nature of asynchronous code

이번엔 비동기적 코드의 특징을 보여줄 수 있는 예시를 보겠습니다. 이 예시는 코드의 실행 순서를 제대로 이해하지 못했을 때 어떤 일이 일어날 수 있는지와 비동기적 코드를 동기적 코드처럼 다루려고 했을 때 일어날 문제점들을 보여줄 것입니다. 이번 예시는 위에 사용된 예시와 거의 비슷합니다. 한 가지 다른점은 몇 개의 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!');

브라우저가 코드를 실행하기 시작하면, 첫번째 console.log(‘Starting’) 을 읽고 실행시킨 후 image 변수를 생성할 것입니다.

그리고 다음 줄로 넘어가 fetch() 블럭을 실행하는데, fetch() 함수는 코드의 실행을 방해하지 않기 때문에, 코드 실행은 프로미스관련 코드를 이후로도 실행되며 마지막 console.log 에 도달하여 (“All done!”) 을 콘솔에 출력합니다.

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() 메세지들은 바로 출력될 것입니다, 하지만 두번째는 마우스 버튼이 클릭되기 전까지는 실행되지 않습니다. 이 예시에선 클릭으로 인해 나타나는 점, 이전의 예시에선 두번째 메세지가 프로미스 체인에서 리소스를 fetch 한 후 나타난다는 점 외엔 똑같은 방식으로 작동합니다.

조금 더 복잡한 코드 예시에서는, 이러한 설정은 문제를 일으킬 수도 있습니다 — 반환값이 동기적 코드 블럭에 의존하는 비동기적 코드를 포함할 수 없습니다. 브라우저가 동기적 블럭을 진행하기 전에 비동기 함수가 값을 반환할지 확신할 수 없기 때문입니다.

이것을 확인해보려면, our example 에서 세번째 console.log() 를 아래처럼 바꾸어 시도해보십시오.

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

이제 세번째 메세지 대신 아래와 같은 에러가 뜰 것입니다.

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

이것은 브라우저가 세번째 console.log() 를 실행시키려고 할 순간에, fetch() 블럭의 실행이 종료되지 않으 변수 img 에 값이 할당되지 않았기 때문입니다.

Note: 보안 문제 때문에 로컬 파일시스템에서 파일들을 fetch (하거나 로컬에서 그러한 작업을) 할 수 없습니다. 위의 예시를 로컬에서 실행하려면 local webserver 에서 실행시켜야 합니다.

Active learning: make it all async!

이러한 오류가 있는 fetch() 예시를 고쳐서 세번째 console.log() 가 예상된 순서대로 나오게 하려면, 세번째 console.log() 또한 비동기적으로 작동하게 만들면 됩니다. 그렇게 하려면 세번째 console.log().then() 블럭 안에 넣은 후 뒤에서 두번째(catch 바로 전) 에 위치하게 하거나 두번째 then() 안에 포함시키면 됩니다. 한번 수정해보세요.

Note: 문제를 풀다 막혔다면,  find an answer here 여기서 답을 찾을 수 있습니다 (running live 작동하는지도 확인할 수 있습니다). 또한 프로미스에 관한 여러 가이드를 Graceful asynchronous programming with Promises 에서 확인할 수 있습니다.

Conclusion

자바스크립트는 기본적으로 동기적이고, 블로킹(한가지 실행이 다른 실행들을 막는다)이고, 한 번에 한가지 작업만 가능한 싱글-스레드 언어입니다. 하지만 웹 브라우저들은 동기적으로 작동하는 대신, (시간의 경과, 유저와 마우스의 상호작용, 혹은 데이터가 네트워크에 도착하는 것 등) 어떠한 이벤트가 발생했을 때 비동기적으로 작동하는 함수나 API 를 제공합니다. 그 말은 당신의 코드가 메인 스레드를 멈추거나 방해(block)하지 않고 한 번에 여러가지 일들을 실행하도록 할 수 있다는 뜻입니다.

우리가 무엇을 하려는지에 따라 코드를 동기적이거나 비동기적으로 작동하게 할 수 있습니다.

어떤 때 우리는 어떤 것이 바로 로딩이 되어 나타나길 원할 수도 있습니다. 예를 들어 우리가 정의한 스타일같은 경우는 최대한 빠르게 웹페이지에 적용되길 바랄 것입니다.

하지만 만약 데이터베이스에 검색하여 템플릿을 채우는 것처럼 시간이 필요한 작업을 실행시키려면, 메인 스레드에서 내보내서 비동기적으로 작업을 완료시키는 게 나을 것입니다. 시간이 지나면, 어떤 것을 비동기적으로 작동시키고 동기적으로 작동시켜야 할지 이해가 될 것입니다.



처음으로 글을 옮겨서 써보는 거라 많이 부족한 글입니다. 혹시 읽어보시고 수정하고 싶으신 부분이 있다면 댓글로 남겨주세요, 감사합니다!

0개의 댓글