게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌51 - JavaScript - 7년 전 등록

호출 스택과 이벤트루프

안녕하세요. 이번 시간에는 호출 스택이벤트루프에 대해서 알아보겠습니다! 정말 오랜만에 자바스크립트 파트 강좌를 올리네요. 사실 웬만한 것들은 다 다뤘다고 생각해서 50강으로 끝내려고 했는데, 정작 제일 중요한 이벤트루프를 다루지 않았습니다. 사실 제가 이벤트 루프를 최근에 와서야 제대로 이해하게 되었기 때문이기도 합니다.

호출 스택과 이벤트 루프는 자바스크립트 코드 동작 원리를 이해하는데 필수이기 때문에 꼭 알아두고 가세요. 이걸 모른다면 자바스크립트를 모르는 겁니다. 단, 입문자 분들도 쉽게 이해하실 수 있도록 복잡한 엔진 내용은 제외하고 핵심적인 개념만 간추려서 설명하겠습니다. 따라서 실제보다 몇 과정이 간소화되어 있을 수도 있습니다.

호출 스택

먼저 호출 스택에 대해 알아봅시다. 단, 실행 컨텍스트가 뭔지 미리 알고 계셔야 합니다. 실행 컨텍스트를 찬찬히 보다 보면 이게 스택 과 비슷하다는 것을 알 수 있습니다. 먼저 다음 코드의 실행 순서를 예측해봅시다.

function first() {
  second();
  console.log('첫 번째');
}
function second() {
  third();
  console.log('두 번째');
}
function third() {
  console.log('세 번째');
}
first();
third();

쉽죠? 세 번째, 두 번째, 첫 번째, 세 번째 순으로 콘솔이 나옵니다. 실행 컨텍스트의 원리를 따라 분석하면 됩니다. 그림으로 그려보면 아래와 같습니다.

undefined

무려 제가 그림판으로 그림을 그려보았습니다. ㅎㅎ. first()가 호출되었을 때의 상황입니다. 처음의 main()은 전역 컨텍스트라고 생각해주세요. 크롬에서는 main 대신 (anonymous)함수로 나옵니다. first가 호출되고, first 안의 second가 호출되고, 마지막에 second 안의 third가 호출됩니다. 호출된 순서와 반대로 실행이 되는데요. 실행되면 저기 쌓여진 더미에서 제거됩니다. LIFO(마지막에 들어온 것이 먼저 나감) 구조죠. 호출 "스택"이라고 부르는 이유입니다.

third가 실행되고 지워지고, second가 실행되고 지워지고, first가 실행되고 지워지면 main만 남았죠?그 후 third()가 호출됩니다. 그러면 third가 스택에 들어가고, 실행되고 다시 지워집니다. 마지막으로 전역 컨텍스트 main이 지워집니다. 그러면 호출 스택에는 아무것도 남지 않게 되겠죠. 이러면 실행 완료입니다.

다음과 같은 에러를 보신 적이 있을 겁니다.

Uncaught RangeError: Maximum call stack size exceeded

바로 호출 스택이 가득 찼을 때 발생하는 에러인데요. 재귀함수같이 함수 안에서 계속 다른 함수를 호출하다보면 저기 스택이 가득 차다못해 터져버립니다. 그래서 나오는 에러입니다. 브라우저마다 호출 스택 최대치가 다른데요. 요즘은 10만 개가 넘는 브라우저도 있습니다. 하지만 보통 만 개 정도라고 생각하시면 편합니다. 함수를 만 번 이상 중첩해서 호출하지 마세요!

이벤트 루프

이벤트 루프는 실행 컨텍스트와 함께 필수적으로 알고 있어야 하는 개념입니다. 자바스크립트와 노드에서 사용되는데요. 자바스크립트는 보통 싱글 쓰레드라고 불리는데, 바로 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문입니다.

다음 코드의 순서를 예측해보세요.

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 3000);
console.log('끝');

정말 쉽죠? 시작, 끝이 콘솔에 찍힌 후 3초 후에 동작이 콘솔에 찍힙니다. 그렇다면 이것은요?

function run() {
  console.log('동작');
}
console.log('시작');
setTimeout(run, 0);
console.log('끝');

이번에는 3초가 아니라 0초입니다. 하지만 콘솔에는 그대로 시작, 끝, 동작이 찍힙니다. 0초면 바로 실행하라는 것 같은데 왜 끝이 먼저 콘솔에 찍힐까요? 호출 스택으로 설명할 수 있을까요?

