JS 의 비동기란?

임동규·2022년 1월 19일
0

JS

목록 보기
1/1
post-thumbnail

JS의 비동기에 대해 쓰기 전에 비동기라는 단어와 동기라는 단의에 대해 먼저 정의해보자

동기?

동기라는 것은 주문을 받으면 결과를 내어주기 전까지 다음 작업이 일어나지 않는 것이다.


비동기?

비동기는 주문을 받고 결과가 나오기 전에 다른 작업을 수행하는 것이다.



그냥 동기적으로 다 실행하면 되지 왜 비동기가 필요해?

예를 들어보자. 만약 한가로운 주말 스타벅스에 커피를 마시러 갔다고 생각하자. 그런데 알바생이 주문 한 사람의 커피가 제공될 때까지 주문을 받지 않는 것이다. 보통 아메리카노를 하나 뽑는데 3분이 걸린다고 치자. 내앞의 사람은 5팀이나 있다. 주문을 하고 커피를 받을 떄까지 18분이 걸릴 것이다!

우리의 인터넷 세상도 마찬가지다. 만약 어떤 홈페이지에 사람이 동시에 로그인 한다고 생각해보자. 이 홈페이지는 로그인 처리를 동기적으로 실행한다. 거의 10명 가까운 사람이 거의 동시에 로그인을 한다고 생각해보자. 우리는 로그인이 안되서 계속 화면 위에 버퍼링이 되는 모습을 구경해야 할 것이다. 그리고 우리는 짜증이 나서 홈페이지를 나가 버릴 것이다.

이처럼 프로그래밍에서든 실생활에서든 비동기로 처리해줘야 할 것들은 비동기로 처리해줘야 한다. 그래야지 고객 경험도 좋을 것이고 이는 곧 수익으로 연결될 것이다.


JS Engine은 동기적이다.

JS 엔진은 코드를 한줄씩 읽어 나가면서 실행한다. 코드를 한줄 읽고 함수를 호출하는 코드를 읽으면 한줄한줄씩 읽어나가서 로직을 수행하게 된다.
이는 사실 당연한 거다. 만약 코드를 한줄 한줄 읽지 않고 수행하게 된다면 우리의 코드는 제멋대로 해석이 될테니깐 말이다.



JS Engine은 동기적인데 어떻게 비동기를 처리할까?

JS 에서 비동기 처리를 하는 방법은 3가지 정도이다.
1. 브라우저 API,Node.js Api 를 콜백으로 활용해서 비동기 처리
2. Promise 객체를 통한 비동기 처리
3. async await 을 통한 비동기 처리

먼저 아래의 사진을 통해 JS가 위 3가지를 통해 어떻게 비동기처리를 하는지 알아보자.

