[JS] 비동기처리 에디션

JJeong·2021년 10월 1일
0

🐇 토끼굴은 다소 주제와 거리가 있습니다. 건너뛰어 읽어도 괜찮습니다! 🐇

🥁 서문

올해 초부터 비동기에 대해 찾아본 글들이 꽤 있었지만 대부분 읽고 그냥 넘어갔다.
솔직히 이해도 안 갔고, 비동기처리를 업무에서 쓰는 일이 없다보니 중요성이 실감나지 않았다.
지금은 서버와 통신을 하며 돌아오는 데이터 반환값을 확인하는 어엿한 프론트엔드 유경험자(?)가 되었다.
아래 링크를 계기로 자바스크립트 비동기처리에 대한 가을 에디션 글을 작성해보려 한다.🍁


🎼 비동기란 무엇일까

동기 (Synchronous) : 동시에 처리된다.
비동기 (Asynchronous) : 동시에 처리되지 않는다.

동시에, 또는 다른 시각에 무엇이 처리된다는 것일까?
이는 작업의 결과값이 언제 돌아오느냐와 관련이 있다.
작업의 요청과 결과값 반환이 동시에 일어나느냐 그렇지 않느냐의 차이이다.
가령 api 연동에 사용되는 fetch()는 서버와 통신하는 과정에서 시간이 소요된다.
이 처리가 전부 끝나고 서버로부터 결과값을 받은 후에 밑에 코드들이 실행된다면 동기적이라 하고, 서버로 요청을 보내놓고 바로 밑에 코드로 넘어가 실행한다면 비동기적이라 한다.


📯 콜백 함수에서 Promise로

만일 비동기로 실행되는 게 자연스러운 코드 흐름에서 동기적으로 실행되어야 할 필요가 생기면 어떻게 해야 할까?
위에서 든 예시로 fetch() 함수로 서버로부터 받아온 데이터를 가공한다고 할 때, 아직 데이터는 오지 않았는데 처리를 먼저 해버린다면 에러가 발생할 것이다.
이럴 때 과거에는 콜백 함수를 사용했다.

다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권(실행 시점)도 함께 위임한 함수

이 포스팅에서 정리한 콜백 함수의 정의가 가장 와닿았다.
다른 함수에 인자로 들어감으로써 실행 시점도 그 함수 내부에서 정해지는 함수이다.
즉 비동기 함수 내부의 프로세스가 전부 끝난 시점에 콜백 함수를 호출하면 동기적으로 실행이 가능해진다!

function work(callback) {
setTimeout(() => {
const start = Date.now();
for (let i = 0; i < 10000; i++) {}
const end = Date.now();
callback(end - start + 'ms');
}, 0);
}

console.log('작업 시작!');
work((delayTime) => {
console.log(delayTime);
console.log('작업이 끝났어요');
});
console.log('다음 작업')

출처 - 3장. 자바스크립트에서 비동기 처리 다루기 / ham****님 댓글

그런데 이렇게 콜백 함수로 작성하면 중첩이 될 경우 뎁스(depth)가 깊어지고 코드 흐름을 파악하기 어려워진다는 단점이 생긴다. (콜백 지옥)

이에 따라 비동기 작업이 끝나면 promise라는 객체에 담아 결과값을 돌려주는 방식이 등장한다.
promise는 직역하면 '약속'이라는 뜻이다. 아직 결과값이 주어지진 않았지만, 곧 돌려줄 거라는 약속과 다름 없기에 이런 이름이 붙은 것 같다.

프라미스(promise) 는 '제작 코드’와 '소비 코드’를 연결해 주는 특별한 자바스크립트 객체입니다. 프라미스는 시간이 얼마나 걸리든 상관없이 약속한 결과를 만들어 내는 '제작 코드’가 준비되었을 때, 모든 소비 코드가 결과를 사용할 수 있도록 해줍니다.

let promise = new Promise(function(resolve, reject) {
  // executor (제작 코드, '가수')
});

executor에선 결과를 즉시 얻든 늦게 얻든 상관없이 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 합니다.

  • resolve(value) — 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
  • reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖습니다.

  • state — 처음엔 "pending"(보류)이었다 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"로 변합니다.
  • result — 처음엔 undefined이었다 resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변합니다.

state는 작업의 성공 여부를 나타내고 result는 성공/실패에 따른 결과를 저장한다.
우리는 조건문 등을 사용하여 executor 코드를 짜고 정상적으로 작업이 진행되었을 경우 resolve를, 작업에 문제가 생겼을 경우엔 reject을 호출한다.
그리고 result에 값이 담기면 value이든 error든 꺼내서 사용하면 된다!🤩

