자바스크립트는 싱글 스레드를 기반으로 동작합니다.
자바스크립트의 문법을 적당히 배우고 DOM 조작을 통해 HTML과 CSS를 건드리는 작업도 해보고, NodeJS를 통해 간단한 서버도 작성해보고 이 후엔 프론트엔드에 유용한 라이브러리인 React를 배울 때도 언제나 저 한 줄은 머리 어딘가에 있었다.
나름대로 뭐 스레드는 프로세스 안에서 작동하는 처리의 흐름이고 자바스크립트는 프로세스안에 스레드가 하나만 존재하면서 작동하겠지 싶으며 넘어 갔지만 사실 정확히 어떤 의미로 어떻게 자바스크립트가 동작하는지는 몰랐다.
하지만 이벤트 루프에 대해 어느정도 공부하고 난 후엔 지금까지 작성했던 모든 내 자바스크립트 코드가 가짜라는 사실을 깨달았다.
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
이 코드를 실행하면 어떻게 될까? 그냥 단순히 생각했을 때는 hello가 2초마다 하나씩 출력될 것이다.(나만 그렇게 생각했을 수도 있다.) 하지만 사실은 2초후에 hello가 네 번 연속으로 찍힌다. 이 예제를 여러 번 접했지만 그 때마다 이해했다라고 착각하고 넘어간 기억이 많다. 나와 같은 기억이 많은 초보 개발자라면 이 글이 조금이라도 도움이 되기를 바란다.
최근에 코틀린을 통해 안드로이드에서 스레드를 열고 루퍼를 조작하고 코루틴을 생성하고 이것저것 해보았는데, 공부하면서도 둘다 UI단을 만드는데 어쩌면 비슷하지 않을까? 라고 생각했는데 굉장히 유사한 점을 가지고 있었다.
자바스크립트는 싱글 스레드이므로 흐름 그 자체가 메인 스레드이고 이 스레드의 진행을 복잡한 연산, 네트워킹, DB 작업 등이 흐름을 막는다면 다른 UI 작업 및 또 다른 연산들이 불가능해진다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onClick="task()">job button</button>
<script>
let i = 0
function task(){
console.log('doing my job')
}
while(i < 10000){
i++
console.log('I am blocking the thread')
}
</script>
</body>
</html>
위와 같은 코드를 브라우저에서 열면 while 루프에서 열심히 자바스크립트가 증가하는 i를 처리한다. 그리고 그 처리과정에 콘솔에 출력될텐데 그 중간에 job button
을 클릭하면 동작하지 않는 것을 볼 수 있다.
왜냐하면 자바스크립트는 싱글 스레드로 동작하기 때문에 while을 계산하는 동안 task 함수를 처리할 수가 없는 것이다. i가 10000에 도달하고 나면 그제서야 job button
이 작동하게 된다.
자바스크립트는 콜스택에 의해 동작하게 된다. 먼저 자바스크립트가 동작하는 내부 구조를 살펴보면
위와 같은 구조를 가진다. 이중에서 먼저 STACK 부분만 살펴보도록 하자.
function a(){
return 'a'
}
function b(){
return a()
}
function c(){
return b()
}
function main(){
console.log(c())
}
main()
위 코드를 실행하면 결과적으로 콘솔에 a가 출력된다. 쉽지만 이를 콜스택에 연관지어 설명하면
먼저 자바스크립트는 함수이름을 전부 읽어놓고 실행 되는 main()
을 콜스택에 넣게 된다.
그리고 main()
을 보면 안에 c()
를 로그하도록 되어 있기 때문에 c()
를 로그 하는데 그 안에는 b()
가 있고 그안에는 a()
가 있고 최종적으로 'a' 를 반환하게 되는 구조를 차곡차곡 콜스택에 쌓게 된다.
이렇게 콜스택에 쌓이면 위부터 하나씩 꺼내서 실행을 하게 된다.
그렇다면 비동기 작업은 이 자바스크립트의 코어에서 어떻게 동작할까?
console.log('first task')
setTimeout(() => {
console.log('network task')
}, 2000)
console.log('second task')
위 코드의 결과는
이렇게 된다. 당연히도, 그렇다면 콜스택의 입장에서 보자.
바로 위에서 설명한 콜스택처럼 return으로 계속 스택이 쌓이는 구조가 아니고 first task가 들어오면 완료시켜서 콜스택을 초기화하고 그다음 settimeout이 들어와서 처리하고 그 다음 second task가 들어오고 이렇게 따로 따로 실행하고 콜스택이 비워 지는 방식으로 작동합니다. 그림은 순서적인 측면만을 보여준 것입니다.
그런데 콜스택의 입장에서 보면 first task
를 하고 settimeout
을 하고 그 다음에 second task
를 할 것 같은데 이 settimeout
은 어딘가로 사라지고 second task
가 먼저 실행된다. 여기서 Web API
즉 백그라운드 처리의 개념이 등장한다.
콜스택이 setTimeout
을 만나면 이 setTimeout
의 콜백을 백그라운드 태스크(WEB APIs)로 넘겨버린다. 다시 맨 처음에 봤던 그림을 보면
오른쪽에 WEB APIs라고 적힌 부분이 부분에 setTimeout
의 콜백을 2000ms
라는 타이머를 담아서 넘겨버리고 콜스택을 비운다. 그리고 다음 second task
를 실행하게 되는 것이다.
즉 비동기적으로 처리를 할 부분을 콜스택이 만나게 되면 그 처리를 WEB APIs
에 넘겨버리는 것이다. 그리고 그 처리가 끝나면, 즉 여기서는 타이머가 끝나면 그 작업 결과를 아래에 콜백 큐에 넣어준다.
이벤트 루프는 이제 이 콜백 큐와 콜 스택을 관리하는 역할을 하게 된다. 콜 스택과 콜백 큐를 보고 있다가 콜 스택이 비고 콜백 큐에 WEB APIs가 넘긴 작업이 있다면 그 작업을 콜 스택에 넣어는 역할을 하게 된다.
console.log('first task')
setTimeout(() => {
console.log('network task')
}, 2000)
console.log('second task')
결국 여기에서 first task
를 처리하고 setTimeout
은 백그라운드로 넘긴 뒤 second task
까지 처리하고 2000ms의 타이머가 완료되면 이벤트 루프가 콜스택을 보고 있다가 비어있는 것이 확인되면 console.log('network task')
를 콜스택에 넣게 되는 순서로 코드가 실행되는 것이다.
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
setTimeout(() => { console.log('hello') } , 2000)
자 그러면 결국 이 코드는 왜 2초마다 실행되는 것이 아니고 2초 후에 한 방에 실행되는지 이해가 될 것이다. hello
를 로그하는 함수가 하나 씩 백그라운드로 들어가고 2초를 각각 세기 시작하고 콜스택이 비워질 때마다 바로바로 hello
를 내뱉기 때문이다.
웹 어플리케이션에서 자바스크립트의 비중이 아주 커졌기 때문에 이러한 동작 방식을 정확히 이해하는 것은 아주 중요하다. 이벤트 루프와 콜스택의 동작 방식을 이해하고 나니 코드를 작성하면서도 내부에서 이런 동작을 하겠구나라는 부분이 머리 속으로 그려진다. 그리고 그 과정을 겪어야만 좋은 코드를 작성할 수 있을 것이다.