JavaScript 공부하기_Process(프로세스) & Thread(쓰레드), Event Loop, Task Queue & Micro Task Queue & Render

Lina Hongbi Ko·2023년 3월 29일
0

JavaScript-studying

목록 보기
5/6

✏️ 프로세스(Process)

: 프로세스는 운영체제 위에서 연속적으로 독립적인 메모리에서 실행되고 있는 프로그램 (프로세스 = 프로그램)
각각의 프로세스는 저마다 리소스(자원_메모리/데이터)들이 정해져있다.

리소스의 종류

  • Code : 프로그램을 실행하기 위한 코드
  • Stack : 프로세스 안에서 어떤 순서로 실행되는지, 함수가 끝나면 어디로 돌아가야 하는지 알려준다
  • Heap : 오브젝트를 생성하면 저장되는 공간 (동적변수가 저장됨)
  • Data : 전역변수나 static변수가 할당된다

✏️ 쓰레드(Thread)

: 쓰레드는 하나의 프로세스 안에서 동작하는데, 각각 해야하는 업무를 배정받아 그 프로세스 안에서 여러 쓰레드가 일을 한다. (프로그램 안에서 동시에 수행할 수 있는 작은 일꾼 단위)

예를 들어 내 프로그램에서 음악을 들으면서 사진을 편집할 수 있다면, 각각 음악을 재생하는 쓰레드, 사진을 편집할 수 있는 쓰레드, 등 각각 저마다 하는일이 다른 쓰레드들이 프로세스 안에 모여서 일을 한다. 그리고 쓰레드들이 동시에 다발적으로 발생할 수 있기 때문에 프로세스가 효율적으로 일할 수 있게 도와준다. 만약 쓰레드들이 없고 프로세스가 하나의 일밖에 하지 못한다면 다양한 일을 동시에 할 수 없었을 것이다.

쓰레드는 자신들이 일을 수행할 때 어디에서 어디까지 일을 했고, 그 다음에는 어디로 가야하는지 등 이런 고유의 흐름을 기억할 수 있는 Stack이 지정돼 있지만 Data, Code, Heap과 같은 공통적인 Data Resource는 쓰레드가 아닌 프로세스에 있다. 그리고 쓰레드들은 동시 다발적으로 접속해서 업데이트 해야하므로 서로 Data, Code, Heap을 공유하며 사용한다. 따라서, multi threading(한 프로세스 안에서 여러가지 쓰레드가 동시다발적으로 일어나는 것)을 잘못하면 문제가 생긴다.

💡 Java는 multi threading을 구현하지만, JavaScript는 single threading langauage이다.
이 문장 때문에 오해할 것 같은데 자바스크립트 언어 자체는 multi threading할 수 없고 single threading이지만, 자바스립트가 동작하고 있는 브라우저 안에서는 여러가지 thread가 들어 있다. -> Web APIs들을 이용하면 multi threading할 수 있다. 그리고 Web APIs 뿐만 아니라 event loop를 이용해서 multi threading 하는 것처럼 동작할 수 있다.

📍 JavaScript Runtime Environment (자바스크립트가 동작하는 실행환경)에서는 다양한 방식을 이용해서 multi-threading과 같은 효과를 줄 수 있다.

📍 JavaScript가 실행되는 런타임 환경에서는 eventloop를 통해 다양한 동작도 실행시킬 수 있다.

: 우리의 웹어플리케이션이 브라우저 위로 올라가는 순간, 자바스크립트 엔진이 우리가 작성한 소스코드를 한 줄씩 분석하고 실행하게 된다. 자바스크립트 엔진은 자바스크립트 언어 자체를 읽고 번역해서 실행해주는 실행기로 브라우저 안에도 자바스크립트 엔진이 있고, node JS환경에서도 자바스크립트 엔진이 있어서 우리가 작성한 코드가 브라우저와 node JS환경에서 실행될 수 있도록 실행시켜주는 엔진이다.

  • Memory Heap : 우리가 데이터를 만들 때 즉, 변수를 선언해서 오브젝트를 할당하거나 문자열이나 숫자를 할당하게 되면 그 데이터들은 이곳에 저장된다.
  • Call Stack : 우리가 함수를 실행하는 순서에 따라서 차곡차곡 쌓아 놓고 실행시킨다. (LIFO - Last in First out)
    또한 callstack 사이즈는 정해져 있어서 재귀함수나 영원한 반복문을 쓰면 에러가 발생한다.
