this가 동적으로 결정되는 이유는?

this가 선언 시점이 아닌 호출 방식에 따라 동적으로 결정되는 원리를 이해하고, 각 호출 패턴에 따른 바인딩 규칙을 학습합니다

중급 15분 this 동적 바인딩 호출 방식 실행 컨텍스트

JavaScript의 this는 다른 프로그래밍 언어와 달리 코드를 작성하는 시점(정적)이 아닌 함수가 실행되는 시점(동적)에 결정됩니다. 이는 JavaScript가 함수를 일급 객체로 다루며, 다양한 방식으로 호출할 수 있도록 설계되었기 때문입니다. 같은 함수라도 어떻게 호출하느냐에 따라 this가 가리키는 대상이 완전히 달라질 수 있으며, 이로 인해 예상치 못한 버그가 발생하기도 합니다. this 바인딩의 동적 특성을 이해하지 못하면 콜백 함수, 이벤트 핸들러, 메서드 참조 등에서 지속적으로 문제를 겪게 됩니다.

🎯 핵심 문제점

  • 호출 방식에 따른 불확실성: 같은 함수도 일반 호출, 메서드 호출, call/apply, new 등 호출 방식에 따라 this가 달라짐
  • 콜백에서의 컨텍스트 손실: 메서드를 콜백으로 전달하면 원래의 객체 컨텍스트를 잃어버림
  • 암묵적 바인딩의 혼란: 점 표기법으로 호출되면 자동으로 바인딩되지만, 참조만 전달하면 바인딩이 사라짐
  • strict mode와 non-strict mode 차이: 실행 모드에 따라 기본 바인딩 동작이 달라짐

💡 실무에서의 영향

동적 this 바인딩은 JavaScript 코드에서 가장 흔한 버그의 원인 중 하나입니다. React, Vue 같은 프레임워크에서 이벤트 핸들러를 등록할 때, 또는 배열 메서드에 콜백을 전달할 때 this가 예상과 다르게 동작해 에러가 발생하는 경우가 빈번합니다. 화살표 함수가 도입된 이유 중 하나도 이러한 this 바인딩 문제를 해결하기 위해서입니다. this 바인딩 규칙을 명확히 이해하면 프레임워크의 동작 원리를 파악하고, 화살표 함수를 언제 사용해야 하는지 판단할 수 있으며, 레거시 코드에서 bind(), call(), apply() 같은 메서드가 왜 사용되었는지 이해할 수 있습니다. 또한 클래스 기반 컴포넌트에서 메서드 바인딩이 필요한 이유와 최신 문법에서 어떻게 이를 개선했는지도 명확하게 알 수 있습니다.


핵심 개념

Static vs Dynamic Binding

입문

변수에 값이 연결되는 시점은 두 가지 방식이 있어요. 미리 정해지는 방식(정적)과 실행할 때 정해지는 방식(동적)입니다. JavaScript의 this는 후자를 따릅니다.

📋 계약서와 전화번호부 계약서에는 “갑”과 “을”이라고 쓰여 있죠. 실제로 누가 “갑”이고 누가 “을”인지는 계약서를 쓸 때가 아니라, 계약서에 서명하는 순간 정해집니다. 전화번호부도 마찬가지예요. “엄마 전화번호”라고 저장해놨지만, 실제 번호는 엄마가 바꾸면 달라지죠.

🏠 정적 바인딩: 건물 주소 대부분의 프로그래밍 언어(Java, C++)에서 this는 정적 바인딩을 사용해요. 마치 건물 주소처럼, 집을 지을 때 이미 주소가 정해지고 바뀌지 않아요. 코드를 작성하는 순간 this가 무엇인지 확정됩니다.

🎭 동적 바인딩: 배역 이름 JavaScript의 this는 동적 바인딩이에요. 연극 대본에 “주인공”이라고 쓰여 있지만, 실제로 누가 주인공인지는 공연 당일에 정해지죠. 같은 함수(대본)라도 누가 호출하느냐(공연하느냐)에 따라 this가 달라집니다.

⏰ 언제 결정되나요? 정적 바인딩은 코드를 작성할 때(컴파일 시점), 동적 바인딩은 코드가 실행될 때(런타임 시점)에 결정돼요. JavaScript는 실행 시점에 누가 함수를 호출했는지 보고 그때그때 this를 정합니다.

중급

정적 바인딩(Static Binding)은 컴파일 타임에 this가 결정되며, 동적 바인딩(Dynamic Binding)은 런타임에 함수 호출 방식에 따라 this가 결정됩니다.

