JavaScript Deep Dive 스터디 노트 (22~23장)
내다버린 2주.

안냐심까 저번주엔.. 어른이?날 이슈로 좀 쉬었더니...
이번주 숙제를 하려니 귀찮아져서 급하게 우다다다닥 인사올림돠...
이번 주제는 어떻게 보면 중요하다고 볼 수 있는 this...랑 실행 컨텍스트인데
과거 딥다이브 파면서 메서드 안에서 this도 봤고, 따로 면접 준비하면서 정리했던 노트도 있긴 하지만
근데 그게 전부였슴다. 쓸 줄만 알고 왜 그렇게 동작하는지는 모른다는거죠.
(쓸 줄 아는거? this는 함수를 호출한 객체를 말한다...라는거?)
화살표 함수 관련 this 설명도 마찬가지로 면접용으로 달달 외웠으니
"왜?"라는 내용과 연결은 안되어있...고요 하하;
심지어 실행 컨텍스트는 이름만 들어본거 같은데.. 큰났네요
이거 this랑 연결된다는데... 이번주도 호기심 해결의 딥다이브 콘텐츠가 되겠군요.
바로 시작해보도록 하시져. 레스고.
1. this가 뭔데?
그래(게) 바로 "너"

카페에서 바리스타가 일하고 있다고 가정하자.
그 바리스타가 "그 그라인더" 라고 말하면,
어떤 그라인더를 말하는지는 그 바리스타가 어느 카페에서 어떤 상황에 있느냐에 따라 달라진다.
- 에소바에서 일하면 → 에스프레소 그라인더
- 브루잉 바에서 일하면 → 핸드드립용 그라인더
- 그라인더가 여러 대 있는 카페라면 → 그라인더를 지칭하는 것이 더...많아지겠지?
this도 똑같다. 영어 원문 그대로 본다면 "이거" "이것" 정도의 너낌인데,
자바스크립트에서는 누가 호출했는지(함수가 어떻게 호출되었는지)에 따라 "이거"가 달라진다고 보면 된다.
(말장난하는거랑 다를게 없는거 아닌가? 읍읍)
const grinder = {
name: "Ditting",
greet() {
console.log(this.name); // "Ditting"의 name
}
}
grinder.greet(); // "Ditting"
여기서 greet()을 grinder 가 호출했으니까, (= grinder의 객체를 바라봄)
this = Ditting(= 나를 호출한 애) 라고 보면 된다.
this === grinder
this.name === "Ditting"
그런데 여기서 반전이 있다.
JS의 this는 어디에 작성되어 있는지가 아니라 함수를 어떻게 호출하느냐에 따라 매번 달라진다는거.
즉, `this`는 항상 “내가 속한 객체”처럼 고정되어 있지 않다. 이게 this가 JS에서 헷갈리는 근본적인 이유 중 하나라고 한다.
추가적으로 공식 ECMAScript 스펙을 보면, 이 차이를 설명하는 힌트가 나온다.
A Function Environment Record is a Declarative Environment Record that is used to represent the top-level scope of a function and, if the function is not an ArrowFunction, provides a this binding.
(Function Environment Record는 함수의 최상위 스코프를 나타내는 데 사용되는 Declarative Environment Record이며, 함수가 ArrowFunction이 아닌 경우 `this` 바인딩을 제공한다.)
— ECMAScript Living Standard 9.1.1.3, Function Environment Records
여기서 지금 당장 전부 이해할 필요는 없다. 일단 핵심만 잡고 가자.
화살표 함수가 아닌 함수는 `this` 바인딩을 제공한다.
그럼 자연스럽게 이런 질문이 생긴다.
- 그 `this` 바인딩은 어디에 저장되는 걸까?
- 호출 방식에 따라 `this` 값은 어떻게 달라지는 걸까?
- 화살표 함수는 왜 자기 `this`가 없다고 하는 걸까?
이 질문들은 뒤쪽의 “실행 컨텍스트와 this의 연결고리”에서 다시 회수할 예정이다.
지금은 일단 이것만 기억하자. `this`는 함수가 어디에 작성되었는지가 아니라, 함수가 어떻게 호출되었는지에 따라 달라진다.
2. this 바인딩이 결정되는 4가지 순간
같은 함수라도 호출 방식에 따라 this가 완전히 달라진다.
대표적으로 아래 4가지 경우가 있다.
- 일반 함수 호출
- 메서드 호출
- 생성자 함수 호출
- call / apply / bind 호출
1️⃣ 그냥 함수로 호출 → this = window (전역 객체)
브라우저의 non-strict 환경에서 일반 함수를 그냥 호출하면 `this`는 전역 객체를 가리킨다.
function grind() {
console.log(this);
}
grind(); // window (브라우저 non-strict 기준)
아무 객체도 점 앞에서 이 함수를 호출하지 않았고, 그냥 독립 함수로 호출된 것이다.
grind();
"주인" 없이 혼자 호출됐으니, `this`가 전역 객체인 `window`를 가리킨다.
다만 이 기준은 브라우저의 non-strict 환경 기준이다.
strict mode에서는 결과가 달라진다.
function grind() {
console.log(this);
}
grind(); // undefined
strict mode에서는`this`가 자동으로 전역 객체에 연결되지 않고 undefined가 된다.
이 차이는 스펙의 `[[thisMode]]`라는 내부 슬롯과 연결된다.
ECMAScript 스펙에서는 `[[thisMode]]`를 이렇게 설명한다.

