비동기설명하려고 스레드부터 시작해서 js실행환경, 이벤트 루프 긁어 모은 썰 푼다.

요들레이후·2023년 4월 13일
14

FE study

목록 보기
2/5
post-thumbnail

비동기

엘리스 SW트랙 4기에서 배운 내용들을 정리한 것입니다.

이야기 할 전체적인 내용

  • 싱글스레드멀티스레드 차이, 장단점
  • 자바스크립트는 싱글스레드지만 비동기 실행 방식으로 부족한 성능을 극복
  • 블로킹논블로킹은 무엇? 자바스크립트는 논블로킹 방식을 지원함으로 일정 수준의 동시성을 보여줌
  • 자바스크립트 실행 환경(Runtime)의 구조와 각 구성요소의 역할
  • 자바스크립트의 비동기 작업 수행 방식: callback, promise, async/await
  • 오늘날의 자바스크립트는 멀티스레드를 지원함(web worker Or worker thread)

01. 싱글스레드 vs 멀티스레드

싱글스레드(single-threaded)

  • 스레드(Thread) = 일하는 사람, 싱글 스레드 = 일하는 사람 1명
  • 한 사람이 주어진 작업을 하나씩 처리하는 방식

멀티스레드(Multi-threaded)

  • 멀티스레드 = 일하는 사람 여러명
  • 여러 사람이 주어진 작업을 같이 처리하는 방식

싱글스레드 vs 멀티스레드

싱글스레드멀티스레드
장점- 경쟁 상태, 교착 상태 X(하지만 비동기 작업을 동반하는 JS의 경우는 위험이 있다.)
- 멀티스레드 대비 코드 복잡도가 낮음
- 평균적으로 싱글스레드보다 작업 처리 효율이 높음
단점- 여러 명이 일하는 병렬처리보다는 작업 처리 효율이 평균적으로 떨어짐- 사용시 경쟁 상태, 교착 상태를 고려해야함(코드 복잡도가 높음)

경쟁 상태

한 계좌를 A, B가 공유를 하고 있다고 가정

  • 정상적인 계좌 입/출금 흐름
  • 한 사람이 입/출금을 한 후에 다음 사람이 입/출금을 수행
  • 중첩되는 현상이 없음

  • 비정상적인 계좌 입/출금 흐름
  • 한 사람이 입/출금을 하는 도중에 다른 사람이 입/출금
  • 경쟁 상태 문제
  • 한 사람이 작업을 할 때는 다른 사람이 작업을 할 수 없도록 해야함: 계좌에 lock걸기

교착 상태

  • 2명 이상이 각자 리소스를 점유하고 있는데, 내놓지 않는 현상
  • 두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태

  • 멀티스레드에서는 이런 상황을 다 고려해야한다.

정리

  • 자바스크립트는 대표적인 싱글스레드로 작동하는 언어
  • 하지만 싱글스레드인 상황에서 성능(작업 처리 효율)을 극대화하기 위해 비동기(non-blocking, asynchronous) 처리 방식을 채택
  • 자바스크립트: 싱글스레드로 비동기 환경에서 실행된다?!?!?!?



02. 블로킹 vs 논블로킹

  • 자바스크립트 세계에서는 블로킹&동기, 논블로킹&비동기 라는 용어와 혼용

블로킹(blocking), 동기적 제어 흐름

  • 현재 실행 중인 코드가 종료되기 전까지 다음 줄의 코드를 실행하지 않는 것
  • 분기문, 반복문, 함수 호출 등이 동기적으로 실행된다.
  • 코드의 흐름과 실제 제어 흐름이 동일하다.
  • (단점) 싱글 스레드 환경에서 메인 스레드를 긴 시간 점유하면, 프로그램을 멈추게 한다.
    • 무한 루프에 빠진다 했을 때 전체 프로그램을 중단시킴
    • 브라우저가 렌더링을 멈춘다거나, 다른 작업들을 하지 못하게 된다.