위 그림을 보면 함수가 호출 되면 CALL STACK 에 쌓이게 된다. 만약 중첩함수이거나 콜백함수를 쓰게 된다면 계속 계속 콜스택에 쌓이게 될 것이다. 그러다가 비동기로 처리해야 하는 함수를 만난다면? Node.js 환경에서 실행되고 있다면 Node.js환경에게 콜백함수를 던진다. 이는 Node.js 환경이 어떻게 콜백함수를 가지고 있는지 궁금하다면 밑의 를 확인해보시면 된다. 굉장히 잘 정리 해 놓으셨다.
(https://www.korecmblog.com/node-js-event-loop/)

그리고는 Node.js는 콜백함수를 Callback Queue 에 저장해 놓는다. 이제 중요한 점! Node.js 에는 이벤트 루프라는 것이 있는데 Node.js 의 이벤트 루프는 Call Stack 을 항상 지켜보고 있다가 비게 된다면 Call Stack 에 보내서 콜백함수를 실행하게 된다.

자, 그럼 이제 실제로 비동기적으로 실행하는 방법에 대해 알아보도록 하자.



브라우저 API,Node.js Api 에 콜백을 넣어서 비동기 처리

console.log(1);
setTimeout(() => {
  console.log(2);
}, 5000);
console.log(3);
// 실행결과는?

1
3
2

비동기에 대해 모르거나 setTimeout 에 대해 모른다면 당연히 출력결과는 1,2,3이 되어야 할 것이다. 우리 자바스크립트 엔진은 동기적으로 읽어내야 하니까.
하지만 실행결과를 볼 수 있듯이 setTimeout 같은 브라우저 api에 콜백함수를 전달해줌으로써 자바스크립트도 비동기적으로 함수를 실행 할 수 있다.

이 코드를 보면 무슨 생각이 드는가. 개인적으로 나는 읽기 싫다. 아 몰라!라는 말이 나는 나온다......

이처럼 콜백함수가 콜백함수를 부르고 그 콜백함수가 콜백함수를 부르는 이런 것을 콜백지옥이라고 한다. 그래서 JS 개발자들은 콜백지옥에서 벗어나기 위해서 Promise 라는 것을 만들었다.

Promise 객체를 통한 비동기 처리


const getDataFromFilePromise = (filePath) => {
  // return new Promise()
  // TODO: Promise 및 fs.readFile을 이용해 작성합니다.

  return new Promise((resolve, reject) => {
    fs.readFile(filePath, "utf8", (err, data) => {
      if (err) {
        reject(err);
      }
      resolve(data);
    });
  });
};

getDataFromFilePromise("../README.md")
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

Promise 객체는 비동기 처리를 위한 패턴으로 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있기 위해 제작 되었다.



프로미스를 활용하기 위해선?
Promise 생성 방법,resolve와 reject,Promise 의 상태, then,catch 를 알아야 한다.



Promise 생성방법
1. new Promise () 를 통해 Promise 객체를 만들어준다.
2. 생성된 Promise 객체에는 resolve,reject가 인자로 들어가게 된다.
3. 비동기 처리가 성공하였을 시 resolve 함수에 비동기 처리 결과를 보내준다.
4. 비동기 처리가 실패하였을 시 reject 함수에 비동기 처리 결과를 보내준다.



프로미스의 상태
프로미스를 활용하기 위해선 프로미스 객체의 상태를 알 필요가 있다. 프로미스 객체의 상태는 총 3가지다.
1. pending - 비동기 처리가 아직 수행되지 않은 상태
2. fulfilled - 비동기 처리가 수행된 상태(성공) → resolve 함수 호출(value)반환
3. rejected - 비동기 처리가 수행된 상태(실패) - > reject 함수 호출(error)반환



resolve() 함수와 reject함수
1. resolve 함수는 프로미스 객체가 비동기 처리를 한 결과를 인자로 받게 된다. 그리고 then 에 전달하게 된다.
2. reject 함수는 프로미스 객체가 비동기 처리를 한 결과를 인자로 받게 된다. 그리고 catch 에 전달한다.

then()
then은 resolve 함수에서 받아온 인자를 받아온다. 그리고 프로미스의 반환값을 리턴한다.

catch()
catch는 reject 함수에서 받아온 인자를 받아온다. 그리고 만약 비동기 처리가 실패해서 reject 함수에 인자가 들어가게 된다면 실패한 데이터를 처리하는 역할을 한다.

참고할 팁!! Promise 객체는 메서드 체이닝이 가능하다. .then().then().then() 이런식으로 사용이 가능하다.
또한 .then().then().then().catch() 도 가능하다. 결국 then이 반환하는 값도 Promise객체이기 때문에 이러한 메서드 체이닝이 가능한 것이다.

그렇다면 프로미스의 약점은 뭘까?

  1. try catch 문을 활용할 수 없다.
  2. error 가 어디서 났는지 확인하기 어렵다.
  3. 콜백지옥보다는 훨씬 가독성이 높지만 그래도 동기적 코드보다는 가독성이 떨어진다.
function promiseHell(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("프로미스 시작!");
      if (data !== false) {
        reject(new Error("error"));
      } else {
        resolve(data);
      }
    }, 5000);
  });
}