"lexical means that this refers to the this value of a lexically enclosing function. strict means that the this value is used exactly as provided by an invocation of the function. global means that a this value of undefined is interpreted as a reference to the global object."
(lexical은 this가 렉시컬로 둘러싸는 함수의 this 값을 참조한다는 뜻이다. strict는 this 값이 함수 호출에 의해 제공된 그대로 사용된다는 뜻이다. global은 undefined인 this 값이 전역 객체에 대한 참조로 해석된다는 뜻이다.)
— ECMAScript 10.2, ECMAScript Function Objects [Table 25]
그래서 일반 함수 호출에서 `this`가 명시적으로 정해지지 않았을 때,
non-strict 환경에서는 전역 객체로 이어질 수 있고, strict mode에서는 undefined가 된다.
다만 `[[thisMode]]`가 실제 함수 실행 컨텍스트와 어떻게 연결되는지는 뒤쪽에서 다시 보겠다.
여기서는 strict mode가 단순히 “문법 빡세게 검사하는 모드”가 아니라
`this`가 전역 객체로 조용히 흘러가는 문제도 막아준다는 정도로 잡고 넘어가자.
+) 근데 "의미없다"고 넘기면 안 되는데.. 이것이 버그의 출처가 되기 때문이다.
// non-strict — this가 window로 날아감
function setName(name) {
this.name = name; // window.name = name 이 되어버림 😱
}
setName("Ditting");
console.log(window.name); // "Ditting" — 전역 오염!
// strict — 적어도 에러로 잡힌다
"use strict";
function setName(name) {
this.name = name; // TypeError: Cannot set properties of undefined
}
setName("Ditting"); // 💥 바로 터짐 → 디버깅하기 훨씬 쉬움
결국 JS의 "에러를 삼키는 언어" 특성이 this에서도 나타나는 예시로 볼 수 있다.
non-strict에서는 조용히 전역 객체를 오염시키고, strict에서는 TypeError로 즉시 알려준다.
이럴때는 에러가 나는 게 오히려 낫다. 조용히 망가지는 것보다 시끄럽게 터지는 게 찾기 쉬우니까.
2️⃣ 객체의 메서드로 호출 → this = 그 객체
객체의 메서드로 호출하면 `this`는 점 앞의 객체를 가리킨다.
const grinder = {
name: "Ditting",
greet() {
console.log(this.name);
}
}
grinder.greet(); // "Ditting"
여기서 중요한 규칙은 이것이다. 점 앞에 있는 객체가 `this`다.
grinder.greet();
점 앞에 `grinder`가 있으니까 `this`는 `grinder`다.
그런데 this는 "메서드를 소유한 객체"가 아니라 "메서드를 호출한 객체"를 기준으로 결정되니 주의하도록 하자.
const grinder = {
name: "Ditting",
greet() {
console.log(this.name);
}
};
const anotherGrinder = {
name: "EK43",
};
anotherGrinder.greet = grinder.greet;
anotherGrinder.greet(); // "EK43"
`greet` 함수는 원래 `grinder` 객체 안에 작성되어 있었다.
그런데 `anotherGrinder.greet()`로 호출하는 순간, 점 앞의 객체는 `anotherGrinder`가 된다.
그래서 `this.name`은 `"EK43"`을 출력한다. 같은 함수인데도 `this`가 달라졌다.
함수가 어디에 “소속”되어 있는지가 아니라, 어떻게 “호출”되었는지가 중요하다.
3️⃣ new로 호출 → this = 새로 만들어질 인스턴스
함수를 `new`와 함께 호출하면 `this`는 새로 만들어질 인스턴스를 가리킨다.
function Coffee(name) {
this.name = name; // 새로 만들어질 객체에 name 할당
}
const latte = new Coffee("latte");
console.log(latte.name); // "latte"
`new Coffee("latte")`가 호출되면 내부적으로 새 객체가 만들어지고, 그 객체가 `this`에 바인딩된다.
대략적인 느낌은 이렇다.
1. 빈 객체 생성
2. this가 그 빈 객체를 가리킴
3. 함수 본문 실행
4. this.name = "latte" 실행
5. 완성된 객체 반환
그래서 생성자 함수 안에서 `this.name = name`을 하면 새로 생성되는 객체에 프로퍼티가 추가된다.
function Coffee(name) {
this.name = name;
}
여기서 `this`는 아직 완성되기 전의 새 인스턴스라고 보면 된다.
붕어빵 틀이 붕어빵을 찍어내는 중이라고 생각하면 편하다.
4️⃣ call / apply / bind → this = 내가 직접 지정한 객체
`call`, `apply`, `bind`를 쓰면 `this`를 직접 지정할 수 있다.
(간단하게.. this를 내 맘대로 고정시킬 수 하는 방법이랄까..)
function greet() {
console.log(this.name);
}
const cafe = { name: "코히" };
greet.call(cafe); // "코히" — 즉시 호출, 인수는 쉼표로
greet.apply(cafe); // "코히" — 즉시 호출, 인수는 배열로
const boundGreet = greet.bind(cafe);
boundGreet();// "코히" — 새 함수 반환, 나중에 호출
셋의 차이는 아래와 같다.
call— 즉시 호출, 인수를 쉼표로 전달apply— 즉시 호출, 인수를 배열로 전달bind— 함수를 호출하지 않고 this가 고정된 새 함수를 반환
예를 들어 인수를 같이 넘기면 차이가 더 잘 보인다.
function introduce(menu, price) {
console.log(`${this.name}의 ${menu}는 ${price}원입니다.`);
}
const cafe = {
name: "코히",
};
introduce.call(cafe, "라떼", 5000); // "코히의 라떼는 5000원입니다."
introduce.apply(cafe, ["라떼", 5000]); // "코히의 라떼는 5000원입니다."
const boundIntroduce = introduce.bind(cafe);
boundIntroduce("라떼", 5000); // "코히의 라떼는 5000원입니다."
3. 콜백에서 this가 튀는 문제
실제로 `this` 때문에 자주 만나는 문제가 콜백이다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(function () {
console.log(this.name); // 기대: "Ditting"
}, 100);
},
};
grinder.grind();
const grinder = {
name: "Ditting",
grind() {
setTimeout(function() {
console.log(this.name); // 😱 undefined
}, 100)
}
}
grinder.grind();
겉으로 보면 `grind()`는 `grinder`가 호출했다.
그러면 `this.name`이 `"Ditting"`일 것 같지만, 그렇지 않다.
`setTimeout` 안에 전달된 일반 함수는 `grinder`가 메서드로 호출한 게 아니다. 타이머에 의해 나중에 별도의 함수로 호출된다.
즉, 바깥의 `grind()`에서 `this`가 `grinder`였다고 해서, 안쪽 콜백 함수의 `this`까지 자동으로 이어지지는 않는다.
브라우저 non-strict 환경에서는 이 콜백 함수의 `this`가 전역 객체 쪽으로 잡힐 수 있고,
strict mode나 실행 환경에 따라 `undefined`가 될 수도 있다.
중요한 건 이것이다. 콜백 함수는 원래 메서드 호출의 `this`를 자동으로 물려받지 않는다.
그래서 아래처럼 `this`가 예상과 다르게 튀는 문제가 생긴다.
4. 콜백 this 문제 해결책
해결 방법은 대표적으로 세 가지가 있다.
- that = this 패턴
- bind
- 화살표 함수
// 1. that = this 패턴 — 옛날 방식
grind() {
const that = this; // grinder 저장해두기
setTimeout(function() {
console.log(that.name); // "Ditting" ✅
}, 100)
}
// 2. bind
grind() {
setTimeout(function() {
console.log(this.name); // "Ditting" ✅
}.bind(this), 100) // this(=grinder)를 명시적으로 고정
}
// 3. 화살표 함수 — 제일 깔끔
grind() {
setTimeout(() => {
console.log(this.name); // "Ditting" ✅
}, 100)
}
1️⃣ that = this 패턴
옛날 방식이다.
바깥 함수의 `this`를 변수에 저장해두고, 콜백 안에서 그 변수를 사용한다.
const grinder = {
name: "Ditting",
grind() {
const that = this;
setTimeout(function () {
console.log(that.name);
}, 100);
},
};
grinder.grind(); // "Ditting"
`grind()`가 메서드로 호출되었기 때문에 `grind` 안의 `this`는 `grinder`다.
그 값을 `that`에 저장해두면, 콜백 안에서도 `that`을 통해 `grinder`를 참조할 수 있다.
this = grinder
that = this
that = grinder
그래서 콜백 함수의 `this`가 달라져도 `that.name`은 안전하게 `"Ditting"`을 출력한다.
2️⃣ bind 사용
`bind`를 사용하면 함수의 `this`를 명시적으로 고정할 수 있다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(
function () {
console.log(this.name);
}.bind(this),
100
);
},
};
grinder.grind(); // "Ditting"
여기서 `bind(this)`의 `this`는 `grind()` 안의 `this`다. 즉, `grinder`다.
grind() 안의 this = grinder
bind(this) = bind(grinder)
콜백 함수의 this를 grinder로 고정
그래서 콜백이 나중에 호출되어도 `this`는 `grinder`로 유지된다.
3️⃣ 화살표 함수 사용
가장 깔끔한 방식은 화살표 함수다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(() => {
console.log(this.name);
}, 100);
},
};
grinder.grind(); // "Ditting"
화살표 함수는 자기만의 `this` 바인딩을 만들지 않는다.
그래서 콜백 함수 내부에서 `this`를 새로 결정하지 않고, 바깥 함수인 `grind()`의 `this`를 사용한다.
여기서 `grind()`는 `grinder.grind()`로 호출되었으므로 `this`는 `grinder`다.
결국 화살표 함수 안의 `this.name`은 `"Ditting"`을 출력한다.
5. 화살표 함수가 왜 this가 안튀나욤?
화살표 함수에 대해 설명하라고 하면 보통 이렇게 말했던 거 같다.
화살표 함수는 상위 스코프의 `this`를 가진다.
나도 면접 준비할 때는 거의 이렇게 외웠던 것 같다. 실제로 관련 글을 찾아봐도 대부분 비슷하게 설명한다.
그래서 처음에는 “아, 화살표 함수는 상위 스코프의 `this`를 가져오는구나” 정도로 이해하고 넘어갔다.
그런데 설명만으로 애매한 순간이 생겼다.
const grinder = {
name: "Ditting",
grind: () => {
console.log(this.name);
},
};
grinder.grind(); // 기대: "Ditting" ...인데?
grinder.grind()로 호출했으니까 this가 grinder일 것 같은데, 화살표 함수에서는 그렇게 동작하지 않는다.
여기서 “상위 스코프의 this를 가진다”는 말이 조금 애매해진다.
객체 안에 작성했으니까 상위가 grinder인가? 싶지만, 객체 리터럴 {} 자체는 새로운 this 바인딩을 만드는 스코프가 아니다.
그러니까 더 정확히는 이렇게 봐야 한다.
화살표 함수는 자기만의 this 바인딩을 만들지 않는다. 일반 함수는 호출될 때 자기 this 바인딩을 만든다.
반면 화살표 함수는 만들지 않는다.
그래서 this를 만나면, 자기 안에서 새로 결정하지 않고 this 바인딩을 제공하는 바깥 환경을 찾아간다.
ECMAScript 스펙 관점에서 보면 화살표 함수의 `[[thisMode]]`는 `"lexical"`이다.
즉, 화살표 함수는 호출될 때 자기만의 `this` 바인딩을 새로 만들지 않고, 자신을 감싸는 바깥 환경의 `this` 바인딩을 사용한다.
쉽게 말하면 태어날 때부터 “자체 `this`는 새로 안 만들고요, 바깥에서 찾아 쓰겠습니다.” 하는 애라고 보면 된다.
그래서 아래 코드는 기대대로 동작한다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(() => {
console.log(this.name);
}, 100);
},
};
grinder.grind(); // "Ditting"
이 코드는 아까 객체 메서드에 화살표 함수를 바로 넣은 예시와 다르다.
const grinder = {
name: "Ditting",
grind: () => {
console.log(this.name);
},
};
위 코드는 grind 자체가 화살표 함수다.
그래서 grind는 자기 this를 만들지 않고, 객체 리터럴 바깥의 this를 찾아간다.
반면 아래 코드는 grind는 일반 메서드이고, 그 안의 콜백만 화살표 함수다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(() => {
console.log(this.name);
}, 100);
},
};
여기서는 grinder.grind()로 호출되면서 grind의 this가 먼저 grinder로 결정된다.
그리고 안쪽 화살표 함수는 자기 this를 새로 만들지 않기 때문에, 바깥 grind의 this를 사용한다.
흐름으로 보면 이렇다.
객체 메서드 자체를 화살표 함수로 만들면
→ 자기 this 없음
→ 객체가 this가 되지 않음
→ 바깥 this를 찾아감
일반 메서드 안에서 콜백을 화살표 함수로 만들면
→ 바깥 일반 메서드의 this를 그대로 사용
→ 콜백에서 this가 튀지 않음
그래서 “화살표 함수는 상위 스코프의 this를 가진다”는 말은 결과를 빠르게 설명하는 표현이고,
“화살표 함수는 자기 this 바인딩이 없어서, this 바인딩을 제공하는 바깥 환경을 찾아간다”는 말은 동작 원리에 가까운 표현이다.
여기서 더 깊게 들어가면 스펙의 GetThisEnvironment()라는 추상 연산이 등장한다.
이름 그대로 현재 this 바인딩을 제공하는 Environment Record를 찾는 과정인데,
이건 뒤쪽에서 실행 컨텍스트와 함께 다시 보도록 하겠다.
일단 지금은 여기까지만 잡고 가자.
화살표 함수는 자기 this를 만들지 않는다.
그래서 객체 메서드 자체로 쓰면 위험할 수 있고,
일반 메서드 안의 콜백에서는 바깥 this를 유지하는 데 유용하다.
6. 실행 컨텍스트가 뭐예요?
13~15장에서 스코프를 보면서 렉시컬 환경, 환경 레코드, 외부 렉시컬 환경 참조를 이미 한 번 봤다.
그때의 핵심 질문은 이것이었다. JS 엔진은 식별자를 어디서 찾는가?
이번에는 질문을 조금 바꿔보자.
JS 엔진은 실행 중인 코드를 어떻게 추적하고, 함수가 호출될 때 `this`를 어떻게 결정하는가?
이 질문에 답하려면 실행 컨텍스트를 봐야 한다.
컨텍스트에 대해서 간단하게 설명을 해보도록 하겠다.. (설명은 접은글 참조)
간단한 설명인데 너무 길어져서 여기다가 넣어보도록 하겠다..
카페 오픈 전, 바리스타가 출근하면 머신이 가동이 되지 않은 상태이므로 바로 커피를 뽑지는 않는다.. (진짭니다.)
대신 먼저 오늘 카페를 어떻게 운영할지 준비한다. (물론 이건 매니저급이지만요 읍읍)
- 오늘 어떤 메뉴를 만들 수 있는지
- 재료는 어디에 있는지
- 주문이 들어오면 어떤 순서로 처리할지
- 지금 일하는 바리스타가 누구인지
이렇게 커피를 만들기 전에 갖춰지는 “준비된 작업 환경”이 필요하다.
자바스크립트도 비슷하다.
코드를 실행하기 전에 JS 엔진은 먼저 실행에 필요한 정보를 정리한다.
- 어떤 변수가 있는지
- 어떤 함수가 있는지
- 식별자를 어디서 찾아야 하는지
- 현재 코드가 어떤 스코프 안에서 실행되는지
- 함수라면this가 무엇인지
이렇게 코드 실행을 추적하기 위해 만들어지는 추상적인 실행 환경을실행 컨텍스트(Execution Context)라고 한다.
ECMAScript 스펙에서는 실행 컨텍스트를 이렇게 설명한다.

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.
(실행 컨텍스트는 ECMAScript 구현체가 코드의 런타임 평가를 추적하는 데 사용되는 명세 도구다.)
— ECMAScript 9.4.,Execution Contexts
즉, 실행 컨텍스트는 ECMAScript 구현체가 코드의 런타임 평가를 추적하기 위해 사용하는 명세상의 장치다.
여기서 중요한 포인트가 있다.
실행 컨텍스트는 우리가 코드로 직접 접근할 수 있는 실제 객체가 아니다.
console.log(ExecutionContext); // 이런 식으로 접근 불가능
실행 컨텍스트는 “엔진이 코드를 이런 방식으로 실행한다고 설명하기 위한 스펙상의 모델”에 가깝다.
그래서 실행 컨텍스트를 이해한다는 건 결국 아래와 같은 내용을 이해하는 것과 같다.
- 지금 어떤 코드가 실행 중인지
- 함수 호출이 어떤 순서로 쌓이고 사라지는지
- 변수와 함수 선언이 언제 준비되는지
- 식별자를 어떤 환경에서 찾는지
- 함수 호출 시 `this`가 어떻게 바인딩되는지
즉, 실행 컨텍스트는 호이스팅, 콜 스택, 스코프 체인, `this` 바인딩이 만나는 중심 지점이라고 볼 수 있다.
7. 실행 컨텍스트 스택 — 누구인가?

