JavaScript this 바인딩 모든 경우의 수
맥락에 따라 달라지는 “나”
회사 동료에게 “저 분이 누구예요?”라고 물으면 “우리 팀 시니어 개발자요”라고 답하겠지만, 고향 친구 모임에서 같은 질문을 받으면 “내 고등학교 동창이야”라고 답합니다. 동일한 사람을 가리키는데, 어디서 어떻게 물어보느냐에 따라 답이 달라지는 것입니다.
자바스크립트의 this가 정확히 이 방식으로 동작합니다. 동일한 함수 코드라도, 어디서 어떻게 호출했느냐에 따라 this가 가리키는 대상이 완전히 달라집니다. 이것이 자바스크립트를 처음 배울 때 가장 혼란스러운 개념 중 하나인 이유입니다.
비유: 명함에는 “김개발”이라고만 써 있습니다. 회사에서 받으면 “개발팀 팀장”이고, 운동 동호회에서 받으면 “농구 주장”입니다. 명함(함수 코드)은 같은데 맥락(호출 방식)이
this를 결정합니다.
1번 다이어그램 - this 바인딩 결정 전체 지도
graph LR
A["함수 호출"] -->|"new"| B["새 객체"]
A -->|"call/bind"| C["명시적 값"]
A -->|"obj.method"| D["obj"]
A -->|"기본"| E["전역/undefined"]
이 다이어그램이 this의 모든 진실입니다. 복잡해 보이지만, 위에서 아래로 순서대로 적용하는 단순한 규칙입니다. this가 헷갈릴 때마다 이 순서도를 따라가면 반드시 답을 찾을 수 있습니다.
| 바인딩 종류 | 우선순위 | 조건 |
|---|---|---|
| new 바인딩 | 1 (최고) | new 키워드로 함수 호출 |
| 명시적 바인딩 | 2 | call, apply, bind 사용 |
| 암시적 바인딩 | 3 | obj.method() 형태로 호출 |
| 기본 바인딩 | 4 (최저) | 일반 함수 호출 |
2. 기본 바인딩 — 아무도 주인이 없을 때
가장 기본적인 경우입니다. 그냥 함수를 그냥 호출할 때 발동합니다.
비유: 길에서 혼자 걷다가 스스로에게 “나 누구야?”라고 물으면, “나는 지구라는 행성의 주민”이라는 아주 포괄적인 대답이 나옵니다. 자바스크립트에서 그 “지구”가
window전역 객체입니다.
function showThis() {
console.log(this);
}
showThis(); // 브라우저: window 객체, Node.js: global 객체
왜 전역 객체가 this가 될까요? 이유는 자바스크립트에서 전역에서 선언한 모든 것은 사실 전역 객체의 프로퍼티이기 때문입니다. showThis()는 사실 window.showThis()와 같고, 점 앞의 window가 this가 됩니다.
엄격 모드에서의 차이
'use strict';
function showThis() {
console.log(this); // undefined
}
showThis();
엄격 모드에서는 기본 바인딩이 window가 아닌 undefined가 됩니다. 왜냐하면 'use strict'는 “암묵적으로 전역을 가정하는 위험한 동작을 막겠다”는 선언이기 때문입니다. 만약 이걸 모르면 엄격 모드에서 this.name에 접근할 때 TypeError: Cannot read properties of undefined가 왜 뜨는지 이해할 수 없습니다.
중첩 함수에서의 함정 — 가장 흔한 버그
const person = {
name: '김철수',
greet() {
console.log(this.name); // '김철수' — 메서드 호출이므로 OK
function inner() {
console.log(this.name); // undefined! — 기본 바인딩 적용
}
inner(); // 주의: 메서드가 아닌 일반 함수 호출
}
};
person.greet();
비유: 팀장(greet)이 팀원(inner)을 불러서 업무를 지시하는데, 팀원은 “나는 누구 소속이지?”를 모릅니다. 왜냐하면 팀장이 팀원에게 정식으로 임명장을 주지 않고 구두로 부탁했기 때문입니다. 이것이 바로
this손실입니다.
만약 이걸 안 하면? inner() 안에서 this.name을 쓰면 undefined가 나오고, this.someMethod()를 호출하면 TypeError가 발생합니다. 오래된 코드베이스에서 자주 발생하는 버그입니다.
3. 암시적 바인딩 — 점(.) 앞이 주인
점(.) 앞에 있는 객체가 this가 됩니다. 가장 직관적인 규칙입니다.
const person = {
name: '이영희',
greet() {
console.log(`안녕하세요, ${this.name}입니다`);
}
};
person.greet(); // '안녕하세요, 이영희입니다' — person이 this
비유:
person.greet()는 “person이라는 사람에게 greet를 시켜라”입니다. 그러니 greet 안에서this는 당연히 그 사람, 즉person입니다.
암시적 바인딩 손실 — 가장 흔한 버그
여기서 가장 중요하고 가장 많이 실수하는 부분이 나옵니다.
const person = {
name: '이영희',
greet() {
console.log(`안녕하세요, ${this.name}입니다`);
}
};
// 함수를 변수에 할당하면 바인딩이 사라집니다!
const greetFn = person.greet;
greetFn(); // '안녕하세요, undefined입니다' — window가 this
왜 이런 일이 생길까요? person.greet를 변수에 담는 순간, 그것은 더 이상 “person의 greet”가 아니라 그냥 “greet 함수”입니다. 누구의 소속도 아닌 함수를 호출하면 기본 바인딩이 적용됩니다.
graph LR
A["person.greet()"] -->|"this=person"| B["'이영희' 출력"]
C["greetFn()"] -->|"this=전역"| D["undefined 출력"]
콜백으로 전달할 때의 함정
const button = {
label: '클릭',
handleClick() {
console.log(this.label); // undefined! button 객체가 아님
}
};
// 이벤트 리스너에 메서드 전달 — 바인딩 손실
document.getElementById('btn').addEventListener('click', button.handleClick);
// button.handleClick이 addEventListener에 의해 일반 함수로 호출됨
// this는 이벤트 타깃(버튼 DOM 요소)가 됨
이것을 모르면 React 클래스 컴포넌트나 일반 이벤트 핸들러에서 this가 왜 예상과 다른지 이해할 수 없습니다.
4. 명시적 바인딩 — 내가 직접 지정
call, apply, bind로 this를 직접 지정합니다. “이 함수를 실행할 때 this는 반드시 이것이어야 해”라고 강제하는 방식입니다.
비유: 임시직 직원(함수)을 여러 팀에 파견할 때, “오늘은 개발팀 소속으로 일해”(call), “내일은 디자인팀으로”(apply/bind)처럼 소속을 직접 지정합니다.
call() — 즉시 호출, 인자를 쉼표로 전달
function introduce(job, city) {
console.log(`저는 ${this.name}, ${job}, ${city} 거주입니다`);
}
const person = { name: '박지민' };
introduce.call(person, '개발자', '서울');
// '저는 박지민, 개발자, 서울 거주입니다'
apply() — 즉시 호출, 인자를 배열로 전달
introduce.apply(person, ['디자이너', '부산']);
// '저는 박지민, 디자이너, 부산 거주입니다'
call과 apply의 차이는 단순히 인자 전달 방식뿐입니다. 배열 형태로 이미 가지고 있다면 apply가 편하고, 개별 인자라면 call이 편합니다.
bind() — 새 함수 반환, 즉시 호출 안 함
const boundIntroduce = introduce.bind(person, '기획자');
// 나중에 호출
boundIntroduce('대전');
// '저는 박지민, 기획자, 대전 거주입니다'
bind는 즉시 실행하지 않고 this가 영구적으로 고정된 새 함수를 반환합니다. 이 함수는 나중에 아무리 다른 방식으로 호출해도 this가 바뀌지 않습니다.
graph LR
A["명시적 바인딩"] --> B["call(thisArg, a, b"]
A --> C["apply(thisArg, [a,"]
A --> D["bind(thisArg, a) -"]
실용적인 예제
// 유사 배열 객체에 배열 메서드 사용 (구형 코드에서 자주 보임)
function sum() {
// arguments는 배열처럼 생겼지만 배열이 아닙니다
const args = Array.prototype.slice.call(arguments);
return args.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// Math.max로 배열 최댓값 구하기
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
const max = Math.max.apply(null, numbers); // 9
// ES6 이후: Math.max(...numbers)로 더 간단하게
5. new 바인딩 — 새로 태어나는 객체
new 키워드로 함수를 호출하면 새 객체가 this가 됩니다. 이것이 가장 높은 우선순위를 가집니다.
비유: 공장에서 제품을 생산할 때(new), 새로 만들어진 제품 자체가 “나”(this)가 됩니다. 설계도(함수)는 같아도, 생산된 제품(인스턴스)은 각자 독립적입니다.
function Person(name, age) {
// new로 호출하면 자바스크립트가 내부적으로:
// 1. 새 빈 객체 {} 생성
// 2. this = 새 객체로 설정
this.name = name;
this.age = age;
// 3. 명시적 반환값이 없으면 this 반환
}
const kim = new Person('김민준', 25);
console.log(kim.name); // '김민준'
flowchart LR
A["new Person('김민준',"] --> B["새 빈 객체 {} 생성"]
B --> C["this = 새 객체로 설정"]
C --> D["생성자 함수 실행<br>this."]
D --> E["명시적 반환값 없으면<br>thi"]
E --> F["kim = { name: '김민준"]
style C fill:#e74c3c,color:#fff
style F fill:#2ecc71,color:#fff
만약 new를 빠뜨리면?
function Counter(start) {
this.count = start;
}
// new로 호출 — 올바른 사용
const counter1 = new Counter(0);
console.log(counter1.count); // 0
// new 없이 일반 호출 — 전역이 오염됩니다!
const counter2 = Counter(0); // undefined 반환
console.log(window.count); // 0 — window.count가 생겨버림!
이것을 모르면 왜 전역 변수가 갑자기 생기는지 이해할 수 없습니다. new를 빠뜨리면 this가 window가 되어 전역 오염이 발생합니다.
6. 화살표 함수 — 완전히 다른 세계
화살표 함수는 앞서 설명한 4가지 규칙을 전혀 따르지 않습니다. 화살표 함수에는 자신만의 this가 없습니다. 대신 정의된 시점의 바깥 스코프 this를 그대로 사용합니다.
비유: 일반 함수는 “내가 어디서 불렸느냐”에 따라 정체가 결정되는 용병입니다. 반면 화살표 함수는 “내가 어디서 태어났느냐”에 충성하는 세습 신하입니다. 태어난 순간의 주인(this)이 영구히 고정됩니다.
graph LR
A["화살표 함수"] --> B["자신의 this 없음"]
B --> C["렉시컬 스코프의 this 사용"]
C --> D["정의된 시점의 this 캡처"]
D --> E["call/apply/bind로 변"]
style A fill:#9b59b6,color:#fff
style E fill:#e74c3c,color:#fff
const person = {
name: '최수영',
greetLater() {
// 일반 함수: 콜백으로 전달되면 this를 잃어버림
setTimeout(function() {
console.log(this.name); // undefined
}, 1000);
// 화살표 함수: 바깥(greetLater)의 this를 그대로 캡처
setTimeout(() => {
console.log(this.name); // '최수영'
}, 1000);
}
};
person.greetLater();
왜 화살표 함수가 이렇게 동작할까요? 이유는 콜백 안에서 this를 잃어버리는 문제가 너무 자주 발생했기 때문입니다. 화살표 함수는 그 문제를 해결하기 위해 설계된 문법입니다.
화살표 함수를 쓰면 안 되는 곳
const obj = {
name: '테스트',
// 잘못된 사용: 메서드로 화살표 함수 사용
greet: () => {
console.log(this.name); // undefined — 전역 this를 캡처함
},
// 올바른 사용: 메서드는 일반 함수
greetCorrect() {
console.log(this.name); // '테스트'
}
};
// 이벤트 핸들러에서도 주의
button.addEventListener('click', () => {
console.log(this); // window — 이벤트 타깃이 아님
});
button.addEventListener('click', function() {
console.log(this); // button — 올바른 이벤트 타깃
});
비유: 화살표 함수는 세습 신하이기 때문에, 새 주인(객체)을 섬기는 메서드로는 어울리지 않습니다. 메서드는 객체에 속해야 하므로 일반 함수를 사용해야 합니다.
7. 이벤트 핸들러에서의 this
이벤트 핸들러에서 this는 특별한 규칙이 있습니다. addEventListener로 등록된 핸들러에서 this는 이벤트가 발생한 DOM 요소가 됩니다.
const handler = {
prefix: '[공지]',
handleClick: function(event) {
console.log(this); // <button> DOM 요소 — handler 객체가 아님!
console.log(this.prefix); // undefined
}
};
const button = document.getElementById('btn');
// 문제: this가 button DOM 요소가 됨
button.addEventListener('click', handler.handleClick);
// 해결 1: bind 사용
button.addEventListener('click', handler.handleClick.bind(handler));
// 해결 2: 화살표 함수 래퍼
button.addEventListener('click', (e) => handler.handleClick(e));
// 해결 3: 클래스 필드 화살표 함수 (React 클래스 컴포넌트 방식)
class MyComponent {
prefix = '[공지]';
handleClick = () => {
console.log(this.prefix); // 항상 '[공지]' — 화살표 함수이므로 this 고정
};
}
8. 클래스에서의 this
ES6 클래스에서의 this는 인스턴스를 가리킵니다. 단, 클래스는 자동으로 엄격 모드이므로 바인딩이 손실되면 undefined가 됩니다.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name}이(가) 소리를 냅니다`);
}
}
class Dog extends Animal {
speak() {
super.speak();
console.log(`${this.name}: 왈왈!`);
}
}
const dog = new Dog('멍멍이');
dog.speak();
// '멍멍이이(가) 소리를 냅니다'
// '멍멍이: 왈왈!'
// 바인딩 손실 주의!
const speak = dog.speak;
speak(); // TypeError: Cannot read properties of undefined
// 클래스는 엄격 모드 → this = undefined
이것을 모르면 React 클래스 컴포넌트에서 이벤트 핸들러를 등록할 때 왜 this.setState is not a function 에러가 나는지 이해할 수 없습니다.
9. React 클래스 컴포넌트에서의 this 바인딩 문제
클래스형 컴포넌트를 사용하던 시절 가장 흔한 실수였습니다. 현재는 함수형 컴포넌트가 표준이지만, 레거시 코드를 읽으려면 반드시 알아야 합니다.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// 방법 1: constructor에서 bind
this.handleClick1 = this.handleClick1.bind(this);
}
handleClick1() {
// bind 하지 않으면 this가 undefined (클래스는 엄격 모드)
this.setState({ count: this.state.count + 1 });
}
// 방법 2: 화살표 함수 클래스 필드 (가장 권장)
handleClick2 = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<button onClick={this.handleClick1}>클릭1 (bind)</button>
<button onClick={this.handleClick2}>클릭2 (화살표)</button>
{/* 방법 3: 인라인 화살표 — 매 렌더링마다 새 함수 생성 (성능상 비권장) */}
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
클릭3 (인라인)
</button>
</div>
);
}
}
2번 다이어그램 - this 고정하는 3가지 패턴
this 손실 문제를 해결하는 세 가지 방법을 비교합니다.
graph LR
PROBLEM["this 손실"] --> P1["클로저: self=this"]
PROBLEM --> P2["bind(this) 사용"]
PROBLEM --> P3["화살표 함수 (권장)"]
style P3 fill:#9b59b6,color:#fff
const service = {
baseUrl: 'https://api.example.com',
// 패턴 1: 클로저로 this 보존 (구식 방법)
fetchData_closure() {
const self = this; // this를 변수에 저장
setTimeout(function() {
console.log(self.baseUrl); // self 참조
}, 1000);
},
// 패턴 2: bind 사용
fetchData_bind() {
setTimeout(function() {
console.log(this.baseUrl);
}.bind(this), 1000);
},
// 패턴 3: 화살표 함수 (권장)
fetchData_arrow() {
setTimeout(() => {
console.log(this.baseUrl); // 렉시컬 this — 깔끔
}, 1000);
}
};
10. this 디버깅 — 실전 퀴즈
직접 예측해 보세요.
퀴즈 1
var name = '전역';
const obj = {
name: '객체',
arr: [1, 2, 3],
printNames() {
this.arr.forEach(function(item) {
console.log(this.name, item); // ??
});
this.arr.forEach((item) => {
console.log(this.name, item); // ??
});
}
};
obj.printNames();
정답:
- 일반 함수 forEach 콜백:
전역 1,전역 2,전역 3(this = window) - 화살표 함수 forEach 콜백:
객체 1,객체 2,객체 3(this = obj)
왜냐하면 일반 함수는 forEach에 의해 일반 호출되므로 기본 바인딩(window)이 적용되고, 화살표 함수는 printNames 메서드의 렉시컬 this인 obj를 캡처하기 때문입니다.
퀴즈 2
function makeAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = makeAdder(5);
const calculator = {
value: 100,
add5: add5,
addToValue(n) {
return this.value + n;
}
};
console.log(calculator.add5(3)); // ??
console.log(calculator.addToValue(3)); // ??
정답:
calculator.add5(3):8— 클로저로x=5를 기억,this와 무관calculator.addToValue(3):103—this = calculator,value = 100
11번 다이어그램 - this 결정 최종 알고리즘
graph LR
A["this 결정"] -->|"화살표"| B["렉시컬 고정"]
A -->|"new"| C["새 객체"]
A -->|"call/bind"| D["지정 값"]
A -->|"obj.method"| E["obj"]
이 5단계 체크리스트만 있으면 어떤 this 문제도 해결할 수 있습니다.
- 화살표 함수: 정의된 곳의
this를 그대로 사용 (변경 불가) - new: 새 객체가
this - call/apply/bind: 내가 지정한 것이
this - obj.fn(): obj가
this - fn(): window (엄격 모드: undefined)
this가 헷갈릴 때마다 위 순서대로 체크해 보세요. “이 함수가 화살표 함수인가? → new인가? → call/apply/bind인가? → 점 앞에 뭔가 있나? → 없으면 전역” — 이 5개 질문으로 반드시 답을 찾을 수 있습니다.
왜 동적 this 바인딩인가?
| 방식 | this 결정 시점 | 특징 | 사용 언어/방식 |
|---|---|---|---|
| JS 동적 바인딩 | 호출 시점 | 유연하지만 예측 어려움 | JavaScript (일반 함수) |
| 화살표 함수 | 정의 시점 (렉시컬) | 예측 가능, 메서드엔 부적합 | JavaScript (ES6+) |
| Python self | 항상 명시적 전달 | 명확하지만 장황 | Python |
| Java this | 항상 인스턴스 고정 | 단순, 유연성 없음 | Java |
동적 바인딩은 동일한 메서드를 여러 객체가 공유할 수 있어 유연하지만, 콜백 등 맥락이 바뀌는 상황에서 버그를 만들기 쉽습니다. 화살표 함수와 bind로 명시적 바인딩을 적극 활용하는 것이 현대 JavaScript의 정석입니다.
실무에서 자주 하는 실수
실수 1. 콜백에서 this 소실
class Timer {
constructor() { this.count = 0; }
start() {
setInterval(function() {
this.count++; // this는 window (또는 undefined in strict mode)
}, 1000);
}
}
// 올바른 방법 1: 화살표 함수
start() {
setInterval(() => { this.count++; }, 1000); // 외부 this 캡처
}
// 올바른 방법 2: bind
start() {
setInterval(function() { this.count++; }.bind(this), 1000);
}
실수 2. 메서드를 변수에 할당할 때 this 소실
const obj = { name: 'Kim', greet() { return this.name; } };
const greet = obj.greet; // 메서드 참조만 꺼냄
greet(); // undefined — this가 전역(또는 undefined)
// 올바른 방법: bind로 고정
const greet = obj.greet.bind(obj);
greet(); // 'Kim'
실수 3. 화살표 함수를 객체 메서드로 사용
const obj = {
name: 'Kim',
greet: () => { return this.name; } // this는 외부(전역) 스코프
};
obj.greet(); // undefined
// 올바른 방법: 일반 함수 메서드
const obj = {
name: 'Kim',
greet() { return this.name; } // 호출 시 obj가 this
};
실수 4. 이벤트 핸들러에서 화살표 함수와 일반 함수 혼동
// 일반 함수: this = 이벤트 타겟 요소
button.addEventListener('click', function() {
console.log(this); // <button>
});
// 화살표 함수: this = 외부 스코프 (컴포넌트 인스턴스 등)
button.addEventListener('click', () => {
console.log(this); // 외부 this (클래스라면 인스턴스)
});
// React 클래스 컴포넌트 핸들러에는 화살표 함수가 올바른 선택
실수 5. call/apply/bind 혼동
function greet(greeting, punct) {
return `${greeting}, ${this.name}${punct}`;
}
const obj = { name: 'Kim' };
greet.call(obj, 'Hello', '!'); // 즉시 실행, 인수 나열
greet.apply(obj, ['Hello', '!']); // 즉시 실행, 인수 배열
const bound = greet.bind(obj, 'Hello'); // 새 함수 반환, 나중에 실행
bound('!'); // 'Hello, Kim!'
면접 포인트
Q. 화살표 함수의 this가 일반 함수와 다른 이유를 설명하세요.
일반 함수는 this가 호출 시점에 동적으로 결정됩니다. 화살표 함수는 this가 없어 자신을 감싼 외부 스코프의 this를 렉시컬로 캡처합니다. 이를 “렉시컬 this”라고 합니다. 화살표 함수는 call, apply, bind로 this를 바꿀 수 없습니다.
Q. React 클래스 컴포넌트에서 핸들러를 바인딩하는 세 가지 방법과 각 단점은?
(1) 생성자에서 this.handleClick = this.handleClick.bind(this) — 보일러플레이트 많음. (2) 화살표 함수 클래스 필드 handleClick = () => {} — 인스턴스마다 함수 생성 (메모리). (3) JSX 인라인 onClick={() => this.handleClick()} — 렌더마다 새 함수 생성, 자식 리렌더 유발. 현대 React에서는 함수형 컴포넌트 + useCallback이 표준입니다.
Q. call, apply, bind의 차이와 각각의 사용 시나리오는?
call은 즉시 실행하며 인수를 쉼표로 나열합니다. apply는 즉시 실행하며 인수를 배열로 전달합니다 (스프레드 이전에 가변 인수 전달에 활용). bind는 새 함수를 반환하며 나중에 실행합니다. 이벤트 핸들러 등록, 부분 적용(Partial Application)에 활용합니다.
Q. strict mode에서 this의 동작이 다른 이유는?
strict mode가 아닐 때 일반 함수 호출에서 this는 전역 객체(window)입니다. strict mode에서는 this가 undefined입니다. 이는 의도치 않은 전역 오염을 방지하기 위한 설계입니다. ES6 모듈은 기본적으로 strict mode이므로 모듈에서 일반 함수의 this는 항상 undefined입니다.
Q. 다음 코드의 출력을 예측하세요.
const obj = {
name: 'A',
outer() {
const inner = () => { console.log(this.name); };
inner();
}
};
obj.outer(); // 'A' — 화살표 inner는 outer의 this(obj)를 캡처
const fn = obj.outer;
fn(); // undefined — fn() 호출 시 this는 전역, outer의 this가 전역이 됨
호출 체인 전체를 추적해 this를 결정해야 합니다.
댓글