function endless() {
	endless(); // 재귀함수
}
endless(); ..==// Error. maximum callstack size exceeded.
  • Web APIs : 브라우저에서 제공하는 APIs. 자바스크립트 언어 자체에는 setTimeout 이라는 함수들이 없기 때문에 브라우저 위에서 필요한 기능을 수행하는 브라우저용 함수들이 Web APIs들이다. 그래서 브라우저의 멀티 쓰레딩을 이용해 조금더 다양한 일들을 동시에 실행할 수 있다. ex) fetch, setTimeOut, 등

그럼 자바스크립트 엔진의 callstack은 순서대로 함수를 실행하는데, 그럼 Web APIs를 이용해서 등록한 콜백함수는 어떻게 동작할까? (어떻게 Web APIs와 엔진이 서로 일을 할까?)

✏️ Event Loop

위의 질문에 대한 답은 이 event Loop라는 얘가 중간자역할을 하면서 처리해준다. 즉, 아래에 설명하겠지만 여러가지의 큐에서 callstack으로 가져오는 역할을 한다.

일단 setTimeOut을 호출하는 순간, setTimeOut은 callstack에서 지워지고 Web API는 타이머를 시작한다. 타이머와 자바스크립트엔진은 병렬적으롤 실행되고 있다가(비동기) 지정된 시간이 끝나면 Web APIs는 Task Queue에 콜백함수를 집어넣는다. 그리고 지정된 setTimeOut이 끝나면 Task Queue에 있던 콜백함수를 event Loop가 callstack을 관찰하고 callstack에 일이 남아 있으면 비워질때까지 기다린다. main()함수까지 끝나면 eventloop는 비워진것을 확인하고 task Queue에 있던 콜백함수를 callstack에 넣어준다. 자바스크립트 엔진은 그리고 그 콜백함수를 실행시킨다.

💡💡 event Loop는 task Queue에서 한번에 하나만 callstack으로 가져온다 👉 이 말은 callstack에서 수행중인 함수나 다른 작업은 그것이 끝날때까지 보장된다.
즉, 그 중간에 다른 Task나 일들을 할 수가 없고, 지금 수행중인 코드블럭이 끝날때까지 event Loop가 기다렸다가 다음 밑에 있는 callstack이 수행되거나 task Queue에 있는 아이가 실행된다. (callstack에 있는 것들도 먼저 끝나야 taskQueue의 콜백함수가 실행되어지겠지만.)

✏️ Micro Task Queue & Render

task Queue 말고도 다른 Queue들도 있다. 얘네들은 또 다른 일을 처리한다:)

  • Task Queue : Web APIs에서 우리가 등록한 콜백함수를 특정한 이벤트가 발생했을때, 즉 지정된 이벤트가 발생했을때 Task Queue에 넣는다.
  • Micro Task Queue : 우리가 흔히 쓰는 Promise에 등록된 then 다음의 콜백함수를, 그리고 mutation observer에 등록된 Web API 중 하나의 콜백함수가 Micro Task Queue에 들어온다.
  • Render : 우리가 요소들을 브라우저에서 움직이고, 애니메이션을 줄 때 브라우저를 업데이트 해야하는데, 브라우저는 그 때 일정간격으로 요청한 작업들을 주기적으로 화면에 업데이트해준다. 이 때, 우리가 DOM요소를 변형한 것을 브라우저에서 표기하기 위해 Render Tree가 만들어져야 되고, layout(크기와 위치가 계산되어진 다음) 후 paint와 composit과정을 거쳐서 표기된다.

💡 Web API 중 하나인 Request Animation Frame()을 통해 콜백을 등록해 놓으면 다음에 브라우저가 업데이트 되기 전에 내 콜백을 실행해준다. (RAF -> 브라우저에서 다음 렌더링이 발생하기전에 해당하는 콜백이 수행되는 것을 보장해준다.)

📍 그렇다면 브라우저는 어떻게 얘네들을 가지고 순서대로 잘 실행시킬 수 있을까?

Event Loop가 callstack, Render, Micro Task Queue, Task Queue를 빙글빙글 돌면서 각각의 순서와 우선순위를 보며 task를 할당한다. 즉, callstack의 작업들이 일단 먼저 끝나야 event Loop가 돌면서 콜백함수를 각각의 큐에서 데려온다. 이 때 콜백을 데려올 때 우선순위와 정해진 규칙에 따라 콜백함수를 callstack으로 가져온다.

