호이스팅은 JavaScript 엔진이 코드를 실행하기 전 컴파일 단계에서 변수와 함수 선언을 스코프의 최상단으로 끌어올리는 동작을 의미합니다. 이는 실제로 코드가 이동하는 것이 아니라 JavaScript 엔진이 내부적으로 선언부를 먼저 메모리에 등록한 후 실행 단계를 진행하기 때문에 발생하는 현상입니다. 호이스팅을 이해하지 못하면 선언 전에 변수를 참조하는 코드에서 예상치 못한 undefined 값이나 ReferenceError가 발생할 수 있습니다. 특히 var, let, const, function 선언이 각각 다르게 호이스팅되기 때문에 이러한 차이점을 명확히 이해하는 것이 중요합니다.
핵심 특징
- JavaScript 엔진은 코드 실행 전 컴파일 단계에서 모든 선언을 먼저 메모리에 등록합니다
- var로 선언된 변수는 호이스팅되며 undefined로 초기화되어 선언 전에도 접근 가능합니다
- let과 const는 호이스팅되지만 TDZ(Temporal Dead Zone) 구간에서는 접근할 수 없습니다
- 함수 선언문은 전체가 호이스팅되어 선언 전에도 호출이 가능하지만, 함수 표현식은 변수 호이스팅 규칙을 따릅니다
- 호이스팅은 스코프 단위로 발생하며 블록 스코프와 함수 스코프에서 각각 다르게 동작합니다
실무에서의 영향
호이스팅을 정확히 이해하지 못하면 레거시 코드를 유지보수하거나 디버깅할 때 큰 어려움을 겪을 수 있습니다. 예를 들어 var 변수가 선언 전에 사용되는 코드를 발견했을 때 왜 에러가 발생하지 않고 undefined가 나오는지 이해하지 못하면 버그의 원인을 찾기 어렵습니다. 또한 let과 const의 TDZ 개념을 모르면 왜 특정 구간에서만 ReferenceError가 발생하는지 혼란스러울 수 있습니다. 함수 선언문과 함수 표현식의 호이스팅 차이를 알면 코드 실행 순서를 예측할 수 있어 의도하지 않은 동작을 방지할 수 있습니다. 최신 JavaScript에서는 let과 const 사용이 권장되지만 호이스팅 원리를 이해해야 오래된 코드베이스를 안전하게 리팩토링하고 팀원들과 명확하게 소통할 수 있습니다. 특히 면접이나 코드 리뷰에서 호이스팅 관련 질문은 JavaScript 기본 개념 이해도를 측정하는 대표적인 주제이므로 반드시 숙지해야 합니다.
핵심 개념
컴파일 단계와 실행 단계
입문
JavaScript는 코드를 바로 실행하는 것이 아니라 준비 단계를 먼저 거쳐요. 이 준비 단계에서 변수와 함수를 미리 기억해 두는 것이 호이스팅의 시작이에요.
📋 코드 실행 전 준비가 필요해요 레스토랑에서 요리를 하기 전에 재료를 먼저 준비하듯이, JavaScript도 코드를 실행하기 전에 ‘준비 단계’를 거쳐요. 이 단계에서 어떤 변수와 함수가 있는지 미리 확인하고 메모해 둡니다.
🎯 두 단계로 나눠서 일해요 JavaScript 엔진은 크게 두 단계로 일합니다. 첫 번째 ‘컴파일 단계’에서는 코드를 읽으며 선언들을 모두 찾아서 기록해요. 두 번째 ‘실행 단계’에서는 실제로 코드를 한 줄씩 실행합니다. 마치 시험 문제를 먼저 쭉 읽어보고(컴파일) 그 다음에 답을 쓰는(실행) 것과 비슷해요.
💡 왜 두 번 일할까요? 한 번에 하면 안 될까요? 그런데 이렇게 두 단계로 나누면 좋은 점이 있어요. 미리 어떤 변수가 있는지 알고 있으면, 나중에 실행할 때 빠르게 찾을 수 있거든요. 도서관에서 책을 찾을 때도 먼저 목록을 보고 위치를 확인하면 훨씬 빠르게 찾을 수 있는 것과 같아요.
🚀 호이스팅은 컴파일 단계의 결과예요 컴파일 단계에서 선언들을 미리 기록해 두기 때문에, 나중에 실행할 때 선언보다 먼저 변수를 사용할 수 있게 되는 거예요. 이게 바로 ‘호이스팅’이라는 현상입니다.
중급
JavaScript 엔진은 코드를 실행하기 전 컴파일 단계(Compilation Phase)와 실행 단계(Execution Phase)로 나누어 처리합니다.
컴파일 단계의 동작 컴파일 단계에서는 코드를 파싱하며 모든 변수 선언과 함수 선언을 찾아내어 스코프에 등록합니다. 이때 실제 값을 할당하거나 함수를 실행하지는 않고, 식별자(identifier)만 메모리에 등록합니다.
실행 단계의 동작 실행 단계에서는 코드를 위에서 아래로 순차적으로 실행하며, 변수에 값을 할당하고 함수를 호출합니다. 이미 컴파일 단계에서 등록된 식별자를 참조하므로 선언 전에도 접근이 가능한 것입니다.
// 작성한 코드
console.log(x); // undefined
var x = 10;
// 컴파일 단계에서의 내부 동작 (개념적 표현)
// var x; → 메모리에 x 등록, 값은 undefined
// 실행 단계에서의 동작
// console.log(x); → undefined 출력
// x = 10; → x에 10 할당
중요한 오해 호이스팅이 코드를 “물리적으로” 위로 옮기는 것은 아닙니다. 코드는 그대로 있고, 컴파일 단계에서 선언 정보를 미리 처리하는 것입니다.
심화
JavaScript 엔진의 코드 처리는 ECMAScript 명세의 실행 컨텍스트(Execution Context) 생성 과정에서 정의된 두 단계를 통해 이루어집니다.
ECMAScript 명세 기반 컴파일 메커니즘 ECMAScript 2023, Section 9.2 (ECMAScript Function Objects)에 따르면, 함수가 호출될 때 FunctionDeclarationInstantiation이라는 추상 연산이 먼저 실행됩니다. 이 과정에서:
-
환경 레코드 생성: 함수 환경 레코드(Function Environment Record)가 생성되어 변수 환경(Variable Environment)과 렉시컬 환경(Lexical Environment)을 초기화합니다.
-
선언적 바인딩 생성: 모든 함수 선언(FunctionDeclaration)을 스캔하여 환경 레코드에 바인딩을 생성하고 함수 객체로 초기화합니다.
-
변수 바인딩 생성: var 선언을 스캔하여 변수 바인딩을 생성하고 undefined로 초기화합니다. let/const 선언도 바인딩을 생성하지만 초기화하지 않습니다(uninitialized 상태).
V8 엔진의 구현 최적화 V8 엔진은 이 과정을 Ignition 인터프리터의 바이트코드 생성 단계에서 최적화합니다:
Preparse Phase: 전체 소스를 먼저 스캔하여 함수 경계와 변수 선언을 식별합니다. 이 정보는 Scope Info 객체에 저장됩니다.
Bytecode Generation: Ignition이 바이트코드를 생성할 때, 선언 정보를 바탕으로 CreateFunctionContext와 같은 바이트코드 명령어를 생성하여 환경 레코드를 효율적으로 초기화합니다.
Hidden Class Transition: var 선언이 많은 함수는 컴파일 단계에서 미리 메모리 레이아웃을 결정하므로 Hidden Class 전환 오버헤드가 감소합니다.
이러한 두 단계 분리는 Just-In-Time(JIT) 컴파일 최적화를 가능하게 하며, TurboFan 최적화 컴파일러가 타입 추론과 인라인 캐싱을 수행할 수 있는 기반이 됩니다.
var 호이스팅과 undefined 초기화
입문
var로 만든 변수는 선언하기 전에도 사용할 수 있어요. 하지만 이상한 점이 있어요. 값은 우리가 넣은 게 아니라 ‘undefined’라는 특별한 값이 들어있어요.
🎁 미리 준비된 빈 상자 생일 파티를 준비한다고 생각해봐요. 친구가 오기 전에 미리 선물 상자를 테이블에 올려놓았어요. 하지만 아직 선물은 안 넣었죠. 상자는 있지만 비어있는 거예요. var 변수도 똑같아요. 선언하기 전에도 변수는 이미 만들어져 있지만, 값은 ‘undefined(정해지지 않음)‘라는 빈 상태예요.
❓ 왜 에러가 안 날까요? 다른 프로그래밍 언어에서는 선언 전에 변수를 사용하면 보통 에러가 나요. 하지만 var는 특별해요. JavaScript가 코드를 읽을 때 미리 “아, var x라는 변수가 있구나”라고 기억해 두거든요. 그래서 나중에 x를 사용해도 “x가 뭐야?”라고 묻지 않고 “x는 알지만 아직 값이 없어, undefined야”라고 대답하는 거예요.
🔍 undefined가 뭔가요? undefined는 “아직 정해지지 않았어요”라는 뜻이에요. 설문지에 답을 안 쓴 빈칸처럼요. 값이 없다는 걸 표현하는 JavaScript만의 특별한 방법이에요.
🚨 왜 이게 문제일까요? 실수로 선언 전에 변수를 사용해도 에러가 안 나니까 버그를 찾기 어려워요. 마치 선물 상자가 있어서 선물이 있다고 착각했는데, 막상 열어보니 비어있는 것과 같아요. 코드가 이상하게 동작해도 눈치채기 힘들죠.
중급
var로 선언된 변수는 호이스팅 과정에서 undefined로 초기화되어, 선언문보다 앞에서 참조해도 ReferenceError가 발생하지 않습니다.
var 호이스팅의 동작 원리 컴파일 단계에서 var 선언을 발견하면 해당 변수를 스코프에 등록하고 즉시 undefined로 초기화합니다. 따라서 실행 단계에서 선언문 이전에 해당 변수를 참조하면 undefined 값을 얻게 됩니다.
console.log(x); // undefined (에러 아님)
var x = 10;
console.log(x); // 10
// 내부적으로는 이렇게 동작
// var x = undefined; (컴파일 단계)
// console.log(x); // undefined
// x = 10; (실행 단계)
// console.log(x); // 10
undefined vs ReferenceError var로 선언된 변수는 선언 전에도 undefined를 반환하지만, 선언되지 않은 변수는 ReferenceError를 발생시킵니다.
console.log(x); // undefined (var로 호이스팅됨)
var x = 10;
console.log(y); // ReferenceError: y is not defined (선언되지 않음)
함수 스코프 내 호이스팅 var는 함수 스코프를 가지므로, 함수 내 어디서 선언하든 함수 최상단으로 호이스팅됩니다.
function example() {
console.log(a); // undefined
if (true) {
var a = 20;
}
console.log(a); // 20
}
// var a는 함수 최상단으로 호이스팅되어 전체 함수에서 접근 가능
심화
var의 undefined 초기화는 ECMAScript 명세의 CreateMutableBinding과 InitializeBinding 추상 연산의 조합으로 구현되며, 이는 V8 엔진의 컨텍스트 초기화 최적화와 밀접하게 연결되어 있습니다.
ECMAScript 명세 기반 var 바인딩 메커니즘 ECMAScript 2023, Section 9.2.12 (FunctionDeclarationInstantiation)의 알고리즘에 따르면:
-
바인딩 생성: 29단계에서 varEnv.CreateMutableBinding(vn, false)를 호출하여 변경 가능한 바인딩을 생성합니다.
-
즉시 초기화: 30단계에서 varEnv.InitializeBinding(vn, undefined)를 즉시 호출하여 undefined로 초기화합니다.
이는 let/const와 대조적입니다. let/const는 CreateMutableBinding 또는 CreateImmutableBinding만 호출하고 InitializeBinding은 실제 선언문 위치에서 호출됩니다.
V8 엔진의 메모리 레이아웃 최적화 V8에서 var 변수의 undefined 초기화는 성능 최적화에 중요한 역할을 합니다:
Stack Slot 할당: var 변수는 함수 진입 시 스택 프레임에 고정된 슬롯을 할당받습니다. Ignition 바이트코드는 LdaUndefined 명령어를 사용해 이 슬롯을 undefined로 초기화합니다.
// Ignition 바이트코드 예시
Ldar a0 // 매개변수 로드
Star r0 // 레지스터에 저장
LdaUndefined // undefined 로드
Star r1 // var x 슬롯에 undefined 저장
Hidden Class 안정성: 모든 var 변수가 함수 시작 시 undefined로 초기화되므로, 함수의 Hidden Class가 예측 가능하게 됩니다. 이는 Inline Cache의 히트율을 높여 프로퍼티 접근 성능을 향상시킵니다.
Garbage Collection 최적화: undefined로 초기화된 슬롯은 GC가 추적할 필요가 없어 마이너 GC의 스캔 오버헤드가 감소합니다.
TurboFan 최적화 TurboFan 컴파일러는 var 변수의 undefined 초기화 정보를 활용해 타입 추론을 수행합니다. 변수가 undefined에서 시작한다는 것을 알면, 첫 할당 전까지 타입을 Undefined로 좁힐 수 있어 불필요한 타입 체크를 제거할 수 있습니다.
let/const 호이스팅과 TDZ
입문
let과 const도 호이스팅되지만 var와 다르게 동작해요. 선언 전에 사용하면 에러가 나요. 왜 그럴까요? ‘TDZ’라는 특별한 구역 때문이에요.
🚫 접근 금지 구역이 있어요 놀이공원에 들어가려면 입장 게이트를 통과해야 하죠? let과 const 변수도 비슷해요. 변수가 선언되는 줄이 ‘입장 게이트’예요. 그 전까지는 ‘접근 금지 구역(TDZ)‘이라서 절대 들어갈 수 없어요. 들어가려고 하면 경비원이 막는 것처럼 에러가 발생해요.
⏰ 시간적으로 죽은 구역 TDZ는 ‘Temporal Dead Zone’의 줄임말이에요. ‘시간적으로 죽은 구역’이라는 뜻이죠. 코드가 시작되고 변수 선언 줄에 도달하기까지의 시간 동안, 그 변수는 ‘죽은 상태’예요. 만질 수도 볼 수도 없어요.
💡 왜 이렇게 만들었을까요? var의 문제점을 해결하기 위해서예요. var는 선언 전에 써도 에러가 안 나서 실수하기 쉬웠어요. 그래서 let과 const는 “선언하기 전에는 절대 쓰지 마!”라고 강제로 막아놓은 거예요. 실수를 미리 방지하는 안전장치라고 할 수 있어요.
🔍 호이스팅은 되는데 접근이 안 돼요 신기한 점은 let과 const도 사실 호이스팅되긴 해요. JavaScript가 “let x가 있구나”라고 미리 알고 있어요. 하지만 var처럼 undefined로 초기화하지 않고 그냥 “알기만” 하는 거예요. 그래서 선언 줄 전까지는 접근하면 에러가 나는 거죠.
중급
let과 const도 호이스팅되지만 TDZ(Temporal Dead Zone, 일시적 사각지대)가 존재하여 선언 전에 접근하면 ReferenceError가 발생합니다.
TDZ의 정의 TDZ는 스코프의 시작 지점부터 변수 선언문까지의 구간을 의미합니다. 이 구간에서는 변수가 환경 레코드에 등록되어 있지만 초기화되지 않은 상태(uninitialized)이므로 접근할 수 없습니다.
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
// TDZ 구간 시각화
{
// TDZ 시작 - x는 등록되었지만 초기화 안 됨
// console.log(x); ← 여기서 접근하면 ReferenceError
let x = 10; // TDZ 종료 - 초기화 완료
console.log(x); // 10 - 정상 접근 가능
}
const의 TDZ const도 동일하게 TDZ가 적용됩니다. 선언과 동시에 초기화해야 하므로 TDZ 구간에서 접근하면 ReferenceError가 발생합니다.
let x = 1;
function example() {
console.log(x); // ReferenceError!
let x = 2; // 블록 스코프 내 새로운 x 선언
}
example();
// 함수 내부의 let x가 호이스팅되어 외부 x를 가리지만,
// TDZ 구간이므로 접근 불가
TDZ의 실용적 의미 TDZ는 변수를 선언 전에 사용하는 실수를 컴파일 타임이 아닌 런타임에 감지하여 명확한 에러를 제공합니다. 이는 코드의 안정성을 높이는 중요한 기능입니다.
심화
let/const의 TDZ는 ECMAScript 명세의 Lexical Environment와 Environment Record의 초기화 지연 메커니즘을 통해 구현되며, 이는 V8 엔진의 바이트코드 검증 레이어에서 강제됩니다.
ECMAScript 명세 기반 TDZ 구현 ECMAScript 2023, Section 9.2.12 (FunctionDeclarationInstantiation)와 Section 14.3.1 (let and const Declarations)에 따르면:
-
바인딩 생성 시점: let 선언이 스코프에 진입할 때(컴파일 단계) CreateMutableBinding이 호출되어 바인딩을 생성하지만, InitializeBinding은 호출되지 않습니다.
-
초기화 지연: InitializeBinding은 실행 흐름이 실제 let 선언문에 도달했을 때만 호출됩니다. 이 사이의 구간이 TDZ입니다.
-
접근 검증: GetBindingValue 추상 연산은 바인딩이 초기화되지 않은 상태이면 ReferenceError를 발생시킵니다(Section 9.1.1.1.6).
V8 엔진의 TDZ 런타임 검증 V8은 TDZ를 Ignition 바이트코드의 런타임 체크로 구현합니다:
// Ignition 바이트코드 예시
LdaContextSlot r0, [4], [0] // 컨텍스트 슬롯에서 변수 로드
ThrowReferenceErrorIfHole // hole 값이면 ReferenceError 발생
Star r1 // 레지스터에 저장
Hole Value: 초기화되지 않은 let/const 변수는 내부적으로 “the hole” 이라는 특수 값으로 표현됩니다. 이는 undefined와 구별되는 내부 값입니다.
동적 검증: 변수 접근 시마다 ThrowReferenceErrorIfHole 바이트코드가 실행되어 런타임에 TDZ 위반을 감지합니다. 이는 정적 분석만으로는 모든 경우를 커버할 수 없기 때문입니다(예: 조건부 실행 흐름).
TurboFan 최적화와 TDZ TurboFan 컴파일러는 TDZ 체크를 최적화할 수 있습니다:
Constant Folding: 선언 후 접근이 확실한 경우 TDZ 체크를 제거합니다.
Escape Analysis: 변수가 스코프를 벗어나지 않고 순차적으로 사용되면 hole 체크를 생략할 수 있습니다.
그러나 일반적으로 TDZ 체크는 제거되지 않으며, 이는 약간의 성능 오버헤드를 발생시킵니다(벤치마크: var 대비 약 2-3% 느림, 하지만 안전성 향상 대비 무시 가능).
블록 스코프와 TDZ의 상호작용 let/const는 블록 스코프를 가지므로, 각 블록마다 독립적인 TDZ가 생성됩니다. 이는 중첩 블록에서 같은 이름의 변수를 사용할 때 shadowing과 결합되어 복잡한 TDZ 구간을 만들 수 있습니다.
함수 호이스팅
입문
함수도 호이스팅되는데, 변수보다 훨씬 더 강력해요. 함수는 선언 전에 호출해도 완벽하게 작동해요!
🎪 공연 리허설과 본 공연 학교에서 연극을 준비한다고 생각해봐요. 본 공연 전에 리허설을 먼저 하죠? 함수 선언은 리허설 같아요. 코드를 실행하기 전에 JavaScript가 “이런 함수가 있구나”하고 미리 완전히 배우는 거예요. 그래서 나중에 어디서든 그 함수를 부를 수 있어요.
⚡ 함수는 통째로 올라가요 var 변수는 이름만 호이스팅되고 값은 undefined였죠? 하지만 함수 선언은 다릅니다. 이름뿐만 아니라 함수 내용 전체가 통째로 호이스팅돼요. 마치 책 전체를 복사해서 맨 앞에 붙여놓는 것과 같아요.
🎯 함수 표현식은 달라요
함수를 만드는 방법이 두 가지 있어요. 함수 선언문(function greet() {})과 함수 표현식(const greet = function() {})이에요. 함수 선언문만 통째로 호이스팅되고, 함수 표현식은 변수 호이스팅 규칙을 따라요.
📝 실용적인 팁 함수 선언문을 사용하면 코드 순서를 신경 쓰지 않아도 돼요. 함수를 파일 맨 아래에 쓰고 맨 위에서 호출해도 잘 작동해요. 하지만 함수 표현식(let, const로 만든 함수)은 선언 후에만 호출할 수 있어요.
중급
함수 선언문(Function Declaration)은 전체가 호이스팅되어 선언 전에도 호출이 가능하지만, 함수 표현식(Function Expression)은 변수 호이스팅 규칙을 따릅니다.
함수 선언문 호이스팅 함수 선언문은 컴파일 단계에서 함수 이름과 함수 본체 전체가 함께 호이스팅됩니다. 따라서 선언문보다 앞에서 함수를 호출해도 정상적으로 실행됩니다.
greet(); // "Hello!" - 선언 전 호출 가능
function greet() {
console.log("Hello!");
}
// 내부적으로는 이렇게 동작
// function greet() { console.log("Hello!"); } ← 전체가 호이스팅
// greet(); ← 실행
함수 표현식 호이스팅 함수 표현식은 변수에 함수를 할당하는 형태이므로, 변수 호이스팅 규칙을 따릅니다. var로 선언하면 undefined, let/const로 선언하면 TDZ가 적용됩니다.
// var로 선언한 함수 표현식
greet1(); // TypeError: greet1 is not a function
var greet1 = function() {
console.log("Hello!");
};
// let으로 선언한 함수 표현식
greet2(); // ReferenceError: Cannot access 'greet2' before initialization
let greet2 = function() {
console.log("Hi!");
};
화살표 함수와 호이스팅 화살표 함수는 항상 함수 표현식이므로 변수 호이스팅 규칙을 따릅니다.
greet(); // ReferenceError
const greet = () => {
console.log("Hello!");
};
실무 권장사항 함수 선언문은 호이스팅으로 인해 코드 순서가 자유롭지만, 가독성을 위해 사용 전에 선언하는 것이 좋습니다. 함수 표현식(특히 const와 화살표 함수)은 TDZ로 인해 더 예측 가능하므로 현대 JavaScript에서 선호됩니다.
심화
함수 호이스팅은 ECMAScript 명세의 FunctionDeclarationInstantiation 알고리즘과 V8의 Lazy Parsing 메커니즘에서 서로 다른 최적화 전략을 보입니다.
ECMAScript 명세 기반 함수 호이스팅 ECMAScript 2023, Section 9.2.12 (FunctionDeclarationInstantiation), 단계 14-17에 따르면:
-
함수 선언 우선 처리: FunctionDeclaration을 먼저 스캔하여 InstantiateFunctionObject 추상 연산을 호출해 함수 객체를 생성합니다.
-
변수보다 우선: 같은 이름의 var 선언이 있어도 함수 선언이 우선합니다. var는 이미 바인딩이 존재하면 무시됩니다(단계 30의 hasBinding 체크).
-
즉시 초기화: 함수 객체는 생성과 동시에 env.InitializeBinding(fn, fo)로 바인딩에 할당됩니다.
함수 표현식과의 차이 함수 표현식은 AssignmentExpression으로 취급되어 실행 단계에서 평가됩니다. let/const로 선언된 함수 표현식은 TDZ가 적용되므로 선언 전 접근 시 ReferenceError가 발생합니다.
V8 엔진의 Lazy Parsing 최적화 V8은 함수 호이스팅을 처리할 때 Lazy Parsing(지연 파싱) 최적화를 적용합니다:
Eager vs Lazy: 최상위 함수 선언은 즉시 파싱되어 SharedFunctionInfo 객체를 생성하지만, 함수 본문은 실제 호출 시까지 파싱을 지연할 수 있습니다.
// V8 내부 구조 (단순화)
SharedFunctionInfo* Parser::ParseFunctionDeclaration() {
// 함수 선언 즉시 처리
FunctionLiteral* function = ParseFunctionLiteral(...);
SharedFunctionInfo* sfi = CreateSharedFunctionInfo(function);
return sfi; // 환경 레코드에 즉시 바인딩
}
Preparse Data: 지연 파싱 시에도 함수 경계, 매개변수 개수 등 메타데이터는 미리 추출되어 Context 생성에 사용됩니다.
TurboFan 인라인 최적화 함수 선언문은 컴파일 단계에서 확정되므로 TurboFan이 더 공격적으로 인라이닝할 수 있습니다:
Monomorphic Call Sites: 함수 선언문은 변경될 가능성이 낮아 Call IC(Inline Cache)가 안정적으로 유지됩니다.
Inlining Heuristics: 작은 함수 선언문(<600 바이트)은 TurboFan이 즉시 인라이닝 후보로 고려합니다.
반면 함수 표현식은 런타임에 할당되므로 인라이닝이 더 보수적으로 적용됩니다.
메모리 레이아웃 차이 함수 선언문은 컨텍스트 생성 시 슬롯이 확정되지만, 함수 표현식은 실행 흐름에 따라 동적으로 할당됩니다. 이는 마이너한 메모리 오버헤드를 발생시키지만(<1KB per context), 가비지 컬렉션 시 조기 회수 가능성을 높입니다.