2. FrontEnd/Javascript / / 2022. 8. 17. 20:17

[JS] 싱글 스레드 언어

728x90

✅ Javascript : 싱글 스레드 언어

싱글 스레드 = 한 번에 하나의 작업만 할 수 있다

👉🏻 one thread == one call stack == one thing at a time

 

자바스크립트는 싱글 스레드 언어이기에 함수를 실행하면 함수 호출이 스택에 순차적으로 쌓이고, 스택의 맨 위에서부터 차례대로 한 번에 하나의 함수만 처리할 수 있다.

간단한 프로그램이라면 상관없지만 아주 복잡한 프로그램을 구동한다고 생각했을때, 시간이 매우 오래 걸리는 작업이 스택에 쌓이고 실행되면 그 다음 작업은 무한정 대기할 수밖에 없다.

이처럼 다른 작업을 실행하기 위해서 이 전 작업이 완료될 때까지 기다려야만 하는 상황을 블로킹(blocking)이라고 한다.

극복하기 위한 해결 방안이 바로 Asynchronous Callbacks(비동기 콜백)이다

 

자바스크립트가 싱글 스레드 언어임에도 불구하고 웹 사이트에서 끊김없이 여러 작업을 동시에 할 수 있는 것은 바로 브라우저가 Web APIs 같은 것들을 제공하여 비동기 작업을 가능하게 해주기 때문이다.

✅ 기본적인 동작 원리

함수를 동기 호출하게 되면 call stack에 차곡차곡 쌓여 순차적으로 실행된다.

만약 AJAX | setTimeout | DOM event 함수를 실행하면, 자바스크립트 엔진은 call stack에서 Web APIs로 보내고 정해진 시간 혹은 이벤트가 발생한 순간에 순차적으로 callback queue에 적재한다.

callback queue에 줄을 선 함수들은 call stack에 쌓여있던 것들이 모두 제거되어 깨끗해지면 EventLoop가 차례대로 스택에 쌓여서 실행되게 된다.

 

▶️ Event Loop

자바스크립트 엔진이 아닌, 구동하는 환경(브라우저, 노드)에서 가지고 있는 장치

콜 스택과 태스크 큐(= 콜백 큐)를 감시하며, 콜 스택이 비어있을 경우에 태스크 큐에서 태스크(= 콜백함수)를 가져와 콜 스택에 넣어 실행시키는 기능을 한다.

태스크 큐 말고도 마이크로태스크 큐 (Microtask Queue)가 존재하고, 이는 Promise의 동작 방식과 연관이 있다. 

 

▶️ 큐(Queue)

큐는 스택(Stack)과 같이 자료 구조의 일종

자료의 입력과 출력을 한 쪽 끝(front, rear)으로 제한한 자료구조.

 

한 쪽에서만 삽입과 삭제가 이루어졌던 스택과는 달리 한쪽에서는 삽입이 되고 다른 한쪽에서는 삭제 작업이 이루어지는 자료 구조이다.

put(), get()

가장 먼저 삽입된 자료가 가장 먼저 삭제되는 구조이므로 선입선출(FIFO: First In First Out)이라고도 부른다.

▶️ 마이크로테스크큐 -> 애니메이션 프레임 -> 테스크큐

큐 모두 콜백함수가 들어간다는 점에서 동일하지만 어떤 함수를 실행하느냐에 따라 어디로 들어가는지가 달라진다.

또한 명칭은 큐 (Queue) 이지만 실제 우리가 아는 자료구조의 큐와는 다르다.

 

엄밀히 말하자면 우선순위 큐 (Priority Queue) 라고 할 수 있는데, 이벤트 루프가 2개의 큐에서 태스크를 꺼내는 조건이 “제일 오래된 태스크” 이기 때문이다. (동작방식을 확인하고 싶다면 HTML 스펙 을 보자)

  • 콜백함수를 태스크 큐에 넣는 함수들
    • setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI 렌더링
  • 콜백함수를 마이크로태스크 큐에 넣는 함수들
    • process.nextTick, Promise, Object.observe, MutationObserver

Web API의 setTimeout()의 콜백함수가 태스크 큐에 들어가고 Promise의 콜백함수가 마이크로태스크 큐에 들어간다

이벤트 루프는 각 콜백함수를 태스크/마이크로태스크 큐에서 꺼내쓰는 것인데, 이 중 마이크로테스크 큐를 가장 먼저 꺼내어 쓴다.

 

  • 애니메이션 프레임 = request animation frames와 같이 브라우저 렌더링 관련 task들

▶️ 스택(Stack)

call stack은 프로그램 상에서 어떤 순서로 작업을 수행하는지 기록하는 작업 스케쥴링과 관련된 자료 구조

어떤 함수를 실행하게 되면 우리는 그 함수를 스택의 맨 위에 놓는것을 push라고 한다.

만약 함수가 어떤 값을 리턴,실행종료하면 다시 그 함수를 스택 맨 위에서부터 꺼내는데 이를 pop이라고 한다.

 

이러한 구조로 함수가 호출되기 때문에 만약 잘못해서 무한 호출되는 재귀 함수를 실행시키면 스택에 함수 호출이 계속해서 쌓이다가 Maximum call stack size exceeded라는 에러 메시지를 만나게 된다.

 

- 자료의 입력과 출력을 한 곳(방향)으로 제한한 자료구조.

- LIFO(Last In First Out)구조

- push(), pop()

실행과정 예시

function main(){
	console.log(1);
	setTimeout(function cb() {
		console.log(2);
	}, 5000};
	console.log(3);
}
//1 3 2

먼저 main() 함수가 실행되고, console.log(1)이 스택에 쌓인다.

