1. var와 let 선언자의 스코프
var
var 변수 선언자는 함수 스코프입니다.
- 함수 내부에 선언하면 밖에서 참조할 수 없다
/**
* var를 함수 내부에 선언하면, 외부에서 참조가 불가능하다.
*/
function varTest() {
var globalVar = "전역";
console.log(globalVar); // 전역
}
varTest();
console.log(globalVar); // ReferenceError: globalVar is not defined
- var를 함수가 아닌 if와 for 안에서 사용한다면, 스코프 밖에서 사용이 가능하다.
/**
* var를 함수가 아닌 if와 for 안에서 사용한다면, 스코프 밖에서 사용이 가능하다.
*/
if(true){
var varVar = 1;
}
console.log(varVar);
- var를 함수 외부에 선언하면, 전역 스코프가 돼서 어디서든 참조가 가능하다.
/**
* var를 함수 외부에 선언하면, 전역 스코프가 돼서 어디서든 참조가 가능하다.
*/
var globalVar = "전역";
function varTest() {
console.log(globalVar); // 전역
}
varTest();
console.log(globalVar); // 전역
let
let 변수 선언자는 블록 스코프입니다.
- let은 블록스코프여서, 함수가 아닌 if와 for 안에서 사용한다면, 블록 스코프 밖에서 사용이 불가능합니다.
/**
* let은 블록스코프여서, 함수가 아닌 if와 for 안에서 사용한다면, 블록 스코프 밖에서 사용이 불가능합니다.
*/
if(true){
let letVar = 1;
}
console.log(letVar); //! letVar is not defined
❓ 그렇다면 for문에서 let 선언은 블록 스코프임에도 불구하고 어떻게 이전 값을 계속 참조해서 증감을 일으킬 수 있는 것일까요?
let 선언자는 분명 블록 스코프인데 어떻게 이전 블록값을 참조하며 증감시킬 수 있는 것인지 for 반복문의 원리가 궁금했습니다. Babel 을 통해 let for문을 옛날 버전 var로 구현했을 때 내부적으로 다음과 같이 동작하는 것을 확인할 수 있었습니다.
/* let */
function Log() {
for(let i = 0; i < 3; i++) {
console.log(i);
}
}
Log()
/* let 선언자 for 문 원리 */
function LogMutation() {
function _loop(index:number){
console.log(index);
}
for (var i = 0; i < 3; i++){
_loop(i);
}
}
LogMutation()
`_loop` 함수에 arguments(전달인자)로 들어온 `index` 값이 _loop 함수 안에 갇히므로 이후에 인자로 계속 들어오는 `i` 변수값들의 서로에 대한 의존성을 제거할 수 있으며, LogMutation 함수 내부 스코프를 가지는 var i 는 안전하게 계속 접근하여 증가++시킬 수 있는 것이었습니다.
2. var 선언자와 함수 선언문 function 의 호이스팅 우선순위
var 선언자 우선 > 함수 선언문 이후 > var 선언자 변수 할당
예시 단 하나로 쉽게 직관적으로 이해하고자 합니다.
하나는 함수표현식으로 var 선언자를 사용한 것이고, 나머지 하나는 함수 선언문입니다.
/**
* # 좀 더 직관적인 호이스팅 순서 예시 (var 함수 표현식 vs 함수 선언문)
* ? declareFunction() 에서 에러가 뜨지 않는 이유는?
* var declareFunction 는 맨 위로 호이스팅 된 상태이기 때문이다. 그 다음에 function declareFunction () {console.log('declareFunction1');} 가 호이스팅 되어 있다.
* 그래서 위로 찾아갈 때 가장 가까운 함수 선언문 식을 쓰는 것이다.
*/
declareFunction() // declareFunction1
console.log(declareFunction) // [Function: declareFunction]
var declareFunction = function(){
console.log('declareFunction2');
}
function declareFunction () {
console.log('declareFunction1');
}
declareFunction(); // declareFunction2
호이스팅 과정 중 아래와 같이 record 수집을 가장 먼저 합니다.
function declareFunction이 더 아래에 있군요. 아래 코드와 같은 상황인 것이죠.
그래서 위 코드의 가장 상단의 declareFunction() 에서 declareFunction1 이 로그로 찍힙니다.
var declareFunction;
function declareFunction() {
console.log('declareFunction1');
}
다음 순서는 변수 할당입니다. 코드 상으로는 function declareFunction 이 var declareFunction 보다 아래에 있지만 함수 선언문 수집은 이미 끝났고, var 선언자 변수 할당이 함수 선언문보다 이후에 이루어지므로 최종적으로 아래와 같이 남겠군요.
var declareFunction = function(){
console.log('declareFunction2');
}
declareFunction(); // declareFunction2
3. Event LOOP
📝해당 글의 일부 내용을 전적으로 가져왔습니다.
자바스크립트는 단일 스레드 기반으로 비동기로 동작합니다.
- 자바스크립트는 단일 스레드 기반의 언어로서 한 순간 하나의 작업만을 처리할 수 있습니다.
- 자바스크립트는 비동기로 동작하기때문에 단일 스레드임에도 불구하고 동시에 많은 작업을 수행할 수 있습니다.
그렇지만, 자바스크립트 언어 자체가 비동기 동작을 지원하는 것은 아닙니다.
- 비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저가 가지고 있습니다.(Node 에서는 libuv 라이브러리 등)
- 브라우저는 Web APIs, Event Table, Callback Queue, Event Loop 등으로 구성되며 자바스크립트 코드가 실행될 때 브라우저와의 동작은 아래 그림으로 표현할 수 있습니다.