정적 바인딩 (Java, C++) Java나 C++ 같은 언어에서 메서드 내부의 this는 해당 메서드가 정의된 클래스 인스턴스를 가리키며, 컴파일 시점에 확정됩니다. 메서드를 어떻게 호출하든 this는 항상 인스턴스 자신을 가리킵니다.

동적 바인딩 (JavaScript) JavaScript에서 함수는 일급 객체이므로 다양한 방식으로 호출될 수 있습니다. 같은 함수라도 일반 함수로 호출되면 this가 전역 객체(또는 undefined)를 가리키고, 메서드로 호출되면 객체를 가리키며, call/apply로 호출되면 전달된 값을 가리킵니다.

class Person {
    String name = "Alice";

    void greet() {
        System.out.println(this.name); // this는 항상 Person 인스턴스
    }
}

Person p = new Person();
p.greet(); // Alice - this는 p
const person = {
  name: 'Alice',
  greet: function() {
    console.log(this.name);
  }
};

person.greet(); // "Alice" - this는 person

const greet = person.greet;
greet(); // undefined 또는 에러 - this는 전역 객체 또는 undefined

핵심 차이점

  • 정적 바인딩: 코드를 보면 this가 무엇인지 즉시 알 수 있음 (예측 가능)
  • 동적 바인딩: 실행 시점의 호출 방식을 분석해야 this를 알 수 있음 (유연하지만 복잡)

심화

JavaScript의 동적 this 바인딩은 ECMAScript 명세의 실행 컨텍스트(Execution Context) 메커니즘과 thisMode 속성을 통해 구현되며, 이는 정적 타입 언어의 vtable 기반 바인딩과 근본적으로 다른 설계입니다.

ECMAScript 명세 기반 동적 바인딩 메커니즘 ECMAScript 2024, Section 9.4 (Execution Contexts)에 따르면, 모든 함수 실행 시 새로운 실행 컨텍스트가 생성되며 thisBinding 컴포넌트가 포함됩니다. thisBinding은 함수 호출 시점에 ResolveThisBinding 추상 연산을 통해 결정됩니다.

정적 바인딩 언어(C++, Java)는 컴파일 타임에 가상 함수 테이블(vtable)을 생성하고, 메서드 호출 시 vtable 오프셋으로 인스턴스 포인터를 직접 참조합니다. 반면 JavaScript는 다음 단계를 런타임에 수행합니다:

  1. 함수 객체의 [[Call]] 내부 메서드 호출
  2. thisMode 검사 (lexical, strict, global 중 하나)
  3. 호출 방식에 따라 thisArgument 결정
  4. 실행 컨텍스트의 thisBinding에 설정

화살표 함수는 thisMode가 ‘lexical’이므로 별도의 thisBinding을 생성하지 않고 외부 환경 레코드의 this를 참조합니다.

V8 엔진의 동적 바인딩 최적화 V8 엔진에서 동적 this 바인딩은 다음과 같이 최적화됩니다:

Inline Caching (IC): 반복적으로 동일한 방식으로 호출되는 함수의 경우, IC가 호출 패턴을 학습하여 this 결정 로직을 캐싱합니다. 단일형 호출 사이트(Monomorphic Call Site)의 경우 this 바인딩 오버헤드가 <5ns까지 감소합니다.

Hidden Class Transition: 동일한 객체에서 메서드를 반복 호출할 때, Hidden Class가 안정적이면 this 바인딩이 기계어 수준에서 직접 포인터 연산으로 컴파일됩니다.

TurboFan JIT: 자주 실행되는 핫 루프(Hot Loop)에서 this 사용 패턴을 분석하여, 가능한 경우 정적 바인딩처럼 최적화된 기계어로 컴파일합니다. 예를 들어 메서드 호출만 반복되는 경우 this를 레지스터에 고정시킵니다.

그러나 다형성 호출 사이트(Polymorphic Call Site, 3개 이상의 다른 객체에서 호출)의 경우 Megamorphic 상태로 전환되어 최적화가 무효화되고, 매번 동적 조회가 발생하여 성능이 10-50배 저하될 수 있습니다.

정적 vs 동적 바인딩 성능 특성 C++ vtable 호출: 단일 간접 참조(1 cycle), 분기 예측 실패 시 ~20 cycles JavaScript 최적화된 메서드 호출: IC 히트 시 ~5ns, IC 미스 시 ~50-100ns JavaScript Megamorphic 호출: ~500ns (동적 프로퍼티 조회 + this 결정)