실행 컨텍스트는 하나만 덜렁 존재하는 게 아니다.
전역 코드가 평가되면 먼저 전역 실행 컨텍스트가 만들어지고,
함수가 호출될 때마다 새로운 함수 실행 컨텍스트가 위에 하나씩 층층이 쌓이게 된다.
이렇게 만들어진 실행 컨텍스트들은 자료구조 중 하나인 스택(Stack) 구조로 관리되며,
지금 실행 중인 코드는 항상 스택 맨 위에 있는 실행 컨텍스트가 된다.
스펙에서도 이 부분을 아래와 같이 설명하고 있다.
The running execution context is always the top element of this stack.
(현재 실행 중인 실행 컨텍스트는 항상 실행 컨텍스트 스택의 맨 위에 있다.)
— ECMAScript 9.4.,Execution Contexts
예를 들어보자.
function brew() {
function grind() {
console.log("갉갉");
}
grind();
}
brew();
실행 흐름은 이렇게 볼 수 있다.
1. 전역 코드 실행
→ 전역 실행 컨텍스트 push
2. brew() 호출
→ brew 실행 컨텍스트 push
3. grind() 호출
→ grind 실행 컨텍스트 push
4. grind() 종료
→ grind 실행 컨텍스트 pop
5. brew() 종료
→ brew 실행 컨텍스트 pop
6. 전역 코드 종료
→ 전역 실행 컨텍스트 pop
블록쌓기로 생각하면 더 쉽다.
전역 실행 컨텍스트는 제일 먼저 만들어져서 맨 아래 블록이 된다.
그 상태에서 brew()가 호출되면 brew 실행 컨텍스트 블록이 그 위에 쌓이고,
grind()가 호출되면 grind 실행 컨텍스트 블록이 또 그 위에 쌓인다.
[grind 실행 중]
┌────────────────────────┐
│ grind 실행 컨텍스트 │ ← 지금 실행 중인 블록
├────────────────────────┤
│ brew 실행 컨텍스트 │
├────────────────────────┤
│ 전역 실행 컨텍스트 │ ← 제일 먼저 깔린 블록
└────────────────────────┘
스택의 맨 위에 있는 실행 컨텍스트가 바로 지금 코드 실행권을 쥐고 있는 애다.
그리고 함수 실행이 끝나면 위에 있는 블록부터 하나씩 빠진다.
grind 종료 → grind 실행 컨텍스트 pop
brew 종료 → brew 실행 컨텍스트 pop
전역 종료 → 전역 실행 컨텍스트 pop
이 구조가 우리가 개발자 도구에서 보는 콜 스택(Call Stack)과 연결된다.
그래서 실행 컨텍스트를 이해하면 단순히 “아 실행 컨텍스트라는 게 있구나~” 하고
개념어 하나 외우는 데서 끝나지 않는다.
에러가 났을 때 개발자 도구에 찍히는 호출 순서를 보고,
“아, 이 함수가 저 함수를 불렀고, 그 안에서 여기까지 들어갔다가 터졌구나.” 하는 흐름을 읽을 수 있게 된다.
예를 들어 이런 코드가 있다고 해보자.
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error("터졌다!");
}
a();
a()를 호출하면 내부에서 b()가 호출되고, b() 안에서 다시 c()가 호출된다.
그리고 실제 에러는 c() 안에서 발생한다.
이때 에러 스택은 대략 이런 식으로 찍힌다.
Error: 터졌다!
at c
at b
at a
이걸 보면 맨 위에 있는 c가 실제로 에러가 발생한 위치다.
그 아래의 b, a는 거기까지 오기 위해 거쳐온 호출 경로라고 보면 된다.
a 호출
→ b 호출
→ c 호출
→ c에서 에러 발생
즉, 콜 스택은 “지금 어디까지 함수 호출이 쌓여 있었는지”를 보여주는 흔적이다.
처음엔 그냥 빨간 에러 로그처럼 보이지만, 알고 보면 꽤 친절하다.
“어디서 터졌고, 어떤 함수들을 거쳐서 거기까지 갔는지”를 순서대로 알려주고 있기 때문이다.
8. 평가와 실행 — 다시 등장한 호이스팅
실행 컨텍스트 이야기를 하다 보면, 결국 호이스팅이 다시 나온다.
왜냐하면 실행 컨텍스트는 그냥 “함수 호출할 때 쌓이는 애”에서 끝나는 게 아니라,
코드를 실행하기 전에 필요한 준비까지 같이 설명해주는 개념이기 때문이다.
자바스크립트 코드는 크게 보면 두 단계로 처리된다고 이해할 수 있다.
1. 평가 단계
→ 실행 컨텍스트 생성
→ 변수와 함수 선언 등록
→ 렉시컬 환경 구성
2. 실행 단계
→ 코드를 위에서 아래로 실행
→ 값 할당
→ 함수 호출
그래서 아래 코드에서 `undefined`가 나온다.
console.log(x); // undefined
var x = 1;
처음 보면 `var x`가 위로 끌려 올라간 것처럼 보인다.
근데 실제로 코드가 물리적으로 위로 이동한 건 아니다.
평가 단계에서 `var x`라는 선언이 먼저 등록되고, `undefined`로 초기화되었기 때문에
실행 단계에서 `console.log(x)`가 `undefined`를 읽는 것이다.
흐름으로 보면 이렇다.
[평가 단계]
var x 등록
→ undefined로 초기화
[실행 단계]
console.log(x)
→ undefined 출력
x = 1
→ 값 할당
이 내용은 이미 이전에 봤던 호이스팅과 TDZ 이야기와 연결되므로
`var`, `let`, `const` 차이를 또 길게 파지는 않겠다.
이번 글에서 중요한 건 하나다. 실행 컨텍스트는 코드 실행 전에 필요한 선언과 환경 정보를 먼저 준비한다.
그래서 호이스팅은 “코드가 위로 올라가는 마법”이라기보다는,
실행 전에 선언 정보가 먼저 준비되기 때문에 생기는 현상으로 보는 게 더 정확하다.
9. 실행 컨텍스트 해부?해보기
그럼 실행 컨텍스트 안에는 대체 뭐가 들어있을까?
스펙을 보면 실행 컨텍스트는 그냥 “코드 실행하는 공간” 정도로 끝나는 게 아니라,
실행을 추적하기 위한 여러 상태 컴포넌트를 가지고 있다.
먼저 모든 실행 컨텍스트가 공통으로 가지는 컴포넌트는 아래와 같다
스펙 표를 보면 모든 실행 컨텍스트는 대략 이런 정보를 가진다.
| 컴포넌트 | 역할 |
| code evaluation state | 코드 평가를 수행하거나, 중단하거나, 다시 이어서 실행하기 위해 필요한 상태 |
| Function | 지금 함수 코드를 평가 중이라면 그 함수 객체, Script나 Module이면 `null` |
| Realm | 코드가 접근하는 ECMAScript 리소스의 영역 |
| ScriptOrModule | 이 코드가 어떤 Script 또는 Module에서 왔는지 |
여기서 벌써 약간 머리가 아파왔다.
`Realm`이요?...? `ScriptOrModule`이요?...?
갑자기 문 열고 낯선 사람들이 우르르 들어온 느낌인데, 이번 글에서 얘네를 전부 깊게 파기엔 아직 준비가 되지 않아....
여기서는 “실행 컨텍스트가 코드 실행을 추적하기 위해 여러 상태를 들고 있다” 정도만 알고가자.
(미안... 너희는 내가 성장해서 파주도록 하지.)
그리고 ECMAScript 코드 실행 컨텍스트는 여기에 추가로 아래 컴포넌트를 더 가진다.
여기서 다시 `LexicalEnvironment`, `VariableEnvironment`, `PrivateEnvironment`가 등장한다.
스펙에 따르면 ECMAScript 코드 실행 컨텍스트는 이런 추가 상태 컴포넌트를 가진다.
| 컴포넌트 | 역할 |
| LexicalEnvironment | 코드 안에서 식별자 참조를 해결할 때 사용하는 Environment Record를 식별 |
| VariableEnvironment | var 선언으로 만들어진 바인딩을 담는 Environment Record를 식별 |
| PrivateEnvironment | 클래스의 private name을 담는 PrivateEnvironment Record를 식별 |
이 중에서 우리가 스코프, 호이스팅, 클로저를 이해할 때 가장 많이 보게 되는 게 LexicalEnvironment다.
13~15장에서 이미 봤듯이, 렉시컬 환경은 대략 이런 구조였다.
Lexical Environment
├─ Environment Record
│ └─ 현재 스코프의 식별자 저장
└─ Outer Lexical Environment Reference
└─ 바깥 스코프를 가리키는 참조
Environment Record는 현재 스코프에 어떤 식별자가 있는지 기록하는 곳이고,
Outer Lexical Environment Reference는 현재 환경에서 찾지 못했을 때 이동할 바깥 환경의 참조다.
쉽게 말하면 이런 느낌이다.
Environment Record
→ 지금 방 안에 있는 변수/함수 목록
Outer Lexical Environment Reference
→ 이 방에 없으면 찾아갈 바깥 방 주소
식별자를 찾을 때는 현재 Environment Record에서 먼저 찾고,
없으면 Outer Reference를 따라 바깥 환경으로 이동한다. 이게 스코프 체인이다.
다만 이 내용은 이미 앞선 스코프 글에서 꽤 깊게 봤으니, 이번 글에서 다시 길게 파지는 않겠다.
이번 글에서 중요한 건 “실행 컨텍스트 안에는 식별자를 찾기 위한 환경도 있고,
함수 실행과 관련된 상태도 같이 들어 있다”는 점이다.
즉, 실행 컨텍스트는 그냥 콜 스택에 쌓였다가 빠지는 애가 아니라,
코드를 실행하기 위해 필요한 정보를 꽤 이것저것 들고 있는 녀석이라고 보면 된다.
그리고 여기서 다시 앞에서 던졌던 질문으로 돌아가게 된다.
일반 함수는 호출 방식에 따라 `this`가 달라지고,
화살표 함수는 자기 `this` 바인딩을 만들지 않는다고 했다.
근데... 그게 대체 실행 컨텍스트랑 무슨 상관인... 거져?
이제 앞에서 살짝 봤던 `Function Environment Record`와 `[[thisMode]]`를 다시 꺼내볼 차례다.
10. 실행 컨텍스트와 this의 연결고리
너와 나의 연결 고리