구성요소
- Heap: 메모리 할당이 발생하는 곳
- Call Stack : 실행된 코드의 환경을 저장하는 자료구조, 함수 호출 시 Call Stack에 push 됩니다. (Call Stack에 대한 자세한 설명은 🔗여기)
- Web APIs: DOM, AJAX, setTimeout 등 브라우저가 제공하는 API
- Callback Queue, Event Loop, Event Table(그림엔 없음) 은 아래에서 설명하겠습니다.
setTimeout(function exec() {
console.log('second')
}, 1000);
위 코드가 실행될 때 각 구성요소들의 역할은 다음과 같습니다.
- Web APIs: setTimeout이 Call Stack에 들어와 실행되면 Browser API인 timer를 호출합니다.
- Event Table: 특정 event(timeout, click, mouse move 등등)가 발생했을 때 어떤 callback 함수가 호출되야 하는지를 알고 있는 자료구조입니다. 위 코드에서 호출된 timer가 종료되면 event가 발생하게 되는데 이때 exec callback 함수가 실행되어야 한다는 것을 Event Table이 알고 있습니다.
- Callback Queue: 이벤트 발생 시 실행해야 할 callback 함수가 Callback Queue에 추가됩니다.
- Event Loop: Event Loop의 역할은 간단합니다.
1. Call Stack과 Callback Queue를 감시합니다.
2. Call Stack이 비어있을 경우, Callback queue에서 함수를 꺼내 Call Stack에 추가 합니다.
webAPIs 로 어떻게 위임이 된다고 이해해야 할까?
전체 개념은 위와 같고, 개인적으로 setTimout이 webAPI로 위임될 때 어떤 식으로 넘어간다고 생각하는 게 좋을까 고민을 했습니다만, 아주 좋은 글에서 그림을 통해 쉽게 익힐 수 있었습니다.
관련 내용을 아래 기록해둡니다.(사진과 단계 설명 방식이었습니다.)
예시 코드는 다음과 같습니다.
console.log('first')
setTimeout(function cb() {
console.log('second')
}, 1000); // 0ms 뒤 실행
console.log('third')
3-1. Callback Queue 이해하기
1.console.log(‘first’)가 Call Stack에 추가(push) 됩니다.

2. console.log(‘first’)가 실행되어 화면에 출력한 뒤, Call Stack에서 제거(pop) 됩니다.

3.setTimeout(function cb() {..}) 이 Call Stack에 추가됩니다.

4. setTimeout 함수가 실행되면서 Browser가 제공하는 timer Web API 를 호출합니다. 그 후 Call Stack에서 제거됩니다.

5. console.log(‘third’)가 Call Stack에 추가됩니다.

6. console.log(‘third’)가 실행되어 화면에 출력되고 Call Stack에서 제거됩니다.

7. setTimeout 함수에 전달한 0ms 시간이 지난뒤 Callback으로 전달한 cb 함수가 Callback Queue에 추가됩니다.

8. Event Loop는 Call Stack이 비어있는 것을 확인하고 Callback Queue를 살펴봅니다. cb를 발견한 Event Loop는 Call Stack에 cb를 추가합니다.

9. cb 함수가 실행 되고 내부의 console.log(‘second’)가 Call Stack에 추가됩니다.

10. console.log(‘second’)가 화면에 출력되고 Call Stack에서 제거됩니다.

11. cb가 Call Stack에서 제거됩니다.

setTimeout 함수가 실행되면서 Browser가 제공하는 timer Web API 를 호출하면서 call stack 에서 제거된다는 점과 Call Stack이 비어있을 경우, Callback queue에서 함수를 꺼내 Call Stack에 추가 한다는 점을 기억하면 될 것 같습니다.
3-2. ES6 Job Queue (= microtask queue) 이해하기