먼저 Render을 보면, Event Loop가 빙글빙글 루프를 돌면서 callstack에서 수행중인 함수가 있다는 것을 알면 다시 함수가 끝날때까지 기다린다. 그 함수들이 끝나면 Event Loop는 다시 돌면서 Render에 들릴 수도 있고, 들리지 않을 수도 있다. 이유는 브라우저는 1초에 60개의 프레임을 보여주려고 노력하는데 매번 1ms마다 Render를 들러서 업데이트해야할 이유가 없기 때문에 어느정도 시간이 지나고 다른일을 하다가 업데이트한다. 즉, Event Loop는 주기적으로 도는데 매번 Render을 들러서 업데이트하지 않는다는 얘기.

그리고 Micro Task Queue을 Event Loop가 지나는 시점이 되면 Queue 안에 들어있는 아이템(Promise, mutation observer)들이 없어질때까지 계속 callstack으로 Event Loop가 callstack에 콜백함수들을 넣는다. 예를 들어 Promise then 콜백함수를 callstack에 넣고 끝날때쯤 mutation observer의 콜백함수가 또 실행되면 이때는 event Loop가 Micro Task Queue가 비워질때까지 계속 머무르면서 콜백함수를 callstack에 넣어준다. (Task Queue는 event Loop가 전체적으로 callstack과 각 큐를 돌면서 하나씩 콜백함수를 넣어줘 자바스크립트 엔진이 실행하는 반면, Micro Task Queue는 event Loop가 돌지않고 머무르면서 콜백함수들이 순차적으로 모두 다 끝날때까지 자바스크립트 엔진이 실행시키도록 기다린다)

이제 Micro Task Queue가 텅텅 비게 되면 Event Loop가 다시 순회를 재개하면서 Task Queue로 넘어온다. Micro Task Queue에서는 앞서 말한것처럼 콜백함수를 하나만 callstack으로 보내놓고 콜백이 끝날때까지 기다린다. 그리고 다시 루프를 돌며 순회를 하는데 이때 브라우저 업데이트할 시간이 마침 다다르면 render sequence 큐에 들어와서 먼저 Request Animation Frame에 등록된 콜백함수들을 천천히 하나씩 다 실행한다음에 Render Tree를 만들고, 트리를 이용해 layout을 계산하고 paint을 통해 브라우저를 업데이트하고 composit 과정을 거쳐 우리는 바뀐 DOM요소들을 쉽게 확인할 수 있다.

이런식으로 event Loop가 각각의 큐와 callstack의 순회를 돌면서 자바스크립트 언어와 WebAPIs는 브라우저 위에서 동작한다.

📍 그렇다면 예제들을 통해 앞에서 말한것들을 확인해보자.

1.

<script>
const button = document.querySelector('button');
button.addEventListener('click', function() {
	const element = document.createElement('h1');
    document.body.appendChild(element);
    element.style.color = 'red';
    element.textContent = 'hello';
});
</script>

이 코드를 보면 요소를 먼저 집어넣고, style과 textContent를 지정해줬다. 이것은 좋은 코드일까?

🐶 정답: 상관없다.

Web API click event가 발생하면 콜백함수를 Task Queue에 넣는다. 그리고 콜백함수 안에서 작성된 코드는 위의 코드들처럼 어떤 것이 먼저 나오든 상관없이 callstack에 들어가는 순간 코드들이 다 실행될까지 event Loop가 기다리고 다시 rendering 즉, Render을 지나칠때 전체적으로 적용된 코드들이 layout,paint 과정을 거쳐 브라우저에 표기되기 때문에 위처럼 코드를 작성하는 것은 상관없다.

2.

<script>
const button = document.querySelector('button');
const box = document.querySelector('.box');
button.addEventListener('click', function() {
	box.style.transition = 'transform 1s ease-in';
    box.style.transform = 'translateX(800px)';
    box.style.transform = 'translateX(500px)';
});
</script>

이 코드도 마찬가지로 transform 효과를 몇개 쓰더라도 콜백함수가 다 실행될때까지 event Loop가 기다리기 때문에 결국 최종적으로 할당된 코드가 적용된다. 그리고 블럭 안의 코드들이 callstack에서 실행되는 동안 브라우저에서는 업데이트가 바로 안되기 때문에 결국 우리 눈에 보이는 건 마지막 코드만 볼 수 있다. 결국 최종적으로 할당된 코드가 적용되고, 콜백이 다 끝나고 최종적으로 render tree가 업데이트 되고 layout, paint 과정을 거쳐 브라우저에서 나타나므로 최종적으로 적은 코드만 실행된다.

