암시적 바인딩(Implicit Binding)은 JavaScript에서 this가 결정되는 규칙 중 실무에서 가장 자주 마주치는 규칙으로, 함수를 어떤 객체의 메서드로 호출할 때 그 객체가 자동으로 this에 연결되는 원리입니다. “암시적”이라는 이름처럼, 개발자가 this를 직접 지정하지 않아도 호출 방식만으로 this가 결정됩니다. 객체지향 스타일로 JavaScript 코드를 작성할 때 이 규칙이 기반이 되기 때문에, 암시적 바인딩을 정확히 이해하는 것은 의도한 대로 동작하는 메서드를 작성하기 위한 필수 조건입니다. 그러나 이 규칙은 동시에 가장 흔한 this 관련 버그의 원인이기도 합니다. 메서드를 변수에 저장하거나 콜백으로 전달하는 순간 암시적 바인딩이 조용히 사라지고, 개발자가 기대하는 객체 대신 전혀 다른 값이 this가 되는 일이 발생합니다.
핵심 특징
- 🔗 호출 방식이 this를 결정:
obj.method()형태로 호출하면 점(.) 앞의 객체obj가 자동으로this에 바인딩됨 - 체이닝 호출의 기준:
a.b.c()처럼 여러 단계로 이어진 호출에서는 가장 마지막 점 앞 객체b가this로 결정됨 - 암시적 바인딩 손실(Implicit Binding Loss): 메서드를 변수에 할당하거나 다른 함수의 인자로 전달하면 객체와의 연결이 끊어져 기본 바인딩으로 되돌아감
- ⚠️ 콜백에서의 함정:
setTimeout, 이벤트 핸들러, 배열 메서드(forEach,map)에 메서드를 전달하면 암시적 바인딩이 소실되어this가 의도치 않게 변함 - 명시적 바인딩보다 낮은 우선순위:
call,apply,bind나new가 사용되면 암시적 바인딩은 덮어씌워짐
왜 중요한가?
일상적인 JavaScript 코드에서 객체의 메서드를 호출하는 패턴은 어디에나 존재합니다. 암시적 바인딩은 이 패턴이 작동하게 만드는 원리이므로, 이를 이해해야 메서드 내부에서 this로 객체의 다른 속성에 안전하게 접근하는 코드를 작성할 수 있습니다. 그러나 React, Vue 같은 프레임워크에서 이벤트 핸들러나 비동기 콜백을 다룰 때 암시적 바인딩 손실은 반복적으로 등장하는 문제이고, 이를 모르면 this가 undefined이거나 전혀 다른 객체를 가리키는 버그를 디버깅하는 데 많은 시간을 낭비하게 됩니다. 특히 클래스 기반 컴포넌트에서 this.handleClick을 버튼에 전달할 때나, 서비스 객체의 메서드를 다른 모듈로 넘길 때 이 문제가 자주 나타납니다. 암시적 바인딩이 언제 작동하고 언제 소실되는지를 정확히 파악해야, 이후에 배울 bind, 화살표 함수 같은 해결책이 왜 필요한지를 진정으로 이해하고 상황에 맞게 적용할 수 있습니다.
핵심 개념
암시적 바인딩의 동작 원리
입문
객체의 메서드를 호출할 때 점(.) 앞에 있는 객체가 자동으로 this가 돼요. 개발자가 아무것도 지정하지 않아도 이 규칙이 자동으로 적용돼요.
🏠 누구의 집에서 일하느냐가 중요해요
친구 집에 가서 친구 집 물건을 쓰면, 자연스럽게 그 집 물건을 쓰는 거잖아요. 객체의 메서드를 호출할 때도 마찬가지예요. user.greet()라고 쓰면, greet 함수는 user의 집에서 실행되는 거예요. 그래서 함수 안에서 this는 자동으로 user를 가리켜요.
🎯 점(.) 앞의 객체가 this예요
obj.method()처럼 점으로 연결해서 호출할 때, 점 앞에 있는 것이 this가 돼요. 마치 “누구의 메서드인가?”를 점 앞에서 알 수 있는 것처럼요. 사람.인사() 라면 “사람”이 this가 되는 거예요.
💡 왜 “암시적”이라고 부를까요?
“암시적”은 “눈에 보이지 않게 자동으로”라는 뜻이에요. this가 어디에 연결되는지 코드에 직접 쓰지 않아도, 호출하는 방식만으로 자동으로 연결되기 때문에 “암시적 바인딩”이라고 불러요. 반대로 call이나 bind처럼 직접 지정하면 “명시적 바인딩”이라고 해요.
📦 함수 자체가 아니라 호출 방식이 중요해요
같은 함수라도 어떻게 호출하느냐에 따라 this가 달라져요. 마치 같은 배달부라도 어느 회사 소속으로 일하느냐에 따라 쓸 수 있는 차량이 달라지는 것처럼요. 함수 자체가 아니라 “점 앞의 객체”가 this를 결정해요.
중급
암시적 바인딩(Implicit Binding)은 함수가 어떤 객체의 속성으로 참조되어 호출될 때 — 즉 obj.method() 형태로 호출될 때 — 해당 객체(obj)가 자동으로 this에 바인딩되는 규칙입니다.
핵심은 함수가 어떻게 정의되었는지가 아니라 어떻게 호출되었는지입니다. 함수가 호출 시점에 점(.) 앞의 객체를 context 객체로 가지면 암시적 바인딩이 적용됩니다.
function greet() {
console.log(`Hello, I'm ${this.name}`);
}
const user = {
name: 'Alice',
greet: greet // 객체의 속성으로 참조
};
user.greet(); // "Hello, I'm Alice" — this === user
함수가 어느 객체에 정의되어 있든 상관없이, **호출 시점에 점 앞의 객체가 this**가 됩니다. 위 예시처럼 외부에서 정의된 함수를 객체에 추가해도 암시적 바인딩이 동일하게 적용됩니다.
function introduce() {
console.log(`My name is ${this.name}`);
}
const alice = { name: 'Alice', introduce };
const bob = { name: 'Bob', introduce };
alice.introduce(); // "My name is Alice" — this === alice
bob.introduce(); // "My name is Bob" — this === bob
심화
암시적 바인딩은 ECMAScript 명세의 함수 호출 평가 과정에서 Reference Record의 IsPropertyReference 검사가 참(true)으로 평가될 때 발동하며, 이 Reference Record의 [[Base]] 값이 this로 공급됩니다.
ECMAScript 명세의 EvaluateCall과 Reference Record
ECMAScript 2023, Section 13.3.6.1 (Runtime Semantics: EvaluateCall)에 따르면, 함수 호출 시 thisValue는 다음 방식으로 결정됩니다. MemberExpression을 평가하면 Reference Record가 반환됩니다. IsPropertyReference(ref)가 true — 즉 ref.[[Base]]가 객체(또는 원시값)인 경우 — ref.[[Base]]를 thisValue로 사용합니다. obj.method()에서 [[Base]]는 obj이고, [[ReferencedName]]은 "method"이므로, thisValue는 자동으로 obj가 됩니다.
OrdinaryCallBindThis와 암시적 바인딩의 상호작용
Section 10.2.1.2 (OrdinaryCallBindThis)에서 thisMode가 strict이면 전달된 thisValue를 그대로 사용하고, global이면 원시값을 ToObject로 감쌉니다. 암시적 바인딩으로 전달된 [[Base]]는 이미 객체이므로 coercion 없이 그대로 바인딩됩니다. 원시값을 통한 메서드 호출("hello".toUpperCase())은 래퍼 객체(String 인스턴스)가 [[Base]]로 전달되어 일시적으로 this가 됩니다.
V8 엔진의 암시적 바인딩 최적화
V8의 TurboFan 컴파일러는 obj.method() 호출 패턴에서 Inline Cache(인라인 캐시)를 적극 활용합니다. Hidden Class(히든 클래스, 객체의 구조를 추적하는 내부 표현)가 안정적인 객체에서 같은 메서드를 반복 호출하면, 프로퍼티 조회 없이 직접 메서드 포인터로 분기합니다. 이로 인해 암시적 바인딩 패턴은 성능상 최적화된 경로(Monomorphic IC)를 타는 경우가 많습니다.
체이닝 호출에서의 this 결정
입문
a.b.c() 처럼 점이 여러 개 이어진 호출에서는, 가장 마지막 점 바로 앞에 있는 것만 this가 돼요.
🔗 체인의 맨 끝이 중요해요
여러 사람이 손을 잡고 서 있다고 상상해봐요. 맨 끝 사람이 무언가를 할 때, 그 사람이 현재 어느 팀 소속인지는 바로 옆에 있는 사람(오른쪽)을 보면 알 수 있어요. a.b.c()에서 c()를 실행할 때 this는 a가 아니라 c를 소유한 b예요.
🎯 “직접 소유”가 기준이에요
a.b.c()에서 c를 직접 갖고 있는 건 b예요. a는 b를 갖고 있을 뿐이에요. 그래서 c()가 실행될 때 this는 b가 돼요. 마치 회사에서 팀장(b)이 직원(c)을 직접 관리하고, 부서장(a)은 팀장을 관리하는 것처럼요. 직원 입장에서 직속 상관은 팀장이에요.
💡 왜 이 규칙이 있을까요?
JavaScript는 항상 “바로 직전 점 앞의 객체”를 this로 삼아요. 점이 여러 개라도 규칙은 변하지 않아요. 마지막 점 앞을 보면 돼요. first.second.third()라면 third()를 직접 부른 건 second이니까 this는 second예요.
🚨 혼동하기 쉬운 함정이에요
a.b.c()에서 this가 a라고 생각하기 쉬워요. 하지만 아니에요. 맨 처음 객체가 아니라 “마지막 점 바로 앞”이 this예요. 이 차이를 정확히 알아야 나중에 긴 체이닝 코드를 읽을 때 헷갈리지 않아요.
중급
체이닝 호출(a.b.c())에서는 가장 마지막 점(.) 바로 앞의 객체가 this로 결정됩니다. 즉 c()를 직접 호출한 컨텍스트 객체인 b가 this가 되고, a는 this에 영향을 주지 않습니다.
이 규칙은 체인 길이와 무관하게 일관되게 적용됩니다. ref.[[Base]]는 항상 “마지막 점 앞의 객체”이기 때문입니다.
const company = {
team: {
name: 'Frontend',
introduce() {
console.log(`Team: ${this.name}`);
}
}
};
company.team.introduce();
// this === company.team
// 출력: "Team: Frontend"
// company는 this가 되지 않음
const a = {
label: 'A',
b: {
label: 'B',
c() {
console.log(this.label); // B — this는 b
}
}
};
a.b.c(); // 출력: "B"
체이닝 호출에서 착각하기 쉬운 지점은 “처음 객체(a)“가 this라고 오해하는 것입니다. 실제로는 함수를 직접 보유한 “마지막 점 앞 객체”가 this입니다.
심화
체이닝 호출에서의 this 결정은 MemberExpression의 중첩 평가 과정에서 Reference Record가 순차적으로 생성되고 마지막 참조만 thisValue로 전달되는 메커니즘에 의해 구현됩니다.
중첩 MemberExpression 평가와 Reference Record 생성
a.b.c() 호출은 ECMAScript 명세의 좌결합(left-associative) 파싱에 의해 ((a.b).c)()로 처리됩니다. 평가 순서는 다음과 같습니다. 먼저 a.b가 평가되어 [[Base]]: a, [[ReferencedName]]: "b"인 Reference Record R1이 생성됩니다. GetValue(R1)으로 b 객체를 획득합니다. 이어서 b.c가 평가되어 [[Base]]: b, [[ReferencedName]]: "c"인 Reference Record R2가 생성됩니다. 최종 호출 EvaluateCall(func, R2, args)에서 IsPropertyReference(R2)가 true이므로 R2.[[Base]] 즉 b가 thisValue로 전달됩니다. a에 대한 Reference Record R1은 중간 값 획득에만 사용되고 thisValue로 전달되지 않습니다.
실용적 함의 — 구조 분해와 체이닝
이 메커니즘 때문에 const { c } = a.b; c() 형태로 메서드를 추출하면 Reference Record의 [[Base]]가 소실되어 기본 바인딩이 적용됩니다. 반면 a.b.c()는 호출 시점에 R2가 생성되므로 암시적 바인딩이 유지됩니다. Proxy 객체를 사용한 체이닝(proxy.a.b())에서는 [[Get]] 트랩이 각 단계에서 호출되지만, 최종 thisValue는 여전히 마지막 점 앞 객체(proxy.a의 결과 값)입니다.
암시적 바인딩 손실
입문
객체의 메서드를 변수에 담거나 다른 곳에 전달하면, 함수는 그 객체와의 연결 고리를 잃어버려요. 마치 팀 유니폼을 벗으면 어느 팀인지 알 수 없는 것처럼요.
🎽 유니폼을 벗으면 팀 소속을 알 수 없어요 축구선수가 팀 유니폼을 입고 있으면 누구 소속인지 알 수 있어요. 그런데 유니폼을 벗고 평상복을 입으면, 겉으로 봐서는 어느 팀인지 알 수 없죠. 메서드를 변수에 저장하는 것도 같아요. 함수가 변수에 담기는 순간 “이 함수는 어떤 객체 소속”이라는 정보가 사라져요.
📤 함수만 전달되고 소속 정보는 남아요
const fn = user.greet처럼 변수에 저장하면, fn에는 함수 코드만 들어가고 “user 소속”이라는 정보는 사라져요. 마치 레시피 카드만 복사했는데 “이 레시피는 할머니 것”이라는 메모는 복사되지 않는 것처럼요.
🚨 어떤 결과가 생기나요?
변수에 저장한 후 호출하면 this가 원래 객체가 아닌 전역 객체나 undefined가 돼요. 코드는 아무 오류 없이 실행되는데 this.name 같은 값이 undefined로 나오는 버그가 생겨요. 이런 종류의 버그는 원인을 찾기가 특히 어려워요.
💡 이게 왜 중요한가요?
이 현상은 JavaScript 코드를 짜다가 가장 자주 마주치는 this 버그의 원인이에요. 나중에 배울 bind나 화살표 함수가 바로 이 문제를 해결하기 위해 만들어진 방법이에요. 지금 이 원인을 이해해야 해결 방법도 제대로 쓸 수 있어요.
중급
암시적 바인딩 손실(Implicit Binding Loss)은 메서드를 변수에 할당하거나 다른 함수의 인자로 전달할 때 발생합니다. 이 경우 함수는 객체 컨텍스트 없이 단독으로 호출되어 기본 바인딩이 적용됩니다.
손실이 발생하는 두 가지 주요 패턴:
- 변수 할당:
const fn = obj.method— Reference Record의 base가 소실됨 - 인자로 전달:
doSomething(obj.method)— 함수 값만 전달, base 소실
const user = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // "Hello, Alice" — 암시적 바인딩 적용
const fn = user.greet; // 함수만 추출, base(user) 정보 소실
fn(); // "Hello, undefined" (비엄격 모드) 또는 TypeError (엄격 모드)
function execute(callback) {
callback(); // 단독 호출 → 기본 바인딩
}
const counter = {
count: 0,
increment() {
this.count++;
console.log(this.count);
}
};
execute(counter.increment);
// NaN (비엄격 모드: this는 전역 객체, 전역에 count 없음)
// 또는 TypeError (엄격 모드: this는 undefined)
암시적 바인딩 손실은 코드가 오류 없이 실행되지만 값이 의도와 다르게 나오는 **조용한 버그(silent bug)**를 유발합니다. 이 때문에 원인을 찾기 어렵습니다.
심화
암시적 바인딩 손실은 ECMAScript의 GetValue 추상 연산이 Reference Record를 순수한 함수 값(function value)으로 변환하는 과정에서 [[Base]] 정보가 영구적으로 소실되는 것에서 기인합니다.
GetValue 연산과 [[Base]] 소실 메커니즘
ECMAScript 2023, Section 6.2.4.8 (GetValue)에 따르면, Reference Record ref에 GetValue를 적용하면 ref.[[Base]]의 [[Get]](ref.[[ReferencedName]], GetThisValue(ref))를 호출하여 순수한 ECMAScript 값을 반환합니다. 반환된 함수 값(Function Object)은 [[Base]] 정보를 포함하지 않습니다. 할당 표현식 const fn = obj.method에서 우변 평가 시 GetValue가 암묵적으로 호출되어 이 소실이 발생합니다.
식별자 참조 vs 프로퍼티 참조
이후 fn()을 호출하면 식별자 fn은 Section 6.2.4.4 (ResolveBinding)에 의해 환경 레코드를 base로 하는 Reference Record로 평가됩니다. IsPropertyReference(ref)가 false이고 ref.[[Base]]가 Environment Record이므로 ref.[[Base]].WithBaseObject()가 호출됩니다. 글로벌 환경에서는 전역 객체가, 모듈 환경에서는 undefined가 반환되어 기본 바인딩이 적용됩니다.
실무 패턴별 손실 발생 지점 분석
객체 구조 분해(const { method } = obj)는 obj.method에 GetValue를 적용하므로 할당과 동일한 손실이 발생합니다. 배열 콜백(arr.forEach(obj.method))에서는 인자 평가 시 GetValue가 적용되어 Array.prototype.forEach의 내부 호출(callback.call(undefined, ...))에서 기본 바인딩이 됩니다. 단, Array.prototype.forEach의 두 번째 인자 thisArg를 제공하면 callback.call(thisArg, ...)로 호출되어 명시적 바인딩이 적용됩니다.
콜백 전달 시 바인딩 손실 패턴
입문
setTimeout, 이벤트 핸들러, 배열 메서드에 객체의 메서드를 넘기면 this가 바뀌어버려요. 실무에서 가장 자주 만나는 this 버그예요.
⏰ setTimeout에 메서드를 넘기면
알람 앱을 만든다고 생각해봐요. clock.ring이라는 메서드를 1초 후에 실행하고 싶어서 setTimeout(clock.ring, 1000)이라고 썼어요. 그런데 1초 후에 실행될 때, ring 함수는 이미 clock 소속이 아니에요. 넘기는 순간 소속 정보가 사라지거든요. 그래서 this.sound 같은 값이 undefined가 돼요.
🖱️ 이벤트 핸들러에서도 같은 일이 생겨요
버튼에 클릭 핸들러를 연결할 때 button.addEventListener('click', this.handleClick) 처럼 쓰면, 클릭 이벤트가 발생할 때 this는 원래 객체가 아니라 클릭된 버튼 요소가 돼요. 함수를 이벤트 핸들러로 “등록”하는 순간 소속 정보가 날아가요.
🔄 forEach, map 같은 배열 메서드도 마찬가지예요
arr.forEach(this.process) 처럼 쓰면, process 함수가 실행될 때 this는 원래 객체가 아니에요. 배열 메서드에 두 번째 인자로 this를 전달하거나, 화살표 함수로 감싸서 해결할 수 있어요.
💡 공통적인 원인이 있어요
이 모든 경우의 공통점은 “함수를 넘기는 것”이에요. 함수가 어딘가로 넘어가는 순간, 객체 소속 정보가 사라져요. 이후에 배울 화살표 함수와 bind가 이 문제를 해결하는 핵심 도구예요.
중급
콜백으로 메서드를 전달하는 세 가지 주요 패턴에서 암시적 바인딩 손실이 발생합니다. 각 패턴의 동작과 원인을 이해해야 올바른 해결책을 선택할 수 있습니다.
const clock = {
time: '12:00',
ring() {
console.log(`알람: ${this.time}`); // this.time이 undefined
}
};
setTimeout(clock.ring, 1000);
// 1초 후: "알람: undefined"
// clock.ring이 콜백으로 전달되며 base(clock) 소실
const logger = {
prefix: '[LOG]',
items: ['a', 'b', 'c'],
print() {
this.items.forEach(function(item) {
console.log(`${this.prefix} ${item}`); // this.prefix가 undefined
});
}
};
logger.print();
// "[undefined] a", "[undefined] b", "[undefined] c"
// 해결 1: thisArg 전달
this.items.forEach(function(item) {
console.log(`${this.prefix} ${item}`);
}, this); // 두 번째 인자로 this 전달
콜백에서 바인딩 손실 문제의 주요 해결 방법:
bind(this)로 this를 고정한 새 함수 생성- 화살표 함수로 감싸기 (렉시컬 this 캡처)
forEach/map등의thisArg활용
이 해결책들은 이후 명시적 바인딩과 화살표 함수 섹션에서 상세히 다룹니다.
const clock = {
time: '12:00',
ring() {
console.log(`알람: ${this.time}`);
}
};
// bind로 this 고정
setTimeout(clock.ring.bind(clock), 1000); // "알람: 12:00"
// 화살표 함수로 감싸기
setTimeout(() => clock.ring(), 1000); // "알람: 12:00"
심화
콜백 패턴에서의 바인딩 손실은 각 API의 콜백 실행 메커니즘(Web API 타이머, DOM 이벤트 모델, ECMAScript 내장 배열 메서드)이 함수를 순수 값으로 저장하고 호출하는 방식에서 공통적으로 발생합니다.
setTimeout 콜백의 호출 메커니즘
HTML Living Standard의 타이머 알고리즘에 따르면, setTimeout(callback, delay)는 콜백을 태스크(task)로 등록할 때 callback을 순수한 함수 객체로 저장합니다. 타이머 만료 시 이벤트 루프가 태스크를 실행할 때 [[Call]](undefined, []) 방식으로 호출합니다. ECMAScript의 OrdinaryCallBindThis에서 비엄격 모드 함수는 undefined를 전역 객체로 coerce하고, 엄격 모드 함수는 undefined를 그대로 유지합니다.
DOM 이벤트 핸들러와 this
DOM Living Standard의 EventListener 호출 알고리즘에서, 이벤트 리스너 콜백은 callback.[[Call]](currentTarget, [event]) 방식으로 호출됩니다. 따라서 addEventListener에 전달된 일반 함수에서 this는 이벤트를 발생시킨 DOM 요소(currentTarget)가 됩니다. 이는 기본 바인딩이 아닌 DOM 표준이 명시적으로 지정한 암시적 바인딩의 변형이며, React의 합성 이벤트(SyntheticEvent) 시스템은 이와 달리 클래스 컴포넌트 핸들러를 undefined thisArgument로 호출하므로 별도의 바인딩이 필요합니다.
Array.prototype.forEach의 thisArg 메커니즘
ECMAScript 2023, Section 23.1.3.14 (Array.prototype.forEach)에 따르면, forEach(callbackfn, thisArg)는 콜백을 callbackfn.[[Call]](T, [kValue, k, O]) 방식으로 호출합니다. 여기서 T는 thisArg가 제공되면 그 값, 아니면 undefined입니다. undefined가 전달되면 OrdinaryCallBindThis의 비엄격 모드 coercion에 의해 전역 객체가 됩니다. thisArg를 활용하는 것은 명시적 바인딩을 내장 API 수준에서 제공하는 패턴으로, bind를 사용하는 것과 동일한 효과를 가집니다.