ES6/ES2015 에서 소개된 Job Queue는 Callback Queue와 다른 Queue이며 Promise를 사용할 경우 Job Queue를 사용하게 됩니다. promise를 사용할 때 callback 함수 역할을 하는 후속 처리 메서드 `.then` 을 사용하게 되며, 이런 thenable한 함수들은 Job Queue에 추가됩니다.
Job Queue와 Callback Queue는 우선순위가 다릅니다.
Job Queue가 Callback Queue 보다 우선순위가 높습니다.
예시 코드입니다.
console.log('fisrt');
setTimeout(function() {
console.log('setTimeout - second');
}, 0);
var promise = new Promise(function(resolve, reject) {
resolve();
});
promise.then(function(resolve) {
console.log('promise - third');
})
.then(function(resolve) {
console.log('promise - four');
});
console.log('five');
코드의 결과입니다.
fisrt
five
promise - third
promise - four
setTimeout - second
Job Queue(Microtask Queue)의 우선순위가 Callback Queue(Macrotask queue)보다 높습니다. 따라서 Event Loop는 Call Stack이 비어있을 경우, Job Queue에서 기다리는 모든 작업을 처리하고 Callback Queue로 이동하게 됩니다.
4. 종합문제
위에서 다룬 모든 개념을 총 동원하여 문제를 딱 하나만 풀어봅시다.
다음 문제의 로그로 찍히는 숫자의 순서를 맞춰보세요.
for (let i = 0; i < 3; i++) {
const print1 = () => {
console.log(i);
};
console.log(i);
setTimeout(print1, 100); // 100ms timeout이 지정되어 있습니다.
}
for (var i = 0; i < 3; i++) {
const print2 = () => {
console.log(i);
};
console.log(i);
setTimeout(print2);
}
console.log(i);
i -= 1;
console.log(i);
정답 및 해설
`정답`
0
1
2
0
1
2
3
2
2
2
2
0
1
2
`해설`
/**
* 실행 컨텍스트가 실행되기 이전에, 컨텍스트 내부 전체를 처음부터 끝까지 순서대로 식별자들을 수집하는 이 과정을 호이스팅 이라고 합니다.
* # 첫 번째 for 루프 (let 사용)
* 첫 번째 for 루프에서는 `let i`를 사용하여 변수 `i`를 선언했습니다.
* ### `let`은 블록 스코프 <중괄호 {}>를 갖는 변수를 선언할 때 사용됩니다. for 문에서의 let 은 특별하게 동작합니다.
* print1 함수는 for 루프의 블록 스코프 내에서 실행되며, 클로저로서 자신이 정의된 블록 스코프 내에서의 i 값을 기억합니다.
* 그래서 setTimeout이 호출될 때마다 해당 print1 함수는 자신이 정의된 블록 스코프에서의 i 값을 출력하게 됩니다.
* 이로 인해 예상대로 0, 1, 2가 각각 출력됩니다.
*/
for (let i = 0; i < 3; i++) {
const print1 = () => {
console.log(i);
};
console.log(i);
// 0
// 1
// 2
setTimeout(print1, 100); // * setTimeout 함수가 실행되면서 브라우저가 제공하는 timer Web API 함수는 timeout 시간에 따라서 콜백 queue 에서의 우선순위를 가집니다.
// 0
// 1
// 2
}
/**
* # 두 번째 for 루프 (var 사용)
* for 루프에서는 var i를 사용하여 변수 i를 선언했습니다.
* ### var는 함수 스코프를 갖는 변수를 선언할 때 사용되며, 해당 변수의 값은 함수 내부에서 공유됩니다.
* 따라서 각각의 print2 함수는 같은 스코프 내에서 정의되었고, 그 스코프의 i 값은 for 루프가 종료된 후에도 유지됩니다.
* 따라서 setTimeout에 의해 print2 함수가 호출될 때에는 이미 for 루프가 종료되었고 i 값은 3이 되어 있습니다.
* 이 상태로 전역변수가 된 i 는 -1 이 되어 2가 됩니다.
* 그래서 var 선언자 for loop에서는 2가 세 번 출력되게 됩니다.
*/
for (var i = 0; i < 3; i++) {
const print2 = () => {
console.log(i);
};
console.log(i);
// 0
// 1
// 2
setTimeout(print2); // * 최종적으로 i++ 된 값인 3은 더이상 반복문을 실행시키지 않음. eventLoop에 위임했던 setTimeout이 한꺼번에 콘솔을 찍음.
// 2
// 2
// 2
}
console.log(i); // 3
i -= 1;
console.log(i); // 2
// ### 결과: 0 1 2 0 1 2 3 2 2 2 2 0 1 2
// 위 코드에서 let for문은 이런 식으로 변환될 수 있겠네요.
for (var i = 0; i < 3; i++) {
function _loop(i: number) {
console.log(i);
const log = () => {
console.log(i);
};
setTimeout(log, 100);
}
_loop(i);
}
console.log(i) // 3
// 0
// 1
// 2
// 3
// 0
// 1
// 2