앞에서 `this`를 설명하면서 두 가지 스펙 힌트를 봤다.
하나는 Function Environment Record였고, 다른 하나는 함수 객체의 내부 슬롯인 `[[thisMode]]`였다.
이제 이 둘을 실행 컨텍스트 관점에서 다시 연결해보자. 함수가 호출되면 새로운 함수 실행 컨텍스트가 만들어진다.
그리고 화살표 함수가 아닌 일반 함수라면,
그 함수 실행 컨텍스트의 Function Environment Record가 `this` 바인딩을 제공한다.
앞에서 봤던 스펙 문장을 다시 가져오면 이렇다.
A Function Environment Record is a Declarative Environment Record that is used to represent the top-level scope of a function and, if the function is not an ArrowFunction, provides a this binding.
(Function Environment Record는 함수의 최상위 스코프를 나타내는 데 사용되는 Declarative Environment Record이며, 함수가 ArrowFunction이 아닌 경우 `this` 바인딩을 제공한다.)
— ECMAScript Living Standard 9.1.1.3, Function Environment Records
즉, 일반 함수는 실행 컨텍스트가 만들어질 때 자기 `this` 바인딩을 가진다.
그런데 이 `this`가 어떤 값이 될지는 함수 객체의 `[[thisMode]]`와 호출 방식에 따라 달라진다.
대략적인 흐름은 이렇다.
함수 호출
→ 함수 실행 컨텍스트 생성
→ Function Environment Record 생성
→ [[thisMode]]와 호출 방식에 따라 this 바인딩 결정
→ 함수 코드 실행
앞에서 봤던 일반 함수 호출 예시를 다시 보자.
function grind() {
console.log(this);
}
grind();
이 함수가 브라우저 non-strict 환경에서 실행된다면 `[[thisMode]]`는 `"global"` 쪽으로 동작한다.
그래서 `this`가 명시적으로 제공되지 않았을 때 전역 객체로 해석될 수 있다.
반대로 strict mode라면 `[[thisMode]]`는 `"strict"` 쪽으로 동작한다.
"use strict";
function grind() {
console.log(this);
}
grind(); // undefined
strict mode에서는 호출 시 제공된 `this` 값을 그대로 사용한다.
독립 함수 호출에서는 점 앞의 객체 같은 명시적인 수신자가 없으므로 `this`는 `undefined`가 된다.
그러니까 앞에서 봤던 차이는 결국 이렇게 연결된다.
non-strict 일반 함수
→ [[thisMode]] = "global"
→ undefined인 this를 전역 객체로 해석할 수 있음
strict 일반 함수
→ [[thisMode]] = "strict"
→ 호출 시 제공된 this 값을 그대로 사용
→ 독립 함수 호출이면 undefined
반면 화살표 함수는 다르다. 화살표 함수의 `[[thisMode]]`는 `"lexical"`이다.
const grinder = {
name: "Ditting",
grind() {
setTimeout(() => {
console.log(this.name);
}, 100);
},
};
grinder.grind(); // "Ditting"
화살표 함수는 자기 `this` 바인딩을 만들지 않는다.
그래서 `this`를 만나면 자기 실행 컨텍스트 안에서 새로 결정하지 않고, 바깥 환경에서 `this` 바인딩을 찾아간다.
여기서 등장하는 것이 `GetThisEnvironment()`라는 추상 연산이다.