단, 주의할 점이 있는데...
반환된 결과값을 promise.result 이런 식으로 꺼내 사용하는 건 불가능하다.

  • resolve 함수의 반환값(value)을 사용할 때 - .then((value)=>{ //value 사용 코드 })
  • reject 함수의 반환값(error)을 사용할 때 - .catch((error)=>{ //error 사용 코드 }), 또는 .then()의 두번째 인자로 받아서 .then(()=>{}, (error)=>{ //error 사용 코드 }))
  • 비동기 결과값을 이용하는 코드가 전부 끝난 후 - .finally(()=>{ //후속 처리 코드 })

간단하게 서버로 api를 호출해서 데이터를 불러오는 예시 코드를 작성해보았다.

// api를 통해 data를 불러와 state 값으로 넣어줌

apiService.getData(id)
.then((json) => { setData(json) })
.catch((err) => { console.log(err) })
.finally(() => { alert("작업이 모두 끝났습니다!") })

🎹 async, await의 등장

이렇게 모두가 행복해질 줄 알았지만, 새로운 기술이 등장한다. (인간의 욕심은 끝이 없고)
비동기로 받아온 결과값(result)을 굳이 .then(), .catch(), .finally()로 받아 처리하지 않고 일반적으로 변수에 저장하는 것처럼 사용할 수 있다.

사용법은 간단하다.
async는 내부 코드가 멋대로 비동기를 진행하지 않도록 잠시 일시정지를 할 함수 앞에 붙인다.
await시간이 소요될 작업 앞에 붙이면 해당 작업의 결과물을 꺼내준다.

// 위의 예시 코드를 async, await로 변경

const example = async () => {
  try { 
    const json = await apiService.getData(id);
    setData(json);
  } catch(err) {
    console.log(err)
  }
  alert("작업이 모두 끝났습니다!")
}

await를 붙임으로써 apiService.getData(id)의 반환값이 바로 json 변수에 저장될 수 있다.

🚨 에러 발생 🚨

async, await를 적용해서 코드를 작성했는데 에러가 발생했다...

  const getDeliveryTracking = async (id) => {
    const deliveryTracking = await CourierService.getDeliveryTracking(id);
    return deliveryTracking.data;
  };

deliveryTracking.data의 데이터 형태는 array이다.
하지만 이 값에 .map() 함수를 적용하면 해당 함수가 없다는 메시지가 나온다.
async가 붙은 함수가 반환하는 값은 promise라는 글을 읽었고 아래 링크가 도움이 될 것 같아서 일단 첨부해놓았다.
Convert array of objects with Promise to normal array


(🐇토끼굴) JS = 싱글스레드?

자바스크립트는 싱글스레드이기 때문에 비동기처리를 해서 사용자 경험을 높일 수 있다는 글을 읽었다. 반면 Go 언어의 장점은 멀티스레드라는 글도 읽은 적이 있다.
그렇다면 자바스크립트는 런타임에서 어떻게 실행되는 걸까?

정확하게 말하면 자바스크립트의 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어라고 부른다. 하지만 이벤트 루프만 독립적으로 실행되지 않고 웹 브라우저나 NodeJS같은 멀티 쓰레드 환경에서 실행된다. 즉, 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아니다.

자바스크립트가 실행될 때는 다음과 같은 요소들이 실행을 도와준다.

  • Call Stack: 자바스크립트에서 수행해야 할 함수들을 순차적으로 스택에 담아 처리
  • Web API: 웹 브라우저에서 제공하는 API로 AJAX나 Timeout등의 비동기 작업을 실행
  • Task Queue: Callback Queue라고도 하며 Web API에서 넘겨받은 Callback함수를 저장
  • Event Loop: Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김

이 포스팅에 따르면 Web API를 사용하는 함수가 실행 순서에서 밀리는 이유는 Task Queue에 우선 들어가기 때문이다. 그 사이에 console.log() 등의 코드가 Call Stack에 들어가 먼저 실행되고 Call Stack이 비워진 이후에 Event Loop가 Task Queue에 있던 Web API 콜백함수를 옮겨 실행되게 한다.
(이게 setTimeOut()에서 대기 시간을 0초로 설정해도 바로 밑에 코드가 먼저 실행되는 이유도 될까?🤔)


🔔 포스팅을 마치며

거의 처음으로 벨로그 포스팅다운 글을 써봤는데 정말 오래 걸려서 놀랐다...
이 글을 쓰며 참고했던 포스팅을 작성하신 모든 분들이 존경스럽다.😂
다음에 작성할 때는 설명을 줄이고 코드를 늘리는 방향으로 써보고 싶다.

자바스크립트 비동기를 처음 접하는 분들께 조금이라도 도움이 되셨길 바라며! 🙋🏻‍♀️

0개의 댓글