promiseHell('hello')
.then(result=>{
  ....
  return ~~~~~
}).then((result)=>{
  ....
  return ~~~~~~
}).then((result)=>{
  ....
  return ~~~~~~
}).then((result)=>{
  ....
  return ~~~~~~
}).catch(err=>console.log(err))

async await 을 통한 비동기 처리

async await는 프로미스를 기반으로 동작하는 프로미스 syntax sugar 버전이다.
promise의 then catch 없이도 동기처럼 프로미스를 사용할 수 있다.

**async await 사용 예시**

const newsURL = "http://localhost:4999/data/latestNews";
const weatherURL = "http://localhost:4999/data/weather";

async function getNewsAndWeatherAsync() {
  const data1 = await fetch(newsURL).then((res) => res.json());
  const data2 = await fetch(weatherURL).then((res) => res.json());

  return {
    news: data1.data,
    weather: data2,
  };
}

**promise 사용예시**

function getNewsAndWeatherAll() {
  return Promise.all([fetch(newsURL), fetch(weatherURL)])
    .then(([data1, data2]) => {
      return Promise.all([data1.json(), data2.json()]);
    })
    .then(([data1, data2]) => {
      return {
        news: data1.data,
        weather: data2,
      };
    });
}

위의 코드를 보면 뭐가 더 가독성이 좋은가? 아마 대부분은 async 라고 대답을 할 것이다.
이처럼 aysnc / await 을 활용하면 비동기를 처리하는데 동기적으로 작성할 수 있게 된다.

그럼 어떻게 쓸까?

async
간단하다. 함수 앞에 async 라는 키워드를 붙여주면 그 함수는 비동기로 실행이 된다.

주의할 점! async 함수가 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다. 참고하길 바란다.

await
await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야 한다. 밑의 코드를 보면서 코드가 어떤 흐름으로 흐를지 생각해본다면 좋을 것이다.

  const data1 = await fetch(newsURL).then((res) => res.json());
  const data2 = await fetch(weatherURL).then((res) => res.json());

  return {
    news: data1.data,
    weather: data2,
  };
}

글을 마치며

Code States 에서 블록체인 부트캠프 교육을 받는 중인데 비동기 처리에 대해서 공부를 하며 든 생각을 정리하겠다.

나는 6개월 전에 잠깐 코딩학원에서 프론트앤드를 공부한 적이 있었다. 그때 잠깐 node.js 를 배웠었는데 강사분이 콜백지옥으로 node.js 를 가르쳤던 기억이 떠올랐다. 정말 이해하기 어려웠다. 안그래도 초보인데 콜백지옥에서 로직을 이해하기란 정말 쉽지 않았다.

그분은 왜 콜백지옥으로 가르쳤을까? 참고자료로 가져오는 것도 전부 콜백지옥이었다. 학생들이 이해하지 못할 것이란걸 뻔히 알면서도...
혹시 Promise 나 async/await 이 나온지도 몇년이 지났는데 지식을 배우기 싫어서 계속 콜백지옥을 사용하는 것은 아닐까?아니면 이미 알고 있음에도 자신에게 어색하다는 이유로 쓰지 않았던 것이 아닐까?

물론 내가 모르는 또다른 Promise 와 async 의 약점? 이 있을지도 모른다. 그러나 내가 생각하기에 좋은 코드는 다른 사람들도 알아볼 수 있는 코드다. 내가 불편하더라도 다른 상대방이 편하게 볼 수 있다면 과감하게 선택하고 몸에 흡수해야 하는 것이 협업을 해야하는 개발자들의 숙명이 아닐까 라는 생각이 든다.

profile
I will be Blockchain Core Developer

0개의 댓글