아마 아무도 할 수 없을 겁니다. 눈을 감고 머리속으로 따라와보세요. 전역 컨텍스트 main()이 호출 스택에 들어가고, console.log('시작')이 들어갑니다. 콘솔이 실행되어 호출스택에서 빠지고, 다시 setTimeout()이 들어갑니다. setTimeout이 실행되어 빠지고, console.log('끝')이 들어갑니다. 이제 3초 뒤에 run이 실행되어야 하는데요. 호출 스택에는 run이 없는데 어떻게 실행된 걸까요? 바로 여기서 이벤트 루프백그라운드, 태스크 큐가 나옵니다.

undefined

setTimeout 3초의 경우를 자세히 살펴봅시다. setTimeout이 호출되고 지워지면서 백그라운드로 run 함수와 함께 3초 타이머를 보냅니다. 백그라운드는 3초를 센 후 태스크 큐에 run 함수를 보냅니다.

undefined

이벤트 루프는 항상 대기하고 있다가 호출 스택이 비워지면(전역 컨텍스트 main 실행이 종료되면) 태스크 큐에서 함수를 하나씩 호출 스택으로 밀어 올립니다.

undefined

이제 run 함수가 실행되고 호출 스택에서 지워지게 됩니다. 이벤트루프는 태스크 큐에 새로운 함수가 들어올 때까지 대기합니다.

setTimeout 0초도 마찬가지입니다. 일단 setTimeout을 하는 순간 백그라운드를 거쳐 태스크 큐로 run 함수가 이동하기 때문에 끝이 먼저 콘솔에 찍히고 동작이 찍힙니다. (사실 setTimeout 0도 기본적으로 4ms의 지연 시간을 갖고 있어서 setTimeout 4ms와 마찬가지입니다. 노드는 1ms의 지연 시간을 갖고 있습니다)

참고로 백그라운드에서 3초를 정확하게 세어 주었다고 하더라도, 호출 스택에 함수들이 가득차 있다면 3초 후에 실행되지 않을 수도 있습니다. 이벤트 루프가 태스크 큐에서 run 함수를 호출 스택으로 끌어올리지 못하거든요. setTimeout의 초가 정확하지 않을 수도 있는 이유입니다. 호출 스택에서 너무 많은 일을 하게 되면 태스크 큐에 쌓인 콜백 함수들이 제 때에 실행되지 않기 때문에 너무 버거운 일들은 하지 않는 게 좋습니다.

위에서 나왔던 Maximum call stack size exceeded 에러도 setTimeout 0을 사용해서 극복할 수 있습니다. setTimeout을 하는 순간 호출 스택에 함수가 쌓이는 게 아니라 백그라운드를 거쳐 태스크 큐로 넘어가기 때문에 호출 스택이 터지는 일이 발생하지 않습니다.

이제 모든 현상이 설명이 되죠? 호출 스택 외에도 백그라운드와 태스크 큐, 이벤트 루프가 존재했던 겁니다. 백그라운드를 사용하는 작업은 타이머 외에도 ajax 요청, 이벤트 리스너나 FileReader 등이 있습니다. 자바스크립트 기본 제공 메소드 중 콜백 함수를 사용하는 것들은 백그라운드를 사용하는 경우가 많습니다.

이벤트 루프의 동작을 시각적으로 보시려면 http://latentflip.com/loupe/ 여기를 이용하세요.

여기서는 백그라운드와 태스크 큐, 이벤트 루프가 정확히 어떻게 구현되어 있는지에 대해서는 다루지 않습니다. 심지어 태스크 큐 외에도 마이크로태스크 큐, 잡 큐 등이 더 있어 이벤트 루프가 실행하는 순서가 다릅니다. 하지만 위의 그림만 명확하게 이해하신다면 앞으로 자바스크립트 코딩을 하는 데 있어 큰 문제는 없을 겁니다. 이상으로 자바스크립트 강좌를 마칩니다. (그래놓고 몇 개 더 추가할 수도..?)

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