따라서 성능 크리티컬한 코드에서는 동일한 객체 형태로 메서드를 호출하여 IC 효율을 높이고, 가능한 경우 화살표 함수로 this 조회를 제거하는 것이 권장됩니다.

Call-site Determines this

입문

JavaScript에서 this는 함수를 “어디서” 썼는지가 아니라 “어떻게” 불렀는지에 따라 결정됩니다. 같은 함수라도 부르는 방법이 다르면 this가 완전히 달라져요.

📞 전화기와 발신자 집 전화기를 생각해보세요. 전화기는 그 자체로는 “누가 전화를 거는지” 모릅니다. 엄마가 전화기를 들면 엄마의 전화가 되고, 아빠가 들면 아빠의 전화가 되죠. 전화기(함수)가 거실에 있든 안방에 있든(어디 정의되었든) 중요하지 않아요. 누가 받느냐(호출하느냐)가 중요합니다.

🎮 게임 컨트롤러 똑같은 게임 컨트롤러라도, 1P 포트에 꽂으면 1번 캐릭터를 조종하고, 2P 포트에 꽂으면 2번 캐릭터를 조종해요. 컨트롤러(함수) 자체는 변하지 않지만, 어디에 연결하느냐(어떻게 호출하느냐)에 따라 제어 대상(this)이 달라집니다.

🔍 호출 위치를 찾아라 this가 무엇인지 알고 싶다면, 함수가 정의된 코드를 보지 말고, 함수가 실제로 실행되는 곳(호출 위치, Call-site)을 찾아야 해요. 그곳에서 함수가 어떤 방식으로 호출되었는지 보면 this를 알 수 있습니다.

🎯 왜 헷갈릴까요? 우리가 함수를 만들 때는 특정 객체를 염두에 두고 만들지만, JavaScript는 그걸 신경 쓰지 않아요. 오직 실행되는 순간의 호출 방식만 봅니다. 그래서 같은 함수를 다른 방식으로 부르면 전혀 다른 결과가 나올 수 있어요.

중급

호출 위치(Call-site)는 함수가 실제로 호출되는 코드의 위치를 의미하며, this 바인딩은 호출 위치에서의 호출 방식에 따라 결정됩니다. 함수가 정의된 위치나 함수가 어떤 객체의 프로퍼티인지는 this 바인딩에 영향을 주지 않습니다.

Call-site 분석 방법 this를 정확히 파악하려면 함수가 호출되는 지점의 코드를 분석해야 합니다. 중첩된 함수 호출이 있을 때는 콜 스택(Call Stack)을 추적하여 현재 실행 중인 함수를 호출한 직전 지점을 찾습니다.

function identify() {
  console.log(this.name);
}

const person1 = {
  name: 'Alice',
  identify: identify  // 함수를 프로퍼티로 할당
};

const person2 = {
  name: 'Bob',
  identify: identify  // 동일한 함수를 할당
};

// Call-site 1: person1 객체를 통한 호출
person1.identify(); // "Alice" - this는 person1

// Call-site 2: person2 객체를 통한 호출
person2.identify(); // "Bob" - this는 person2

// Call-site 3: 함수 직접 호출
identify(); // undefined 또는 에러 - this는 전역 객체 또는 undefined

정의 위치 vs 호출 위치 위 예시에서 identify 함수는 한 곳에만 정의되었지만, 세 가지 다른 방식으로 호출되었습니다. 각 호출 위치에서의 호출 방식이 다르므로 this도 매번 달라집니다.

function outer() {
  console.log('outer this:', this);

  function inner() {
    console.log('inner this:', this);
  }

  inner(); // Call-site: outer 함수 내부의 일반 호출
}

const obj = {
  name: 'Object',
  method: outer
};

obj.method();
// outer this: obj (메서드 호출)
// inner this: global/undefined (일반 함수 호출)

Call Stack과 Call-site 중첩된 함수의 경우, 각 함수마다 고유한 call-site가 있으며, 각각 독립적으로 this가 결정됩니다. 외부 함수의 this가 내부 함수에 자동으로 전달되지 않습니다.

심화

Call-site 기반 this 바인딩은 ECMAScript 명세의 MemberExpression 평가와 Call Expression 처리 과정에서 결정되며, 이는 동적 스코프(Dynamic Scope)와 유사하지만 스코프 체인과는 독립적으로 동작합니다.

