TIL 30 | 비동기에 대한 이해

dabin *.◟(ˊᗨˋ)◞.*·2021년 9월 5일
0

Node.js

목록 보기
4/11
post-thumbnail

동기? 비동기?

Javascript is synchronous! 자바스크립트는 동기적이다. 호이스팅이 된 이후부터 내가 작성한 순서에 맞춰 하나하나 순서대로 실행된다. (호이스팅은 선언한 함수나 변수가 가장 위로 올라가는 것!)

위에서 아래쪽으로 동기적으로! 실행된다. 
console.log(1)
console.log(2)
console.log(3)
따라서 결과 값은
1
2
3

먼저 비동기적인 코드는 어떻게 실행이 되는지 setTimeout을 통해 알아보자. setTimeout은 브라우저에서 제공하는 API로 일정 시간이 지나면 콜백함수를 호출할 수 있게 알려준다.

API란?
API(Application Programming Interface 애플리케이션 프로그래밍 인터페이스, 응용 프로그램 프로그래밍 인터페이스)는 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다.
https://ko.wikipedia.org/wiki/API

console.log(1)
setTimeout(() => console.log(2);, 1000);
console.log(3)

1
3
2(1초 후)

브라우저한테 console.log(2)1초 뒤에 실행해달라고 요청하고, 
응답을 기다리지 않고 함수를 실행한다. 
이후 브라우저가 1초 지났으니까 실행하라고 call back하면 그때 2가 console창에 찍힌다!

비동기는 프로그램의 다른 부분들이 서로 방해를 하지 않고 동시에 실행될 수 있다는 것을 의미한다. 즉, 특정 코드의 연산이 끝날 때 까지 기다리지 않고 다음 코드를 실행한다.

비동기 event-driven
노드 환경에서 event는 front-end에서 받는 요청이고, 비동기적으로 처리한다는 것은 하나의 요청 실행이 끝나기 전에 다른 요청을 받아 실행한다는 의미다.
single thread
client의 요청을 비동기적으로 수행하는 노드의 핵심요소
자바스크립트로 비동기 event-driven 처리
자바스크립트 런타임 환경 덕분이다! 자바스크립트 코드가 메모리 관리, 스케쥴 관리 등의 일을 담당하고, heavy load는 chrome V8 엔진이 해줘서 자바스크립트로 작성된 코드가 정상 작동한다.

call back

동기적인 콜백 함수와 비동기적인 콜백 함수를 살펴보자.
synchronous callback

console.log(1)
setTimeout(() => console.log(2);, 1000);
console.log(3)
function printImmediately(print) {
	print();
}
printImmediately(()=> console.log('hello'));

함수의 선언은 호이스팅 되기 때문에 제일 위로 올라간다. 따라서 결과는 아래와 같다.
1 
3 
hello
2

asynchronous callback

console.log(1)
setTimeout(() => console.log(2);, 1000);
console.log(3)
function printWithDelay(print, timeout) {
	setTimeOut(print, timeout)
}
printWithDelay(()=> console.log('async callback'), 2000);
1
3
2
'async callback'

Callback hell
콜백 안에서 부르고 부르고 부르고 부르고!! 하는 함수의 문제점은 무엇일까? 가독성이 떨어져 유지 보수가 어렵고 로직을 이해하는 것도 어렵고 에러 디버깅도 어렵고, 즉, 어려움!!!!!! 이제, 콜백 지옥을 해결해주는 promise와 async/await을 알아보자.

promise

비동기를 간편하게 처리하도록 도와주는 '객체'로, 정해진 시간에 기능을 수행하고 정상적으로 기능이 수행되면 성공의 메세지와 처리된 결과값을 전달해준다. 예상치 못한 문제 발생하면 에러를 전달해준다. 주로 서버에서 받아온 데이터를 화면에 보여줄 때 사용한다. 데이터를 모두 받기 전에 화면에 데이터를 표시하려고 하면 오류가 발생하는 경우가 있는데, promise로 이를 해결할 수 있다.

state(상태)에 대한 이해

  • pending : 수행중, new Promise()생성하면
  • fulfilled : 성공, 콜백함수의 인자인 resolve 호출시
  • rejected : 실패, 콜백함수의 인자인 reject 호출시

producer과 consumer의 차이

원하는 데이터를 제공하는 측과 필요로하는 측을 잘 이해해야 한다.

producer