function waitFiveSeconds() {
  const startTime = Date.now();
  // 5초간 아무 것도 못하고 대기
  while (Date.now() - startTime < 5000) {}
  console.log("5초 후!");
}

function sayHello() {
  console.log("Hello, World!");
}

waitFiveSeconds();
sayHello();

→ 선언문, console.log와 같은 동기적 실행, 반복문, 함수 호출문 모두 동기적 제어흐름이라고 볼 수 있다.

논 블로킹(non-blocking), 비동기적 제어 흐름

  • 현재 실행 중인 코드가 종료되기 전에 다음 라인의 코드를 실행하는 것을 의미
  • “작업을 실행시켜 놓기만 하는 행위” (작업해~ 어어 끝나면 알려줘~?)
  • 작업이 길어져도 다음 작업이 지연이 되는 문제가 없다.
  • 보통 결과값을 return문으로 리턴을 바로 받지 못하며 콜백함수를 통해 받는 경우가 많다.
    • 단, Promise의 등장으로 콜백함수가 아닌 다른 방식으로 받게 된다.
  • 코드 흐름과 실제 제어 흐름이 다르다.
  • 기술적으로 논블로킹 방식은 싱글스레드만으로는 불가 → 누군가의 도움이 필요! ⇒ 도움 주는 친구가 바로 💡이벤트 루프
// 비동기 함수
function waitFiveSeconds() {
  // 5초가 지나는 사이에도 다른 작업을 할 수 있도록 해줌
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("5초 후!");
      resolve();
    }, 5000);
  });
}

function sayHello() {
  console.log("Hello, World!");
}

// 5초 동안 대기하면서
waitFiveSeconds();
// 곧바로 인사도 가능하다
sayHello();
// 기다리지 않고 Hello, World를 띄우고, 5초 후!가 뜨게 됨

코드는 바로 실행되지 X, 순서가 뒤죽박죽 실행됨

비동기, 왜 필요한가?

  • 동기적 제어흐름의 단점과 연결됨

웹 페이지 로딩되거나, 어떤 동작 하나가 30초 이상 걸린다고 생각, 웹 페이지는 이 동작이 끝날 때까지 화면에 나타나지 않거나 다음 동작을 수행하는 데 지장을 주게 된다. 또 요즘 사용자들은 느리고 응답이 없는 웹 사이트를 원하지 않는다. 그렇기에 자바스크립트가 웹사이트에서 동작할 때 비동기적으로 동작할 수 있어야 함.

자바스크립트는 싱글스레드라 했는데 왜 동시 작업 가능?

  • 자바스크립트 엔진은 비동기 처리를 제공하지 않는다.
  • 대신, 비동기 코드는 정해진 함수를 제공하여 활용할 수 있다. 이 함수들을 API라 한다.
  • 비동기 API의 예시로, setTimeout, XMLHttpRequest, fetch등의 Web API가 있다.
// 타이머 비동기 처리
setTimeout(() => console.log('타이머 끝'), 1000);
setTimeout(() => console.log('인터벌 타이머'), 1000);

// 네트워크 처리, 원격 서버에 있는 데이터를 요청하며 응답이 오기 전에도 다른 작업을 수행할 수 있다.
fetch('https://naver.com')
	.then(() => console.log('네트워크 요청 성공.'))
	.catch(() => console.log('네트워크 요청 실패.'));
  • 마치 스레드가 하나이지 않은 것처럼 작동한다 → 그렇다 자바스크립트는 싱글스레드이면서 멀티스레드이다. → 이벤트루프 덕분에!
  • 개발자가 작성한 자바스크립트 코드는 싱글스레드로 순차적으로 실행시키되 특정 비동기 작업에 대해서는 멀티스레드를 사용한다.



03. 자바스크립트 실행 환경: Runtime