ECMAScript 명세의 Call-site 처리 메커니즘 ECMAScript 2024, Section 13.3.6 (The new Operator)와 13.3.5 (Function Calls)에 따르면, 함수 호출 시 다음 단계를 거쳐 this가 결정됩니다:

  1. MemberExpression 또는 CallExpression 평가
  2. 평가 결과가 Reference인 경우, Reference의 base 값 추출
  3. base 값이 객체이면 thisValue로 사용, 아니면 호출 방식에 따라 결정
  4. [[Call]] 내부 메서드 호출 시 thisValue 전달

핵심은 Reference 타입이 call-site 정보를 담고 있다는 점입니다. Reference는 (base, referencedName, strict) 튜플로, base가 this 바인딩의 후보가 됩니다.

Reference Type의 동작

const obj = {
  name: 'Object',
  getName: function() { return this.name; }
};

obj.getName();
// 1. obj.getName 평가 -> Reference(base: obj, name: "getName", strict: false)
// 2. Reference의 base인 obj가 thisValue로 전달
// 3. this = obj

const fn = obj.getName;
fn();
// 1. fn 평가 -> 함수 객체 직접 반환 (Reference 아님)
// 2. thisValue가 undefined (strict) 또는 global (non-strict)
// 3. this = undefined/global

첫 번째 호출에서는 MemberExpression (obj.getName)이 Reference를 반환하므로 base인 obj가 this가 되지만, 두 번째 호출에서는 변수 fn이 이미 함수 값을 직접 가리키므로 Reference가 생성되지 않아 기본 바인딩 규칙이 적용됩니다.

Call Stack과 this의 독립성 JavaScript의 this는 렉시컬 스코프(Lexical Scope)와 달리 호출 스택을 따라 전파되지 않습니다. 각 함수 실행 컨텍스트는 독립적인 thisBinding을 가지며, 외부 함수의 this를 내부 함수가 자동으로 상속하지 않습니다.

이는 동적 스코프 언어(예: Bash, Perl의 local 변수)와 유사해 보이지만, 스코프 체인을 따르지 않고 오직 call-site의 호출 방식만 고려한다는 점에서 차이가 있습니다. 동적 스코프는 호출 체인을 따라 변수를 조회하지만, this는 현재 call-site만 고려합니다.

V8의 Call-site 최적화 V8 엔진은 call-site 패턴을 분석하여 다음과 같이 최적화합니다:

Monomorphic Call-site: 항상 동일한 객체에서 호출되는 경우, IC(Inline Cache)가 객체 형태(Hidden Class)와 this를 캐싱하여 조회 없이 직접 참조합니다.

Polymorphic Call-site (2-4개 객체): IC에 여러 Hidden Class를 캐싱하고, 호출 시 Hidden Class를 비교하여 해당하는 this를 반환합니다.

Megamorphic Call-site (5개 이상): IC를 포기하고 매번 동적 조회를 수행합니다. 이 경우 성능이 크게 저하됩니다.

따라서 성능 최적화를 위해서는 동일한 call-site에서 동일한 형태의 객체로 함수를 호출하여 Monomorphic 상태를 유지하는 것이 중요합니다.

The 4 Binding Rules

입문

JavaScript에서 this가 무엇인지 결정하는 규칙은 4가지가 있어요. 호출 방식을 보면 어떤 규칙이 적용되는지 알 수 있습니다.

🎯 규칙 1: 기본 바인딩 - 아무것도 없을 때 함수를 그냥 이름만 불러서 실행하면(예: greet()) this는 기본값으로 설정돼요. 마치 손님이 “주인 찾아요” 하면 기본적으로 집주인이 나오는 것처럼요. 보통은 전역 객체(브라우저의 window)가 되지만, 엄격 모드에서는 undefined가 됩니다.

🏠 규칙 2: 암묵적 바인딩 - 점으로 부를 때 person.greet()처럼 객체에 점을 찍고 함수를 부르면, 점 앞의 객체가 this가 돼요. “홍길동씨 전화 주세요”라고 하면 홍길동이 전화를 받는 것처럼, 점 앞의 객체가 주인이 됩니다. 이게 가장 흔한 패턴이에요.

📞 규칙 3: 명시적 바인딩 - 직접 지정할 때 call, apply, bind 같은 특수 메서드를 사용하면 this를 직접 정할 수 있어요. 마치 “이 전화는 김철수씨가 받게 해주세요”라고 명시하는 것처럼, 우리가 원하는 객체를 this로 강제로 설정할 수 있습니다.

🆕 규칙 4: new 바인딩 - 새로 만들 때 new 키워드로 함수를 실행하면, 새로운 빈 객체가 만들어지고 그게 this가 돼요. 마치 새 집을 지으면 그 집의 주인이 되는 것처럼, 새로 만들어진 객체가 자동으로 this가 됩니다. 이건 생성자 함수에서 사용돼요.