3.

<html>
 <style>
 	button:hover {
    	background-color : orange;
    }
 </style>
</html>
<script>
const button = document.querySelector('button');
button.addEventListener('click', function() {
	while(true) {
    // repeat
    }
})
</script>

위의 코드를 보고 마우스로 버튼을 클릭하면 브라우저는 어떻게 동작할까?

🐶 정답: 버튼은 오렌지색으로 바뀌고, 오렌지색을 유지한채 렉에 걸린듯한 상황을 만들고 브라우저가 멈춘다.

마우스 호버 후 색깔이 바뀐 것을 브라우저는 처음 인식한다. 그리고 색을 오렌지색으로 바꾸는데, 이렇게 콜백에 while문을 넣어서 계속 끝나지 않게 만들면 callstack에서 콜백함수는 계속 실행되고 event Loop는 언제 끝나는지 목빠지게 기다리다가 렉에 걸리는 듯한 상황을 만든다. 그리고 렌더링과정을 거치지 못하므로 다음 단계로 event Loop가 돌지 못해서 브라우저는 멈춘다. 그러므로 callstack에 등록하는 함수를 만들 때, 오랫동안 일을 하게 만드는 것은 좋지 않다.
💡 루프를 만드는 반복문이나 재귀함수는 조심히 사용해야 하는 이유이다.

4.

<html>
 <style>
 	button:hover {
    	background-color : orange;
    }
 </style>
</html>
<script>
function handleClick() {
	console.log('handleClick');
    setTimeout(()=>{
    	console.log('setTimeout');
        handleClick();
    }, 0);
}

const button = document.querySelector('button');
button.addEventListener('click', function() {
	handleClick();
})
</script>

여기에서는 마우스로 버튼을 클릭했을때 브라우저는 어떻게 동작할까?

🐶 정답: 버튼은 클릭할때마다 오렌지색으로 잘 바뀌고 console창에는 handleClick, setTimeout 이라는 문구가 계속 출력된다.

클릭했을때 handleClick()이 task Queue에 들어가고, callstack에 들어가면 함수를 실행시킨다. 콘솔을 찍고 setTimeout을 실행시키는데 다시 handleClick()을 실행하므로 task Queue에 handleClick() 함수들이 쌓이고, callstack에서 하나씩 끝날때까지 기다렸다가 넣어준다. 그러면서 event Loop는 순회하면서 처리하는 도중에 render쪽으로 가끔씩 가서 rendering 과정을 처리한다. 그리고 다시 순회하고 이 과정을 반복한다. 그래서 3번의 코드와는 달리 4번의 코드는 렌더링 과정을 계속 거치기 때문에 DOM에게 준 효과들이 잘 적용된다.

5.

<html>
	<style>
    	button:hover {
        	background-color: orange;
        }
    </style>
</html>
<script>
function handleClick() {
	console.log('handle Click');
    Promise.resolve(0)
    	.then(()=>{
        	console.log('then');
            handleClick();
        })
}
const button = document.querySelector('button');
button.addEventListener('click', function() {
	handleClick();
})
</script>

이 코드에서 브라우저는 어떻게 작동할까?

🐶 정답: 마우스를 버튼 위에 올리면 오렌지색으로 변하고 클릭하게 되면 콘솔창에 handle Click, then 이라는 문자들이 계속 출력되고 마우스가 클릭한 상태로 브라우저는 멈춘다.

event Loop가 Task Queue에서는 콜백함수를 하나씩 가져오고 순회하고 다시 하나씩 가져오는 반면, Micro Task Queue는 큐 안에 있는 콜백함수들이 다 끝나야 순회하는 특징이 있다. 그리고 이 코드에서는 Promise를 이용했기 때문에 Micro Task Queue를 사용하는데, 먼저 과정을 살펴보면 버튼을 클릭하고나서 handleClick()이 Task Queue에 들어가고 callstack으로 들어가서 실행된다. callstack에서 handleClick()를 실행하다가 Promise를 만나면 then의 콜백함수가 Micro Task Queue에 넣어지게 되고, Promise를 계속 실행시킨다. 그리고 Promise가 다 완료되면 event Loop는 다시 돌다가 Micro Task Queue를 들를때 then의 콜백함수를 실행시킨다. 하지만 여기서 Micro Task Queue는 함수들이 모두 끝날때까지 event Loop가 머무르므로 위의 코드처럼 재귀함수를 입력하게 되면 브라우저가 렌더링 되지 않은 상태이므로 버튼은 그대로 오렌지색을 유지하고 브라우저는 멈추게 되는 상태가 되는 것이다.