17개의 댓글이 있습니다.
한 달 전
일부 이미지가 보이지 않습니다. 업로드 부탁드려도 될까요~?
7달 전
답변 감사합니다. 혹시 그러면 무거운일 사이사이에 가벼운일이 실행되게끔 끼워넣어주는 건 브라우저에서 자동으로 해주나요? 그리고 혹시나 "백그라운드와 태스크 큐, 이벤트 루프가 정확히 어떻게 구현되어 있는지에 대해서" 다루는 글이 있을까요?
7달 전
https://nodejs.org/en/guides/event-loop-timers-and-nexttick/
당연히 직접 비동기 코드 작성해서 잘 실행되게끔 하셔야 합니다. 이걸 잘 하느냐가 싱글스레드 자바스크립트 실력입니다.
7달 전
"호출 스택에서 너무 많은 일을 하게 되면 태스크 큐에 쌓인 콜백 함수들이 제 때에 실행되지 않기 때문에 너무 버거운 일들은 하지 않는 게 좋습니다." 에 관련하여 질문있습니다. 제가 js 입문자라 잘 모르는 것일 수도 있지만 해비한 작업을 해야하는 순간이 분명 있을텐데 호출스택에서 이와 같은 이슈로 인해서 무거운 작업을 하는 것이 권장되지 않는다면 어디서 해당 작업을 해주어야할까요?
7달 전
결국에는 호출스택에서 해야 하고요. 코드가 실행되는 곳은 거기밖에 없으니까요. 이벤트루프를 잘 사용해서 무거운 일 사이사이에 가벼운 일들이 실행되게끔 잘 조절하는 수밖에 없습니다.
3년 전
질문이 있습니다. 콜백함수 안에 전역으로 쓰인 변수나 함수가 들어있으면 잘 실행되는데, 콜백함수가 실행되는 때에는 main 함수가 사라진 상태에서 실행되는데 어떻게 전역변수와 함수가 적용되는 건지 궁금합니다. main 함수가 전역 컨텍스트이면 얘가 종료되었을 때는 전역 변수 함수에 접근을 못하지 않나요?
3년 전
실행컨텍스트 강좌를 참고하세요. 스코프체인은 렉시컬스코프로 남아있습니다
3년 전
답변 감사드립니다. 스코프체인은 렉시컬스코프로 남아있습니다고 하셨는데 이게 어디에 남아 있는 것인지가 궁금합니다. 제가 사실 이거에 대한 부분을 삼일째 찾고 있는데 못찾아서 ㅠㅠ 일단 선생님의 함수의 범위(scope) 포스팅에서

"스코프는 함수를 호출할 때가 아니라 선언할 때 생깁니다.
함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 됩니다."

라고 나와 있고, 실행 컨텍스트 포스팅에서는

"일단 처음 코드를 실행(여기서 실행은 브라우저가 스크립트를 로딩해서 실행하는 걸 말합니다)하는 순간 모든 것을 포함하는 전역 컨텍스트가 생깁니다. 모든 것을 관리하는 환경입니다. 페이지가 종료될 때까지 유지됩니다.

* 컨텍스트 생성 시 컨텍스트 안에 변수객체(arguments, variable), scope chain, this가 생성됩니다.
* 컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾습니다.
* 함수 실행이 마무리되면 해당 컨텍스트는 사라집니다.(클로저 제외) 페이지가 종료되면 전역 컨텍스트가 사라집니다.

이제 wow 함수 종료 후 wow 컨텍스트가 사라지고, say 함수의 실행이 마무리됩니다. 따라서 say 컨텍스트도 사라지고, 마지막에 전역 컨텍스트도 사라집니다."