heavy한 일들은 promise를 생성해 관리하는 것이 좋다. 
동기적으로 만들게 되면 데이터를 로드하느라 다른 함수들이 실행이 안되기 때문이다.

const promise = new Promise((resolve, reject) => {
	//doing some heavy work(network, read files)
})

프로미스를 만드는 순간 우리가 정의한 executor 함수가 실행된다. 즉, 프로미스 안에 네트워크 통신을 하는 코드를 작성하면 프로미스가 만들어지는 순간 네트워크 통신이 시작되기 시작한다. 이에 따라 특정한 경우에 불필요한 네트워크 통신 일어날 수 있으니 주의해서 사용해야 한다.

consumer

then, catch, finally를 이용해 값을 받아올 수 있다.

2초정도 무언가를 하다 일을 마무리하고 resolve 콜백하는 함수

const promise = new Promise((resolve, reject) => {
	setTimeout(()=> {
    	resolve('dabin');
    }, 2000)
})

promise.then(value) => {
	console.log(value);
}

2초 후 'dabin' console에 찍힘

then은 promise가 정상적으로 작동해 최종적으로 resolve 콜백 함수를 통해 전달한 값이 value의 parameter로 전달되어져 들어온다. reject를 사용하면 어떻게 될까?

const promise = new Promise((resolve, reject) => {
	setTimeout(()=> {
    	reject(new Error('no network'));
    }, 2000)
})

promise.then(value) => {
	console.log(value);
}

이렇게 하면 2초 후 console창에 uncaught error라고 뜬다. then을 이용해 성공적인 케이스만 다루었기 때문이다. 이 때 캐치를 사용하면 error를 handle할 수 있다.

promise
  .then(value) => {
      console.log(value);
  });
  .catch(error => {
  	console.log(error);
  });
  //2초 후 error : no network
  .finally(() => {console.log('finally')});

finally는 실패를 하든 성공을 하든 마지막에 호출된다

promise chaining

const fetchNumber = new Promise(resolve, reject) => {
	setTimeout(() => resolve(1), 1000);
  });

fetchNumber
.then(num => num * 2)
.then(num => num * 3)
.then(num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(num-1), 1000);
  });
})
.then(num => console.log(num));
// 총 2초 소요됨
// 5

then에서는 promise에서 받아온 값을 전달해도 되고, 또 다른 비동기인 promise를 전달해도 된다. 예제로 심하게 귀여운 후라이 만드는 과정을 살펴보자.

위의 코드를 보다 간결하게 만들어보자. 콜백함수 전달시 받아오는 value를 바로 전달하는 경우 생략이 가능하다.

하지만 error가 나는 경우에는 어떻게 할까? 만약 위의 코드 대로라면 에러가 났을 시 uncaught error가 콘솔창에 찍힌다. 이럴 때에는 then catch를 사용해 에러를 잡아주면 된다. 에러시 문어초밥을 만들 수 있게 만들었다!

async & await

우리는 코드를 위에서 아래로 읽는게 편하다. async과 await은 promise 위에 좀 더 간편한 API를 제공하여(syntactic sugar) 프로미스를 간편하고 간결하고 동기적으로 실행되는 것처럼 보이게 만들어준다.

async function functionName() {
  await methodName();
}
비동기 처리 메서드가 꼭 프로미스 객체를 반환해야 await이 의도한 대로 동작한다는 점을 기억하자.  

async

function fetchUser() {
  //do network request in 10 secs....
  return 'dabin';
}

const user = fetchUser();
console.log(user);

아래와 같이 비동기 처리를 하나도 안해주면? 10초 수행하는 동안 그 코드에 머무르느라 다른 줄로 못넘어간다. 이에 따라 UI에 아무 것도 표시되지 않으니까 사용자가 텅 빈 화면만 보게 된다. 이를 해결하기 위해 프로미스로 만들어보자.

function fetchUser() {
 
  return new Promise((resolve, reject) => {
     //do network request in 10 secs....
    resolve( 'dabin' );
  });
}
const user = fetchUser();
user.then(console.log);
console.log(user);

new Promise만 만들면 계속 pending 상태가 되기 때문에 꼭 promise 안에 resolve와 reject를 이용해 완료해주어야 한다. syntactic sugar인 async await을 사용하면 무엇이 편리할까? resolve와 reject를 사용하지 않고도 promise와 같이 변환되어 같은 결과가 나온다. 즉, 함수를 자동으로 프로미스로 변환해준다!