🔢 우선순위가 있어요 네 가지 규칙이 동시에 적용될 수 있을 때는 우선순위가 있어요. new > 명시적 > 암묵적 > 기본 순서입니다. 뒤로 갈수록 약해지고, 앞의 규칙이 뒤의 규칙을 이깁니다.

중급

JavaScript의 this 바인딩은 4가지 규칙에 따라 결정되며, 각 규칙은 명확한 우선순위를 가집니다. 호출 위치에서 어떤 규칙이 적용되는지 판별하면 this를 정확히 예측할 수 있습니다.

규칙 1: 기본 바인딩 (Default Binding)

독립적인 함수 호출(standalone function invocation)에 적용됩니다. 어떤 객체나 컨텍스트 없이 함수 이름만으로 호출할 때 사용됩니다.

function greet() {
  console.log(this); // non-strict: window, strict: undefined
}

greet(); // 일반 함수 호출

strict mode에서는 thisundefined가 되며, non-strict mode에서는 전역 객체가 됩니다.

규칙 2: 암묵적 바인딩 (Implicit Binding)

함수가 객체의 메서드로 호출될 때 적용됩니다. 점 표기법(.) 또는 대괄호 표기법([])으로 호출하면 점/대괄호 앞의 객체가 this가 됩니다.

const person = {
  name: 'Alice',
  greet: function() {
    console.log(this.name);
  }
};

person.greet(); // "Alice" - this는 person

규칙 3: 명시적 바인딩 (Explicit Binding)

call(), apply(), bind() 메서드를 사용하여 this를 명시적으로 지정할 때 적용됩니다.