console.log(1)이 실행되어 콘솔창에 1이 출력되고 setTimeout의 콜백함수 cb가 스택에 쌓이는데, setTimeout은 브라우저에 의해 제공된 API로 자바스크립트 엔진에서 처리하지 않고 바로 web APIs로 넘긴다.

그러면 브라우저는 마치 setTimeout 함수가 완료된 것처럼스택에서 pop하고 다음 작업을 진행하므로 console.log(3)이 실행되어 콘솔 창에 3이 출력된다.

모든 코드가 실행되었으므로 main() 함수가 스택에서 제거되고, 5초 동안 대기하고 있던 cb 함수가 5초가 지난 시점에 task queue에 들어온다.

stack이 비어있으므로 cb 함수를 stack에 적재하고 console.log(2)를 실행하게 된다.

✅ 만약에setTimeout 함수를 0초 후에 실행하도록 코드를 변경하면 어떤 결과?

이 경우에도 결과는 크게 다르지 않다.

task queue에 줄 서 있는 callback 함수들은 stack이 비어있을 때만 stack으로 이동할 수 있기 때문이다.

위 경우 setTimeout으로 설정한 cb 함수는 web APIs로 이동하는 즉시 task queue로 이동하게 되는데, stack이 비어있지 않기 때문에 대기 상태로 있게 되고 console.log(3)이 출력되고 스택이 클리어되면 이동하여 console.log(2)가 실행된다.

<aside> 👉🏻 모든 web APIs는 위와 같은 방식으로 작동된다. AJAX나 DOM 이벤트도 동일하다.

</aside>

document.querySelector('button').addEventListener('click', function () {  
	console.log('clicked');
});

브라우저는 일단 click event 함수를 call stack에 저장하고 이는 즉시 web APIs로 옮겨진다.

그 상태로 무한 대기하고 있다가 사용자가 버튼을 클릭하는 순간 click 이벤트의 콜백 함수는 callback queue로 이동한다. 그리고 stack이 비는 순간에 stack으로 이동하여 함수를 실행한다.

만약에 사용자가 버튼을 10번 누른다면 callback queue에 10개의 콜백 함수가 쌓일 것이고먼저 들어온 콜백부터 순차적으로 스택으로 이동하여 실행되고 없어지고를 반복할 것이다.

setTimeout(function timeout () {
  console.log(1);
}, 1000);

setTimeout()을 4번 썼다고 생각하면 이 경우 각각의 함수들은 stack -> web APIs로 이동하고 차례대로 callback queue에 쌓이게 된다.

이 때 가장 먼저 적재된 timeout 함수부터 순차적으로 stack에 "이동 -> 실행 -> 제거"를 반복하게 된다. 즉, 4개의 함수를 1초 후에 실행하라고 설정했다고 해서 모든 이벤트가 동시에 실행되는 것이 아니다. 이 점을 항상 유의해야한다.

AJAX나 setTimeout() 등을 이용하면 마치 자바스크립트가 여러 가지 일을 동시에 수행하는 것처럼 보이지만 그것은 일종의 눈속임일 뿐이고 자바스크립트는 오직 한 번에 하나의 작업만을 수행한다.그렇기 때문에 위의 timeout 이벤트는 queue에 줄 서서 하나씩 차례대로 스택으로 이동하여 실행된다.

그러므로 setTimeout()과 같은 메소드로 어떤 시간을 설정한다고 해서 아주 정밀하고 정확한 시간이 보장되는 것은 아니다. 모두 미세한 오차가 존재하며 브라우저는 단지 그 오차를 최소한으로 줄여줄 뿐이다.

window.addEventListener('scroll', function () {
  console.log('hello');
});

스크롤 이벤트를 발생시키는 함수의 경우, 스크롤 이벤트는 약간의 움직임에도 많은 이벤트가 실행된다.

이 경우 브라우저는 아주 많은 콜백을 callback queue에 적재하게 된다.

만약 위와 같이 아주 간단한 코드 한 줄이라면 상관없겠지만 매우 복잡한 이벤트가 일어나야한다면 프로그램 성능에 좋지 않은 영향을 줄 것이다. 그래서 디바운싱(debouncing)을 통해 이벤트가 큐에 적재되는 속도를느리게 만든다거나 하는 방법을 사용할 수 있다.

 

 

✅ Web worker_멀티 스레드 구동가능

 

▶️ Web worker 란?

HTML 페이지에서 스크립트를 실행할때 그 페이지는 스크립트가 완료할때 까지 응답하지 않는다.

이를 해결하기 위해 Web worker를 사용한다.

Web worker는 페이지의 퍼포먼스에 영향을 주지 않고 다른 스크립트와는 독립적으로 백그라운드에서 실행되는 javascript이다.

기존의 웹은 다중 스레드가 불가능하기때문에 작업이 끝나기 전까지 UI 멈춰버리는 경우가 발생했지만, Web worker 덕에 웹은 멀티 스레드 구동이 가능하다.

👉🏻 Web worker는 멀티스레드 기능을 지원하며 워커가 생성될 때마다 자바스크립트를 실행할 수 있는 고유스레드를 생성하여 속도성능을 크게 향상시킬 수 있다. 워커에서 실행하는 코드는 브라우저 UI에도, 다른 워커에서 실행하는 코드에도 영향을 주지 않는다.
👉🏻 즉, Web worker = 독립적으로 실행되는 멀티스레드

▶️ Web worker 사용의 적절한 상황

👉🏻 로딩과 실행이 오래걸리는 자바스크립트파일에 사용한다.

  • 매우 큰 문자열의 암호화/복호화
  • 복잡한 수학계산(이미지/비디오 처리 포함)
  • 매우 큰 배열의 정렬
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유