💡 그렇다면 Micro Task Queue가 꽉 차서 브라우저에서 허용하는 용량이 넘으면?? -> 에러 발생후 끝.

덧붙여 설명하자면, 렌더링 기회는 Promise가 언제 끝나느냐에 따라서 한번이 될 수도, 두번 그 이상이 될 수 있다. Promise.then.catch.finally 이렇게 각각 체인들은 비동기적으로 동작하고 하나의 체인이 다 끝나야지 그 다음에 다시 연결된 .then의 콜백함수가 Micro Task Queue에 들어가게 된다. 그래서 Promise체인은 콜백지옥으로 연결해도 하나의 체인이 끝나면 다시 새로운 Promise가 리턴되고, Promise의 기능이 다 완료되면 다시 그에 해당하는 then의 콜백이 Micro Task Queue에 들어가기 때문에 evnet Loop를 막지 않는다. 그냥 Micro Task Queue의 콜백이 끝나길 기다리고 머물뿐.

6.

<script>
	const button = document.querySelector('button');
    button.addEventListener('click', function() {
    	requestAnimationFrame(()=>{
        	document.body.style.backgroundColor = 'beige';
        });
        requestAnimationFrame(()=>{
        	document.body.style.backgroundColor = 'orange';
        });
        requestAnimationFrame(()=>{
        	document.body.style.backgroundColor = 'red';
        });
        setTimeout(()=>{}, 0);
    })
</script>

그럼 이 코드는 브라우저를 어떻게 작동시킬까?

🐶 정답: 버튼을 클릭하면 문서의 색이 빨간색으로 바뀐다.

requestAnimationFrame() API는 우리가 등록한 콜백함수가 나중에 브라우저에서 다음 렌더링이 발생하기 전에 콜백이 수행되는 것을 보장해준다. (브라우저에게 다음에 네 화면이 업데이트 되기 전에 내가 등록한 콜백함수를 수행해줘 하고 등록해놓는 것) 보통 실시간으로 계속 화면에 업데이트 해야하는 경우(애니메이션, 게임 등)에 많이 사용한다.
💡 setInterval로 랜덤하게 주기적으로 업데이트하는 것보다 더 효율적이다.

실행과정을 살펴보면, button에 이벤트가 발생하면 Task Queue에 콜백을 등록한다. 그리고 event Loop가 순회하면서 Task Queue에서 callstack으로 addEventListener Web API의 콜백함수를 가지고 온다. 그리고 Listener의 콜백이 수행되는데 콜백에서 requestAnimationFrame()을 다시 호출하므로 Render의 Request Frame Animation 큐에 콜백들을 등록하고 계속 listener의 콜백이 끝날때까지 event Loop는 기다린다. 그리고나서 listener의 콜백이 끝나면 다시 순회하면서 requestAnimationFrame() API를 사용했기 때문에 브라우저에게 렌더링을 업데이트해야겠다고 하고 렌더링을 시작한다. 순회하다가 Render큐를 돌게 되면 그때 이제 requestAnimationFrame의 큐에 등록된 콜백들을 순서대로 FIFO의 순서대로 실행시키고 render tree, layout, paint과정을 거치고 브라우저는 업데이트한다. 여기서 backgroundColor의 색깔이 마지막에 red였고, 이런 콜백이 RAF 큐에서 마지막으론 남아 있다가 적용되어서 빨간색이 배경색이 된다.
📍 Request Animation Frame -> RAF

이런 방법은 click Listener가 수행될때는 코드를 변경하지 않고 나중에 브라우저가 화면을 업데이트하기 전에 우리가 등록한 변경사항을 적용해서 넣는 것이다. -> 지금 현재 코드가 실행되는 순간에 업데이트하기 보다는, 화면이 업데이트 되기 전에 한번만 실행하는 것이 좋을 때 사용.

💡 setTimeout을 등록하면 지금 수행되고 있는 callstack 안에서의 코드가 실행되는 순간 말고 (event Listener 안의 코드가 다 읽히고) 다시 event Loop가 한바퀴 돌때 그 다음 코드블럭(setTimeout 안의 코드)을 실행해달라고 할 수 있다.


출처
드림코딩아카데미 브라우저 101 강의 듣고 정리한 내용

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글