function greet() {
  console.log(this.name);
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

greet.call(person1);  // "Alice" - this를 person1로 설정
greet.apply(person2); // "Bob" - this를 person2로 설정

const boundGreet = greet.bind(person1);
boundGreet(); // "Alice" - this가 person1로 고정된 새 함수

규칙 4: new 바인딩

new 키워드로 생성자 함수를 호출할 때 적용됩니다. 새로 생성된 객체가 this가 됩니다.

function Person(name) {
  this.name = name; // this는 새로 생성되는 객체
}

const alice = new Person('Alice');
console.log(alice.name); // "Alice"

바인딩 우선순위 여러 규칙이 동시에 적용 가능한 경우 다음 우선순위를 따릅니다:

  1. new 바인딩 (가장 높음)
  2. 명시적 바인딩 (call/apply/bind)
  3. 암묵적 바인딩 (메서드 호출)
  4. 기본 바인딩 (가장 낮음)
function greet() {
  console.log(this.name);
}

const obj = { name: 'Object' };

// 암묵적 바인딩 vs 명시적 바인딩
const objGreet = greet.bind({ name: 'Explicit' });
obj.greet = objGreet;
obj.greet(); // "Explicit" - 명시적 바인딩 우승

심화

4가지 바인딩 규칙은 ECMAScript 명세의 [[Call]] 내부 메서드와 OrdinaryCallBindThis 추상 연산에서 구현되며, 각 규칙의 우선순위는 명세의 조건문 순서로 정의됩니다.

ECMAScript 명세의 바인딩 규칙 구현 ECMAScript 2024, Section 10.2.1.1 (OrdinaryCallBindThis)에서 this 바인딩 규칙이 정의됩니다:

OrdinaryCallBindThis(F, calleeContext, thisArgument)
1. thisMode = F.[[ThisMode]]
2. If thisMode is lexical, return NormalCompletion(undefined)
3. calleeRealm = F.[[Realm]]
4. localEnv = calleeContext의 LexicalEnvironment
5. If thisMode is strict:
   a. thisValue = thisArgument
6. Else:
   a. If thisArgument is undefined or null:
      i. globalEnv = calleeRealm.[[GlobalEnv]]
      ii. thisValue = globalEnv.[[GlobalThisValue]]
   b. Else:
      i. thisValue = ToObject(thisArgument)
7. envRec = localEnv의 EnvironmentRecord
8. envRec.BindThisValue(thisValue)

이 알고리즘에서 thisArgument가 각 바인딩 규칙에 따라 결정됩니다:

  • 기본 바인딩: thisArgument가 undefined → 5.a 또는 6.a.i 경로
  • 암묵적 바인딩: MemberExpression의 base가 thisArgument로 전달
  • 명시적 바인딩: call/apply의 첫 번째 인자가 thisArgument
  • new 바인딩: [[Construct]]에서 새 객체를 thisArgument로 전달

바인딩 우선순위의 명세적 근거 우선순위는 함수 호출 경로의 분기 순서로 결정됩니다:

  1. new 바인딩: [[Construct]] 내부 메서드가 [[Call]]과 별도 경로이며, [[Construct]]에서 OrdinaryCreateFromConstructor로 새 객체를 먼저 생성하여 thisArgument로 전달합니다.

  2. 명시적 바인딩: Function.prototype.call/apply는 직접 thisArgument를 지정하여 [[Call]]을 호출하므로, 암묵적 바인딩(Reference의 base)보다 우선합니다.

  3. 암묵적 바인딩: MemberExpression 평가 시 Reference의 base가 추출되어 thisArgument로 사용됩니다.

  4. 기본 바인딩: 위 규칙이 모두 적용되지 않을 때 OrdinaryCallBindThis의 6.a 경로로 전역 객체 또는 undefined가 사용됩니다.

bind()의 하드 바인딩 구현 Function.prototype.bind()는 BoundFunctionCreate 추상 연산으로 새로운 exotic object를 생성하며, [[BoundThis]] 내부 슬롯에 thisArgument를 저장합니다. Bound Function의 [[Call]]은 원본 함수의 [[Call]]을 [[BoundThis]]로 호출하므로, 이후 call/apply로도 변경할 수 없습니다(하드 바인딩).

const bound = func.bind(obj);
// bound의 [[BoundThis]] = obj (불변)

bound.call(other); // other가 무시되고 여전히 obj가 this

이는 명세의 10.4.1.1 [[Call]]에서 명시됩니다:

BoundFunctionExoticObject.[[Call]](thisArgument, argumentsList)
1. target = F.[[BoundTargetFunction]]
2. boundThis = F.[[BoundThis]]
3. boundArgs = F.[[BoundArguments]]
4. args = boundArgs + argumentsList
5. Return Call(target, boundThis, args)

boundThis가 항상 사용되므로 외부에서 전달된 thisArgument는 무시됩니다.

new와 bind의 상호작용 new는 bind보다 우선순위가 높지만, 이는 [[Construct]] 내부 메서드가 [[Call]]과 독립적이기 때문입니다. Bound Function에 new를 사용하면:

function Person(name) {
  this.name = name;
}

const BoundPerson = Person.bind({ ignored: true });
const instance = new BoundPerson('Alice');
// instance.name === 'Alice'
// instance는 Person.prototype을 상속
// bind의 thisArgument는 무시됨

BoundFunctionExoticObject.[[Construct]]는 원본 함수의 [[Construct]]를 호출하면서 새 객체를 thisArgument로 전달하므로, [[BoundThis]]가 무시됩니다.

V8의 바인딩 최적화 V8 엔진은 바인딩 규칙에 따라 다음과 같이 최적화합니다:

Fast Mode Receiver Checks: 암묵적 바인딩의 경우 IC(Inline Cache)에 Hidden Class를 캐싱하고, 메서드 호출 시 Hidden Class 비교만으로 this를 결정합니다(~5ns).

Bound Function Optimization: bind()로 생성된 함수는 TurboFan에서 원본 함수로 인라이닝되며, boundThis를 상수로 처리하여 조회 오버헤드를 제거합니다.

Constructor Check Elimination: new 호출이 확실한 경우(타입 피드백으로 검증), new.target 체크와 프로토타입 설정을 최적화하여 일반 함수 호출 수준의 성능을 달성합니다.

그러나 동적으로 바인딩 규칙이 변경되는 경우(예: 같은 함수를 때로는 new로, 때로는 일반 호출로 사용) 최적화가 무효화되어 성능이 저하될 수 있습니다.

Why JavaScript Chose Dynamic Binding

입문

JavaScript가 this를 동적으로 결정하도록 만든 이유는 함수를 여러 곳에서 재사용할 수 있게 하기 위해서예요.

🔧 만능 도구 스위스 아미 나이프(만능 칼)를 생각해보세요. 하나의 도구에 여러 기능이 있어서, 상황에 따라 다르게 사용할 수 있죠. 함수도 마찬가지예요. 하나의 함수를 여러 객체에서 공유해서 사용할 수 있도록, this를 호출할 때마다 다르게 정할 수 있게 만든 거예요.

📚 도서관 책 도서관에서 책을 빌리면, 그 책은 빌린 사람의 책이 되죠. 하지만 책 자체는 누구의 것이라고 정해져 있지 않아요. 반납하면 다른 사람이 또 빌릴 수 있습니다. JavaScript 함수도 이렇게 여러 객체가 공유해서 쓸 수 있어요. 누가 부르느냐에 따라 주인(this)이 바뀝니다.

🎮 유연성이 필요했어요 JavaScript는 웹 브라우저에서 동작하는 가벼운 스크립트 언어로 만들어졌어요. 한 가지 일만 잘하는 것보다, 여러 상황에 유연하게 대응할 수 있는 게 더 중요했죠. 그래서 함수를 변수에 담아 전달하고, 다른 객체에서 빌려 쓸 수 있게 설계했어요.

🔄 함수를 값처럼 쓸 수 있어요 JavaScript에서는 함수를 숫자나 문자열처럼 변수에 담고, 다른 함수의 인자로 전달하고, 반환값으로 받을 수 있어요. 이렇게 자유롭게 함수를 다루려면, this도 상황에 맞게 바뀌어야 해요. 만약 함수가 만들어질 때 this가 고정되면 이런 유연성을 잃게 됩니다.

중급

JavaScript가 동적 this 바인딩을 채택한 이유는 함수를 일급 객체(First-class Object)로 다루고, 함수형 프로그래밍과 객체 지향 프로그래밍을 모두 지원하기 위함입니다.

일급 함수와 메서드 빌리기(Method Borrowing) JavaScript에서 함수는 변수에 할당하고, 인자로 전달하고, 반환값으로 사용할 수 있는 일급 객체입니다. 이는 하나의 함수를 여러 객체에서 재사용할 수 있음을 의미하며, 동적 this 바인딩은 이를 가능하게 합니다.

const greet = function() {
  console.log(`Hello, ${this.name}`);
};

const person1 = { name: 'Alice', greet: greet };
const person2 = { name: 'Bob', greet: greet };

person1.greet(); // "Hello, Alice"
person2.greet(); // "Hello, Bob"

동일한 함수를 여러 객체가 공유하며, 호출 시점에 this가 결정되므로 각 객체의 데이터에 접근할 수 있습니다.

유연한 콜백 패턴 지원 JavaScript는 비동기 프로그래밍과 이벤트 기반 아키텍처에서 콜백 함수를 광범위하게 사용합니다. 동적 this 바인딩 덕분에 콜백 함수를 다양한 컨텍스트에서 재사용할 수 있습니다.

const utils = {
  prefix: '>>',
  format: function(item) {
    return `${this.prefix} ${item}`;
  }
};

const arr = ['a', 'b', 'c'];

// call을 사용하여 다른 객체의 메서드를 빌려 씀
const formatted = arr.map(function(item) {
  return utils.format.call(utils, item);
});
// [">> a", ">> b", ">> c"]

프로토타입 기반 상속 JavaScript는 클래스 기반이 아닌 프로토타입 기반 상속을 사용합니다. 메서드는 프로토타입 객체에 한 번만 정의되고, 모든 인스턴스가 공유합니다. 동적 this 바인딩 덕분에 각 인스턴스에서 같은 메서드를 호출해도 자신의 데이터에 접근할 수 있습니다.

function Person(name) {
  this.name = name;
}

// 메서드는 프로토타입에 한 번만 정의
Person.prototype.greet = function() {
  console.log(`Hello, ${this.name}`);
};

const alice = new Person('Alice');
const bob = new Person('Bob');

alice.greet(); // "Hello, Alice" - this는 alice
bob.greet();   // "Hello, Bob" - this는 bob

// 두 인스턴스가 같은 함수를 공유
console.log(alice.greet === bob.greet); // true

정적 바인딩의 한계 만약 JavaScript가 정적 this 바인딩을 사용했다면, 각 객체마다 별도의 함수 복사본을 만들어야 하므로 메모리 낭비가 발생하고, 메서드 재사용과 프로토타입 상속의 이점을 잃게 됩니다.

심화

JavaScript의 동적 this 바인딩은 프로토타입 기반 객체 시스템과 일급 함수의 조화를 위한 설계 선택이며, Self 언어의 영향을 받아 메시지 전달(Message Passing) 패러다임을 지원하기 위해 도입되었습니다.

역사적 맥락: Self와 Smalltalk의 영향 JavaScript의 프로토타입 시스템은 Self 언어에서 영감을 받았으며, Self는 객체 간 메시지 전달을 중심으로 설계되었습니다. 메시지를 받는 객체(receiver)가 메서드 실행의 컨텍스트가 되어야 하므로, this는 메시지를 받은 객체를 가리켜야 합니다.

Smalltalk의 메시지 전달 모델에서 receiver method: arguments 구문은 receiver가 메서드의 this가 되는 것을 의미합니다. JavaScript의 obj.method(args)도 이와 동일한 의미론을 따르며, 점 앞의 객체가 메시지 수신자(this)가 됩니다.

ECMAScript 명세의 설계 철학 ECMAScript 1 (1997) 명세부터 함수는 일급 객체로 정의되었으며, 함수 객체는 [[Call]] 내부 메서드를 통해 다양한 this 값으로 호출될 수 있도록 설계되었습니다. 이는 다음 목적을 달성하기 위함입니다:

  1. 메모리 효율성: 프로토타입 체인의 메서드를 모든 인스턴스가 공유하면서도, 각 인스턴스의 상태에 접근 가능
  2. 함수형 프로그래밍 지원: 고차 함수, 커링, 부분 적용 등에서 this를 동적으로 바인딩
  3. 동적 언어 특성: 런타임에 객체 구조 변경, 메서드 동적 추가 등을 지원

메모리 효율성과 프로토타입 체인 정적 바인딩을 사용하는 언어(C++, Java)에서는 각 인스턴스마다 가상 함수 테이블(vtable) 포인터를 유지하지만, 메서드 자체는 클래스 메타데이터에 저장됩니다. JavaScript는 vtable 없이 프로토타입 체인만으로 메서드를 조회하며, 동적 this 바인딩으로 인스턴스별 메서드 복사를 방지합니다.

예를 들어 10,000개의 인스턴스가 10개의 메서드를 공유한다면:

  • 정적 바인딩: 10,000 × 10 = 100,000개의 바인딩된 함수 필요 (클로저 또는 메서드 복사)
  • 동적 바인딩: 10개의 프로토타입 메서드만 필요, 런타임에 this 바인딩 (메모리 절약)

Function.prototype.call/apply/bind의 존재 이유 동적 바인딩의 유연성을 극대화하기 위해 ECMAScript는 명시적 바인딩 메서드를 제공합니다. 이는 다음 패턴을 가능하게 합니다:

Function Borrowing: 배열이 아닌 유사 배열 객체에서 배열 메서드를 빌려 씀

function toArray() {
  return Array.prototype.slice.call(arguments);
}

Method Delegation: 한 객체의 메서드를 다른 객체의 컨텍스트에서 실행

const hasOwn = Object.prototype.hasOwnProperty;
hasOwn.call(obj, 'prop'); // obj에 프로퍼티 존재 확인

Partial Application: this를 고정한 새 함수 생성 (bind)

const logger = console.log.bind(console);
logger('message'); // this가 console로 고정

화살표 함수의 등장 배경 ES6 화살표 함수는 동적 this의 복잡성을 해결하기 위해 도입되었습니다. 화살표 함수는 thisMode가 ‘lexical’이므로 자체 this 바인딩을 생성하지 않고 외부 스코프의 this를 캡처합니다. 이는 콜백에서 this 손실 문제를 해결합니다.

// 일반 함수: 동적 this
setTimeout(obj.method, 100); // this 손실

// 화살표 함수: 렉시컬 this
setTimeout(() => obj.method(), 100); // this 유지

그러나 화살표 함수는 프로토타입 메서드로 사용할 수 없습니다(항상 외부 this를 참조하므로 인스턴스별 this 바인딩 불가).

성능 트레이드오프 동적 this 바인딩은 런타임 오버헤드를 발생시키지만, 현대 JavaScript 엔진은 다음과 같이 최적화합니다:

Inline Caching: 반복적인 호출 패턴을 학습하여 this 조회를 캐싱 (Monomorphic 호출 시 <5ns) Speculative Optimization: TurboFan/IonMonkey가 타입 피드백으로 this를 예측하여 인라이닝 Escape Analysis: this가 외부로 노출되지 않으면 스택 할당으로 최적화

결과적으로 최적화된 동적 바인딩은 정적 바인딩과 유사한 성능(~2% 차이)을 보이며, 유연성의 이점이 성능 비용을 상쇄합니다.

대안 설계와의 비교 Python의 명시적 self: 메서드의 첫 인자로 self를 받음 (정적이지만 명시적) Lua의 콜론 연산자: obj:method()obj.method(obj)의 syntactic sugar Rust의 self 바인딩: 정적 타입 시스템으로 컴파일 타임에 검증

JavaScript는 동적 타입 언어로서 런타임 유연성을 우선시했으며, 이는 웹 브라우저의 동적 환경(DOM 조작, 이벤트 핸들링)과 잘 부합합니다.