스펙에서는 이렇게 설명한다.
"The abstract operation GetThisEnvironment takes no arguments and returns an Environment Record. It finds the Environment Record that currently supplies the binding of the keyword this."
(추상 연산 GetThisEnvironment는 인수를 받지 않고 Environment Record를 반환한다. 현재 this 키워드의 바인딩을 제공하는 Environment Record를 찾는다.)
— ECMAScript 9.4.3, GetThisEnvironment
스펙 알고리즘을 번역하면 아래와 같은데...
1. Let env be the running execution context's LexicalEnvironment.
(env를 현재 실행 중인 실행 컨텍스트의 LexicalEnvironment로 설정한다.)
2. Repeat,
a. Let exists be env.HasThisBinding()
(exists를 env.HasThisBinding() 결과로 설정한다.)
b. If exists is true, return env.
(exists가 true이면 env를 반환한다. ← this 바인딩 있으면 여기서 반환)
c. Let outer be env.[[OuterEnv]]
(outer를 env.[[OuterEnv]]로 설정한다.)
d. Assert: outer is not null
(outer가 null이 아님을 단언한다.)
e. Set env to outer
(env를 outer로 설정한다. ← 바깥으로 이동해서 반복)
여기서 알고리즘을 전부 따라가려면 HasThisBinding, [[OuterEnv]], ResolveThisBinding 같은 것들까지 같이 봐야 한다.
그리고 솔직히 말하면… 지금의 나는 여기서 스펙 지옥문이 열리는 소리가 들렸다.
그래서 이번 글에서는 알고리즘을 끝까지 파기보다, 핵심 흐름만 잡고 넘어가려고 한다.
현재 환경에서 this 바인딩을 확인한다.
→ 있으면 그 this를 사용한다.
→ 없으면 바깥 환경으로 이동한다.
→ this 바인딩을 제공하는 환경을 찾을 때까지 반복한다.
아까 콜백 예시를 다시 풀어보면 이렇다.
grinder.grind() 호출
→ grind는 일반 메서드이므로 this = grinder
setTimeout 콜백으로 화살표 함수 실행
→ 화살표 함수는 자기 this 바인딩 없음
→ 현재 환경에서 this 해결 불가
→ 바깥 환경인 grind의 Function Environment Record로 이동
→ grind는 this 바인딩을 제공함
→ this = grinder
→ this.name === "Ditting"
그래서 “바깥에서 가져온다”보다 “바깥으로 찾아 나간다”가 더 정확한 표현이다.
정리하면 이렇다.
| 함수 종류 | [[thisMode]] | this 바인딩 |
| non-strict 일반 함수 | "global" | `undefined`인 this를 전역 객체로 해석 |
| strict 일반 함수 | "strict" | 호출 시 제공된 this를 그대로 사용 |
| 화살표 함수 | "lexical" | 자기 this 없음, 바깥 this 사용 |
결국 this는 그냥 감으로 정해지는 값이 아니다.
함수 호출 시 실행 컨텍스트가 만들어지는 과정에서 Function Environment Record와 [[thisMode]]를 통해 설명된다.
물론 GetThisEnvironment, HasThisBinding, ResolveThisBinding까지 제대로 파고들면 스펙을 더 깊게 읽어야 한다.
이번 글에서는 일단 여기까지만 잡고 넘어가자.
11. 화살표 함수는 왜 new로 호출이 안되나욤
여기서 하나 더 연결되는 게 있다. 화살표 함수는 `new`로 호출할 수 없다.
처음엔 그냥 “아, 화살표 함수는 생성자 함수로 못 쓰는구나” 정도로 외웠는데,
이것도 결국 앞에서 본 `this` 이야기와 연결된다.
생성자 함수는 `new`와 함께 호출될 때 새로운 인스턴스를 만들고, 그 인스턴스를 `this`에 바인딩해야 한다.
function Cafe(name) {
this.name = name;
}
const cafe = new Cafe("Ditting");
console.log(cafe.name); // "Ditting"
대략적인 흐름은 이렇다.
new 호출
→ 새 객체 생성
→ this가 새 객체를 가리킴
→ 생성자 함수 본문 실행
→ 완성된 객체 반환
즉, 생성자 함수로 동작하려면 “새로 만들어질 객체를 `this`로 받는 과정”이 필요하다.
그런데 화살표 함수는 자기만의 `this` 바인딩을 만들지 않는다. 그러면 문제가 생긴다.
const Cafe = (name) => {
this.name = name;
};
new Cafe("Ditting"); // TypeError
생성자 함수가 되려면 새 객체를 만들고 그 객체를 `this`로 바인딩해야 하는데,
화살표 함수는 애초에 자기 `this`를 만들지 않는 함수다.
그래서 화살표 함수는 `new`로 호출할 수 없다.
즉, 이건 단순히 “문법적으로 막혀 있다”에서 끝나는 문제가 아니라, 앞에서 본 화살표 함수의 성격과 연결된다.
화살표 함수는 자기 `this` 바인딩을 만들지 않는다.
이 한 문장으로 꽤 많은 것이 설명된다.
콜백에서 this가 안 튀는 이유
→ 자기 this를 만들지 않고 바깥 this를 사용하기 때문
객체 메서드 자체로 쓰면 위험한 이유
→ 객체를 this로 새로 바인딩하지 않기 때문
new로 호출할 수 없는 이유
→ 새 인스턴스를 this로 바인딩할 수 없기 때문
갑자기 퍼즐이 조금씩 맞춰지는 느낌이다.
화살표 함수는 “짧게 쓰는 함수” 정도가 아니라,
`this`를 다루는 방식 자체가 일반 함수와 다른 함수였던 것이다.
12. this 바인딩 최종 정리
여기까지 보면 `this`는 더 이상 외워야 하는 표가 아니다. 물론 외워야 할 건 많다.
근데 적어도 “왜 이렇게 되는지”는 조금 보이기 시작한다. 핵심은 이것이다.
일반 함수의 `this`는 호출 방식에 따라 결정된다. 화살표 함수는 자기 `this` 바인딩을 만들지 않는다.
이 기준으로 다시 정리하면 아래와 같다.
| 호출방식 | this | 이유 |
| 일반함수 호출 | 브라우저 non-strict에서는 전역 객체, strict mode에서는 `undefined` |
독립 함수로 호출되어 명시적인 수신 객체가 없음 |
| 메서드 호출 | 점 앞의 객체 | 객체를 통해 호출되었기 때문 |
| 생성자 함수 호출 | 새로 생성되는 인스턴스 | `new`가 새 객체를 만들고 그 객체를 `this`로 바인딩 |
| call / apply / bind | 첫 번째 인수로 전달한 값 | this`를 명시적으로 지정 |
| 화살표 함수 | 자기 `this` 없음 | 바깥 환경의 `this` 바인딩을 찾아감 |
처음에는 “this는 함수를 호출한 객체다”라고 외웠다. 그런데 이 말은 절반만 맞다.
메서드 호출에서는 꽤 잘 맞는다.
grinder.greet();
이런 경우에는 점 앞의 `grinder`가 `this`가 된다.
하지만 일반 함수 호출, 생성자 함수 호출, `call/apply/bind`, 화살표 함수까지 들어가면
“this는 함수를 호출한 객체”라는 말만으로는 설명이 부족해진다.
조금 더 정확히는 이렇게 정리하는 게 좋다.
this는 함수가 호출되는 방식에 따라 결정된다. 단, 화살표 함수는 자기 this를 새로 만들지 않는다.
렉시컬 스코프와 비교하면 차이가 더 선명해진다.
| 구분 | 결정 기준 |
| 렉시컬 스코프 | 함수가 정의된 위치 |
| 일반함수의 this | 함수가 호출된 방식 |
| 화살표 함수의this | 자기 `this` 없음, 바깥 환경의 `this` 사용 |
렉시컬 스코프는 함수가 어디서 정의되었는지를 기준으로 상위 스코프가 결정된다.
반면 일반 함수의 `this`는 어디서 정의되었는지가 아니라, 어떻게 호출되었는지를 기준으로 결정된다.
그리고 화살표 함수는 예외적으로 자기 `this`를 만들지 않기 때문에, `this`에서도 렉시컬한 성격을 가진다.
그래서 화살표 함수의 `this`를 “렉시컬 this”라고 부르는 것이다.
처음에 외웠던 문장도 이제 조금 다르게 보인다.
화살표 함수는 상위 스코프의 `this`를 가진다.
이 말은 결과를 빠르게 설명하는 표현이고, 동작 원리에 더 가까운 표현은 아래쪽이다.
화살표 함수는 자기 `this` 바인딩이 없어서,`this` 바인딩을 제공하는 바깥 환경을 찾아간다.
외우는 문장에서 이해하는 문장으로 조금 이동한 느낌이라
조금은 성장한 것 같다....아마도?
13. 실행 컨텍스트 기준으로 다시 정리하긔
이번 글은 `this`에서 시작했지만, 결국 실행 컨텍스트까지 오게 됐다.
처음엔 실행 컨텍스트가 그냥 “콜 스택에 쌓이는 실행 단위” 정도인 줄 알았다.
근데 보고 나니 그것보다 조금 더 많은 역할을 하고 있었다.
실행 컨텍스트는 코드 실행을 추적하기 위한 명세상의 장치이고,
그 안에는 실행을 관리하기 위한 여러 상태 컴포넌트가 들어 있다.
이번 글에서 잡은 핵심만 정리하면 아래와 같다.
- 실행 컨텍스트: 코드 실행을 추적하기 위한 명세상의 장치
- 실행 컨텍스트 스택: 현재 실행 중인 컨텍스트를 스택 구조로 관리
- running execution context: 스택 맨 위에 있는 현재 실행 중인 컨텍스트
- 평가 단계: 실행 전에 선언과 환경 정보를 준비하는 단계
- 실행 단계: 준비된 환경을 바탕으로 코드를 실제로 실행하는 단계
- LexicalEnvironment: 식별자를 찾기 위한 환경
- Function Environment Record: 함수 스코프와 `this` 바인딩을 설명하는 환경
- `[[thisMode]]`: 함수가 `this`를 어떻게 처리하는지 설명하는 내부 슬롯
- 일반 함수의 `this`: 호출 시 실행 컨텍스트가 만들어질 때 결정
- 화살표 함수의 `this`: 자기 `this` 바인딩이 없어 바깥 환경에서 찾음
흐름으로 보면 이렇다.
함수 호출
→ 함수 실행 컨텍스트 생성
→ 렉시컬 환경 준비
→ Function Environment Record 확인
→ [[thisMode]]와 호출 방식에 따라 this 처리
→ 코드 실행
물론 실제 스펙 알고리즘은 이것보다 훨씬 더 복잡할 것이다.
하지만 이번 글에서는 일단 이 정도로 잡고 가려고 한다.
실행 컨텍스트는 함수 호출, 실행 흐름, 스코프, `this` 바인딩을 연결해주는 중심축이다.
14. 고찰 — 왜 스코프는 고정이고, this는 흔들릴까?
처음에는 `this`를 그냥 “함수를 호출한 객체” 정도로 외웠다.
근데 이번에 실행 컨텍스트까지 같이 보니까, 이 표현만으로는 부족하다는 걸 알게 됐다.
일반 함수의 `this`는 함수가 어디에 작성됐는지가 아니라, 어떻게 호출됐는지에 따라 달라진다.
화살표 함수는 조금 더 특이하다.
자기 `this` 바인딩을 만들지 않아서, 바깥 환경의 `this`를 찾아간다.
그리고 이 차이는 실행 컨텍스트가 만들어질 때 `Function Environment Record`와 `[[thisMode]]`를 통해 설명할 수 있었다.
물론 여기서 렉시컬 환경이 어떻게 유지되는지, 함수가 정의된 위치를 어떻게 기억하는지까지 파고들면...
그렇다. 그게 바로 다음 장 클로저다.
이번 글에서는 일단 여기까지만 잡고 넘어...가겠다.. 절대 힘들어서가 아니다
마무리
정리하고 나니 22장과 23장은 결국 하나의 질문으로 연결됐다.
코드가 실행될 때, JS 엔진은 어떻게 실행 흐름을 관리하고 `this`를 결정하는가?
처음에는 `this`를 그냥 “함수를 호출한 객체” 정도로 외웠다.
근데 이번에 실행 컨텍스트까지 같이 보니까, 그 말만으로는 부족하다는 걸 알게 됐다.
일반 함수의 `this`는 함수가 어디에 작성됐는지가 아니라, 어떻게 호출됐는지에 따라 달라진다.
메서드로 호출하면 점 앞의 객체가 `this`가 되고,
그냥 함수로 호출하면 브라우저 non-strict 환경에서는 전역 객체로 이어질 수 있고, strict mode에서는 `undefined`가 된다.
`new`로 호출하면 새로 만들어질 인스턴스가 `this`가 되고, call`, `apply`, `bind`를 쓰면 내가 직접 `this`를 지정할 수도 있다.
그리고 화살표 함수는 여기서 조금 다르다. 화살표 함수는 자기 `this` 바인딩을 만들지 않는다.
그래서 `this`를 만나면 자기 안에서 새로 결정하지 않고, `this` 바인딩을 제공하는 바깥 환경을 찾아간다.
처음에 외웠던 말처럼 “상위 스코프의 `this`를 가진다”고 말할 수도 있지만,
조금 더 정확히는 “자기 `this`가 없어서 바깥 환경의 `this`를 찾아간다”에 가깝다.
그리고 이 차이는 실행 컨텍스트가 만들어질 때
`Function Environment Record`와 `[[thisMode]]`를 통해 설명할 수 있었다.
물론 아직 완전히 끝난 건 아니다.
`GetThisEnvironment()`도 제대로 파려면 추상 연산부터 다시 봐야 할 것 같고,
렉시컬 환경이 함수에 어떻게 남는지까지 들어가면 결국 다음 장인 클로저로 이어진다.
이번 글에서는 일단 여기까지만 잡고 넘어가려고 한다.
더 가면 지금 글이 클로저 예고편이 아니라 클로저 지옥문이 될 것 같다.
클로저 발표라니. 이제 진짜 깨질 차례인가 보다.
나 자신... ㅎ...화이또...
앗차차, 나중에 볼 것들
1. 클로저와 [[Environment]]
이번 글에서 실행 컨텍스트와 렉시컬 환경을 다시 봤는데, 여기서 한 발 더 들어가면 결국 클로저로 이어진다.
함수가 정의된 위치의 환경을 어떻게 기억하는지, 함수 실행이 끝난 뒤에도 왜 변수가 살아있는지 보려면 [[Environment]]를 제대로 봐야 할 것 같다. 다음 장에서 제대로 깨져볼 예정. 발표니까.
2. 추상 연산(Abstract Operation)
이번 글에서 GetThisEnvironment() 같은 추상 연산이 등장했다.
대충 무슨 역할인지는 알겠는데, 스펙에서 말하는 추상 연산이 정확히 무엇이고 실제 알고리즘을 어떻게 읽어야 하는지는 아직 더 봐야겠다.
3. GetThisEnvironment와 this 탐색 과정
지금은 화살표 함수가 자기 this 바인딩이 없어서 바깥 환경으로 찾아간다는 정도로 이해했다.
다만 이를 제대로 설명하려면 ResolveThisBinding, HasThisBinding(), [[OuterEnv]]가 어떻게 연결되는지 다시 봐야 할 것 같다.
4. GetIdentifierReference와 ResolveThisBinding 비교
식별자를 찾는 과정과 this를 찾는 과정이 둘 다 환경을 따라간다는 점에서 비슷해 보였다.
정말 비슷한 구조인지, 아니면 닮아 보이는 다른 과정인지 나중에 비교해보고 싶다.
5. 클래스에서 super() 전에 this를 못 쓰는 이유
예전에는 그냥 “자식 클래스에서는 super() 먼저 호출”로 외웠는데, 이번에 보니 this 바인딩이 아직 초기화되지 않은 상태와 연결되는 것 같다. [[ThisBindingStatus]]와 함께 다시 봐야겠다.
6. React에서 화살표 함수 이벤트 핸들러
이번 글에서는 화살표 함수를 this 관점에서 봤지만, React에서는 함수 참조가 매번 새로 만들어지는 문제와도 연결된다.
나중에 useCallback과 함께 정리해보고 싶다.
7. call, apply의 실제 활용
Array.prototype.slice.call(arguments) 같은 패턴이 왜 가능한지, 유사 배열 객체와 함께 다시 보면 더 명확해질 것 같다.
8. [[thisMode]], [[ThisValue]], [[ThisBindingStatus]] 정리
이번 글에서 셋 다 살짝 등장했지만 아직 머릿속에서 완전히 정리되지는 않았다.
특히 화살표 함수의 [[thisMode]] = "lexical"과 [[ThisBindingStatus]] = lexical이 어떻게 연결되는지 나중에 다시 봐야겠다.
'JavaScript Deep Dive 스터디' 카테고리의 다른 글
| [모던 자바스크립트 Deep Dive] - 24장 디이입 다이브 (0) | 2026.05.19 |
|---|---|
| [모던 자바스크립트 Deep Dive] - 24장 정리 (0) | 2026.05.19 |
| [모던 자바스크립트 Deep Dive] - 19~21장 정리 (0) | 2026.04.29 |
| [모던 자바스크립트 Deep Dive] - 19장 맛뵈기? (0) | 2026.04.29 |
| [모던 자바스크립트 Deep Dive] - 16~18장 정리 (re) (1) | 2026.04.21 |