자바스크립트 실행 환경 조감도

  • 자바스크립트 코드를 실행하기 위한 환경의 구성요소들
  • 세가지가 유기적으로 돌아가면서 자바스크립트 실행환경을 이룬다.

자바스크립트 엔진

  • 자바스크립트 코드를 읽어서 해석하고 작업을 수행하는 역할
  • 엔진 자체는 작업을 수행만 할 뿐 비동기/동기와는 관계가 없음
  • 그래서 자바스크립트 엔진만 있으면 비동기 작업이 불가능함, 왜? 그냥 싱글스레드니까~
  • 예: V8(chromium), SpiderMonkey(Mozilla, firefox), JavaScriptCore(safari)

큐(Queue)

  • 비동기 작업을 마친 후 실행될 콜백함수가 쌓이는 곳
  • 예: Micro Task Queue - Promise에 then으로 붙은 것들, Task Queue보다 높은 우선순위를 가짐
    Task Queue(Macro Task Queue) - 그 외의 것들

이벤트 루프

  • 자바스크립트 코드가 비동기(non-blocking OR asynchronous)로 실행될 수 있도록 하는 역할
  • 이벤트 루프가 없으면 자바스크립트 코드는 모두 동기 방식으로 실행될 수 밖에… 실세임
  • 이벤트 루프는 자바스크립트 엔진 외부에 존재
  • 큐에 쌓인 콜백함수들을 꺼내서 자바스크립트 엔진에게 전달해주는 역할
  • 엔진은 해당 함수들을 전달받은 순서대로 실행

플랫폼 API

  • 비동기/동기 작업들의 묶음
  • 비동기 작업 실행은 대부분 플랫폼 API를 통해 일어난다.
  • 비동기 API의 경우 작업이 완료되면 큐에 콜백함수를 등록한다.
  • 자바스크립트는 플랫폼 API를 사용해서 비동기 작업을 위임하며 작업이 마쳤을 때 실행되는 콜백함수를 대부분 해당 API 매개변수로 전달한다. (비동기 API의 경우, Promise, aync/await의 경우 제외)
  • 예) setTimeout, setInterval, fetch, console, fs, path

자바스크립트 실행 환경 실생활 예시 : 음식 배달

Z라는 음식점에서 세 가지 배달 플랫폼을 사용: A, B, C

  • 각 플랫폼의 주문 요청 리스트:
  • 음식점 사장: 이벤트 루프
  • 음식점 주방장: JS엔진
  • 배달 기사: 플랫폼 API

  1. 음식점 사장은 오픈 시간에 각 배달 플랫폼에서 주문을 받을 수 있도록 설정
  2. 각 소비자는 각자가 원하는 플랫폼을 사용하여 Z 음식점 음식을 주문
  3. 음식점 사장은 한 플랫폼의 주문 요청 리스트를 순서대로 확인, 선입선출로 주문을 수락 및 주방으로 순서대로 요리 요청
  4. 음식점 주방장은 주문에 맞게 요리 + 완료되면 배달 기사에게 전달 + 배달 완료되면 배달 완료 알림
  5. 음식점 사장은 현재 확인 중인 요청 리스트에 주문 요청이 하나도 없으면 다음 플랫폼 리스트 확인
  6. 주문 요청이 하나도 없으면 대기
  7. 2~5과정 반복

전체 흐름 예제

  • 비동기 처리가 끝나면, 태스크 큐에 콜백 함수를 넣는다. fetch, XMLHttpRequest 등의 API 또한 마찬가지이다.
  • 이벤트 루프는 콜 스택이 비워지는 것을 기다렸다 콜 스택이 비워졌을 때, 테스크 큐에서 콜백 함수가 들어온 순서대로 꺼내서 콜스택으로 함수를 내보낸다. 단, 큐는 여러개가 존재하며 그 중 우선순위가 높은 큐에서 나중에 들어온 콜백함수가 실행될 수 있다.
function logA() { console.log('A') }
function logB() { console.log('B') }
function logC() { console.log('C') }
function logD() { console.log('D') }