이렇게 나와있어서 스코프체인이 전역 컨텍스트 생성될 때 만들어지고 전역 컨텍스트가 사라질 때 같이 사라지는 것으로 이해했는데, 근데 선생님의 답변인 스코프체인은 렉시컬스코프로 남아있습니다 는 말씀은 스코프체인 이 전역 컨텍스트와는 별개로 따로 저장된다는 말씀이신 것 같습니다. 만약 이게 맞다면 스코프체인이 어디에 생성되고 또 어떤 단계에서 생성되는 건지 궁금합니다 (예를 들면 호이스팅 단계에서 생성). 전역 컨텍스트가 브라우저에서 스크립트 로딩해서 실행하는 것을 말하는데 이때 생성되는게 아니면 언제 생성되는건지.. 혹은 전역 컨텍스트 생성할때 내부에 스코프 체인이 들어가면서 또 다른 곳에 함께 저장되는건지... 도통 모르겠네요 ㅠㅠ 답변해 주시면 너무 감사드리겠습니다!!
3년 전
스코프체인이 언제 생기는지는 전혀 중요하지 않습니다. 자바스크립트의 스코프는 정적 스코프라서 코드를 적는 순간 이미 스코프체인이 고정되어 있습니다. 즉, 아무 때에나 스코프를 체크해도 같은 스코프, 스코프 체인이 나온다는 뜻입니다. 따라서 전역 컨텍스트가 생긴 후에 생성되든, 생기기 전에 생성되든 실행컨텍스트 분석에는 아무런 영향이 없습니다. 언제나 존재한다고 보셔도 됩니다.
4년 전
안녕하세요 글 잘보고 있습니다. 본문에 설명하신 Javascript의 이벤트루프와 Nodejs의 이벤트루프는 같은 이벤트 루프인가요?? Nodejs의 이벤트루프는 libuv에서 제공하는걸로 알고있는데, 그럼 자바스크립트도 결국 내부적으로 libuv에서 제공하는 이벤트루프를 이용하는건가요??
4년 전
같습니다. 자바스크립트 언어에는 이벤트루프가 없습니다. 브라우저가 구현한 것입니다. libuv로 구현되었는지는 브라우저마다 다릅니다.
5년 전
nodejs에서도 호출스택이 존재하는건가요??
5년 전
네 JS면 다 존재합니다.
5년 전
자바스크립트는 싱글스레드라고 알고 있는데 백그라운드가 다른스레드라는게 어떤 의미인가요?
5년 전
자바스크립트 엔진은 c++로 만들어졌습니다. 백그라운드는 C++같은 다른 환경이 담당하는 부분을 말하며 C++에서는 멀티 쓰레드를 활용할 수 있습니다.
5년 전
http://latentflip.com/loupe/ 이 주소가 페이스북으로 연결된 후에 다시 해당 주소로 이동하는 것 같네요.
5년 전
백그라운드가 혹시 힙메모리 영역으로 이해하면 되나요?
5년 전
다른 쓰레드로 이해하시면 됩니다. 물론 이벤트리스너나 타이머를 저장해두는 힙메모리도 사용됩니다.
5년 전
호출스택이 꼭 비워져야만 콜백 큐에서 콜백함수를 퍼올리나요? 예를들어 콜백함수를 기다렸다가 데이터를 받아서 처리하는 그런 함수가 있다면 걔도 같이 콜백 큐로 들어가나요?
5년 전
네 비워져야 올립니다. 콜백함수를 기다린다는게 어떤 건지 모르겠네요. 비동기인 콜백함수를 기다리는 것은 불가능합니다.
6년 전
오~!!!! 정말 이해를 부셔주는 좋은 자료네요!
7년 전
안녕하세요

$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});

setTimeout이 안의 콜백 함수를 백그라운드로 보낸후

Web Apis에
$.on('button', 'click', ...) 은 사라지지 않고 계속 유지해 있어요
2초후 없어져야 하는거 아닌지요??
7년 전
이벤트리스너는 다음 이벤트를 대기해야하기 때문에 사라지지 않습니다.
7년 전
http://latentflip.com/loupe/ 알려주신 사이트에서 봤어요. 그런데

$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});

이 코드가 실행됐을때 setTimeout은 백그라운드에 포함되지 않는건가요?
7년 전
여행다녀와서 답변이 늦었습니다! setTimeout이 안의 콜백 함수를 백그라운드로 보내는 것입니다!
7년 전
질문있습니다! 모든 콜백함수는 콜백큐로 들어가는 것인가요? 아니면 특정 api(setTimeOut, onClick 등) 를 가진 콜백함수만 콜백큐로 들어가는 것인가요?
7년 전
특정 API만 들어갑니다. 이벤트리스너나 비동기API들요.
7년 전
정말 큰 도움 됐습니다!
7년 전
아 settimeout 5초를 해도 5초보다 약간 더걸리는게 그런 이유였네요 감사합니다 이제 이해를 했습니다.
7년 전
쉽게 이해할 수 있었습니다! 정말 감사합니다.
질문있습니다 ^^
위에서 언급하신 백그라운드 => Web Apis, 태스크 큐 => Callback Queue 로 이해하면 될까요?
그리고 그림으로 설명해주선 4번 부분에서, 호출스택이 다 끝나야 태스트 큐의 콜백을 호출 스택으로 올리나요?
저는 태스크 큐가 끝나고 호출스택으로 올려서 처리하지 않나 생각해서, 궁금합니다 ^^
7년 전
얼추 맞습니다. 콜백 큐는 사실 태스크 큐, 마이크로 태스크, 잡 큐 등등 여러 개로 구성되어 있습니다. 호출 스택이 다 끝나서 빈 상태여야 이벤트 루프가 태스크 큐의 콜백을 호출 스택으로 올려 실행합니다.
7년 전
zerocho님 답변 감사합니다!^^ 항상 좋은 자료로 열심히 공부하고 있습니다 :)