async function fetchUser() 
     //do network request in 10 secs....
    return 'dabin';
}
const user = fetchUser();
user.then(console.log);
console.log(user);

await
async가 붙은 함수 안에서만 사용이 가능하다. 먼저 드라이브 스루로 주문들어온 햄버거 세트를 주는 코드를 만들어보자.

driveThru 함수에서 makeburger 하는데 1초 기다리고, makeFrenchFries 하는데 1초 기다려야 한다? 연관이 없는 것은 서로 기다릴 필요가 없으니 비효율적이다. 어떻게 동시에 수행하게 할 수 있을까? 어떻게 병렬 처리를 해줄 수 있을까?

async function driveThru() {
  const burgerPromise = makeBurger();
  const frenchFriesPromise = makeFrenchFries();
  const beerPromise = pourBeer();
  const burger = await makeBurger();
  const frenchFries = await makeFrenchFries();
  const beer = await pourBeer();
  return `${burger} + ${frenchFries} + ${beer}`;
}

이렇게 하면 만들자마자 각 함수가 실행되기 때문에 동시에 기능을 수행하고 await에서 기다렸다가 출력하게 된다. all이라는 promise API로 깔끔하게 작성해보자.

async function driveThru() {
  return Promise.all([makeBurger(), makeFrenchFries(), pourBeer()])
  .then( food => food.join('+'));
}

마지막에 한 번에 주는 것이 아니고 먼저 완성되는 것을 먼저 가져올 수 있는 방법은 뭘까?

function dalay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function makeBurger() {
  await dalay(1000);
  return '🍔'
}

async function makeFrenchFries() {
  await dalay(2000);
  return '🍟'
}

async function pourBeer() {
  await dalay(3000);
  return '🍺'
}

async function readyOnlyOne() {
  return Promise.race([makeBurger(), makeFrenchFries(), pourBeer()])
}
readyOnlyOne().then(console.log);
//'🍔' 

race를 사용하면 가장 먼저 끝난 친구를 반환받을 수 있다!!!

try catch로 에러 잡는 법!

async function functionName() {
  try {
  } catch (error) {
  }
}

개념 정리

마지막으로, 자바스크립트의 비동기를 이해하기 위한 개념을 정리해보자.

Chrome V8

웹 브라우저를 만드는 데 기반을 제공하는 오픈 소스 자바스크립트 엔진

Runtime Environment

대부분의 언어는 프로그램이 실행되는 환경을 제공하는 특정한 형태의 런타임 환경이 있다. 이 환경은 프로그램이 변수에 접근하는 방식, 프로시저 간 매개변수를 전달하는 매커니즘, 운영 체제와의 통신 등의 문제를 해결한다.

Single Threaded


스레드를 작업을 처리하는 일손이라고 생각하자. 싱글 스레드는 하나의 스레드만 직접 조작할 수 있으니 일손이 하나다. 요청이 많이 들어오면 한 번에 하나씩 요청을 처리하는데, 블로킹이 심하게 일어나는 작업을 처리하지만 않으면 싱글 스레드로 충분하다. 만약 블로킹이 발생할 것 같은 경우 논블로킹 방식으로 대기 시간을 최대한 줄이면 된다.

Call Stack

함수의 호출을 저장하는 자료구조다. 어떤 함수를 호출하면 스택에 쌓고 또 다른 함수를 호출하면 그 다음 스택 에 쌓으면서 가장 위에 쌓인 함수를 가장 먼저 처리한다.

Event Loop

Single-Threaded 기반에서 비동기 메세지를 처리하며, 고성능의 병렬처리를 보장하도록 설계되어 있다. 만약, 이벤트에 의해 처리해야 할 단위 작업이 아주 짧은 시간 안에 처리된다면 Node.js의 고성능의 장점을 극대화할 수 있다.

Concurrent

Non Blocking


논블로킹 방식은 이전 작업이 완료될 때까지 멈추지 않고 다음 작업을 수행한다. 따라서 블로킹 방식보다 같은 작업을 더 짧은 시간 내에 처리할 수 있다.

[참고 게시글]
https://m.blog.naver.com/hhw1990/221394005779
https://thebook.io/080229/ch01/01/05-01/
https://jongbeom-dev.tistory.com/119
https://medium.com/platformer-blog/node-js-concurrency-with-async-await-and-promises-b4c4ae8f4510

profile
모르는것투성이

0개의 댓글