// Click the "RUN" button to learn how this works!
logA();
setTimeout(logB, 0);
Promise.resolve().then(logC);
logD();

JS Visualizer 9000




04. 자바스크립트에서 비동기 작업을 수행하는 방식

전통: 콜백 패턴(callback pattern)

“나 끝났으니, 알려줘~”, 작업을 한 주체가 반대로 끝났다는 것을 알려줌

  • 비동기 작업들은 return이 아닌 별도의 결과를 반환하는 방식이 필요했고 콜백 패턴이 그 대안
  • 만약 콜백 패턴이 없었다면 JS는 비동기 작업의 완료 여부를 수동으로 확인했어야 했을 것
    ⇒ “끝났ㄴㅣ? 끝났니? 끝났니?”(polling), 이는 프로그램 측면에서 매우 낭비적인 행위
  • JS는 함수를 일급 객체로 취급했기에 가능
setTimeout(() => {
	console.log("1분 후 실행");
}, 60000)

콜백 패턴의 문제: 콜백 지옥

  • 중첩 함수로 인해 가로로 길어지는 코드
  • 매우 안좋은 개발자 경험(DX; Developer Experience)을 제공

혁명: 프로미스 패턴(Promise pattern)

  • 콜백 지옥에서 벗어나기 위한 움직임
  • 로직 상 순차적으로 호출되어야하는 비동기 함수들을 쉽게 연결해주는 then
  • 비동기 함수에서 에러가 발생했을 때 쉽게 에러 처리를 할 수 있도록 해주는 catch
  • 비동기 함수를 일관성 있는 형식으로 관리(then, catch, finally)
  • DX향상: 코드 가독성, chaining 기법
fetch('/data/sample.json')
	.then(response => response.json())
	.then(samples => samples.map(sample => sample.id))
	.catch(error => console.log(error.message));

Promise 패턴의 문제

  • 초기 호환성 문제: Promise가 ECMAScript 표준이 되기 이전에 만들어진 비동기 함수들
  • 아직은 아쉬운 DX: then 체이닝 → then() 지옥
// Promise화한 setTimeout
function setTimeoutToPromise(miliseconds) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve();
		}, miliseconds);
	});
}

// then-then-then-~,,~,,
setTimeoutToPromise(1000)
	.then(asyncFn1)
	.then(asyncFn2)
	.then(asyncFn3)
	.then(()=>{ //do something 
	})

Promise를 넘어서

then~then보다는 다른 언어들처럼 순차적으로 받아와서 작업을 할 순 없을까?

평정: async/await 패턴(문법)

  • 비동기 코드를 동기 코드와 같이 보자.
  • 가독성 향상, 익숙치 않은 사람들도 읽기 쉬움, 코드 흐름 보기 쉬움, 익숙한 문법
  • 높은 호환성: Promise로 되어있는 비동기 함수들을 async/await만 붙여서 손쉽게 코드 치환
  • await은 promise 반환 값을 받아옴
    • 즉, 겉으로 보기엔 새로운 문법이지만 결국에는 promise 문법임 (syntax sugar)
  • await은 async 함수 안에서만 동작, await들이 의존 관계일 시에 각각 await을 붙여줘야함
async function main() {
	try{
		const response = await fetch('/data/sample.json');
		const samples = await response.json();
		for(let i = 0; i < samples.length; i++) {
			/* do something */
		}
	} catch(error) {
		console.log(error.message);
	}
}
profile
💩 잘㈛는건 ㅇr닌데, 포ブl㈛ズl 않을꺼○F. ㄴr는 별로 코딩을 잘㈛ズl는 않ズl口ざ, ㄱH발ㅈr를 포ブl㈛ズl 않을꺼○F. ュ 정도로 포ブl를 먼저 んı작ㅎŁ⊂ト면, ㅇr무것도 도전㈛ヱ 싶ズl 않을 꺼○F. 💩

0개의 댓글