JavaScript의 this 바인딩: 상황별 동작 원리
JavaScript를 학습하면서 가장 헷갈리는 개념 중 하나가 바로 this
입니다. 다른 언어와 달리 JavaScript의 this
는 함수가 호출되는 방식에 따라 값이 달라지기 때문에 많은 개발자들이 어려움을 겪습니다. 이 글에서는 다양한 상황에서 this
가 어떻게 바인딩되는지 6가지 핵심 상황을 통해 자세히 알아보겠습니다.
this 바인딩의 기본 원리
JavaScript에서 this
는 함수가 호출되는 방식에 따라 결정됩니다. 이는 함수가 정의된 위치가 아닌, 실행되는 순간의 호출 방식이 중요하다는 의미입니다. 마치 전화를 걸 때 상황에 따라 다른 사람과 연결되는 것과 비슷합니다.
1. 전역 호출 (Global Invocation)
전역에서 함수가 호출되면, this
는 전역 객체를 참조합니다.
function globalFunc() {
console.log(this);
}
globalFunc(); // 브라우저: window, Node.js: global
실행 환경별 차이점
환경 | this 값 | 설명 |
---|---|---|
브라우저 | window |
DOM과 관련된 모든 전역 객체 |
Node.js | global |
Node.js 런타임 전역 객체 |
Strict Mode | undefined |
엄격 모드에서는 전역 객체 대신 undefined |
"use strict";
function strictFunc() {
console.log(this); // undefined
}
strictFunc();
2. 메서드 호출 (Method Invocation)
객체의 메서드로 호출된 함수에서는 this
가 해당 객체를 참조합니다. 이는 가장 직관적인 동작 방식입니다.
const user = {
name: "김철수",
age: 25,
greet: function() {
console.log(`안녕하세요, 저는 ${this.name}입니다.`);
console.log(`나이는 ${this.age}살입니다.`);
}
};
user.greet();
// 출력: 안녕하세요, 저는 김철수입니다.
// 출력: 나이는 25살입니다.
중첩 객체에서의 this
const company = {
name: "테크 회사",
department: {
name: "개발팀",
introduce: function() {
console.log(`${this.name}입니다.`); // "개발팀입니다."
}
}
};
company.department.introduce();
3. 생성자 함수와 클래스 (Constructor & Class)
생성자 함수나 클래스에서 this
는 새로 생성되는 객체(인스턴스)를 참조합니다.
생성자 함수
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce = function() {
console.log(`저는 ${this.name}이고, ${this.age}살입니다.`);
};
}
const person1 = new Person("이영희", 28);
const person2 = new Person("박민수", 32);
person1.introduce(); // 저는 이영희이고, 28살입니다.
person2.introduce(); // 저는 박민수이고, 32살입니다.
ES6 클래스
class Developer {
constructor(name, language) {
this.name = name;
this.language = language;
}
coding() {
console.log(`${this.name}이 ${this.language}로 코딩합니다.`);
}
}
const developer = new Developer("김개발", "JavaScript");
developer.coding(); // 김개발이 JavaScript로 코딩합니다.
4. 명시적 바인딩 (Explicit Binding)
call()
, apply()
, bind()
메서드를 사용하면 this
를 명시적으로 설정할 수 있습니다.
call() 메서드
function greet() {
console.log(`안녕하세요, ${this.name}님!`);
}
const user1 = { name: "홍길동" };
const user2 = { name: "김철수" };
greet.call(user1); // 안녕하세요, 홍길동님!
greet.call(user2); // 안녕하세요, 김철수님!
apply() 메서드
function introduce(job, city) {
console.log(`${this.name}은 ${city}에서 ${job}으로 일합니다.`);
}
const person = { name: "이개발" };
introduce.apply(person, ["개발자", "서울"]);
// 이개발은 서울에서 개발자로 일합니다.
bind() 메서드
function sayHello() {
console.log(`Hello, ${this.name}!`);
}
const user = { name: "Alice" };
const boundSayHello = sayHello.bind(user);
boundSayHello(); // Hello, Alice!
메서드 비교표
메서드 | 즉시 실행 | 인자 전달 방식 | 반환값 |
---|---|---|---|
call() |
✅ | 개별 인자 | 함수 실행 결과 |
apply() |
✅ | 배열 형태 | 함수 실행 결과 |
bind() |
❌ | 개별 인자 | 새로운 함수 |
5. 화살표 함수 (Arrow Function)
화살표 함수는 자체적인 this
를 가지지 않고, 상위 스코프의 this
를 상속받습니다. 이는 화살표 함수의 가장 중요한 특징입니다.
일반 함수 vs 화살표 함수
const obj = {
name: "테스트",
regularFunction: function() {
console.log("일반 함수:", this.name); // "테스트"
},
arrowFunction: () => {
console.log("화살표 함수:", this.name); // undefined (전역 this)
}
};
obj.regularFunction(); // 일반 함수: 테스트
obj.arrowFunction(); // 화살표 함수: undefined
실무에서 유용한 활용
class EventHandler {
constructor() {
this.message = "클릭되었습니다!";
}
setupEventListener() {
// 화살표 함수를 사용하여 this 바인딩 유지
document.getElementById("btn").addEventListener("click", () => {
console.log(this.message); // "클릭되었습니다!"
});
}
}
const handler = new EventHandler();
handler.setupEventListener();
6. DOM 이벤트 핸들러 (DOM Event Handler)
DOM 요소의 이벤트 핸들러에서 this
는 기본적으로 이벤트를 발생시킨 요소를 참조합니다.
일반 함수 이벤트 핸들러
const button = document.getElementById("myButton");
button.addEventListener("click", function() {
console.log(this); // 클릭된 button 요소
console.log(this.textContent); // 버튼의 텍스트
this.style.backgroundColor = "blue"; // 버튼 색상 변경
});
화살표 함수 이벤트 핸들러
const button = document.getElementById("myButton");
button.addEventListener("click", () => {
console.log(this); // 상위 스코프의 this (보통 window)
// 이벤트 객체를 통해 요소에 접근해야 함
});
// 이벤트 객체 활용
button.addEventListener("click", (event) => {
console.log(event.target); // 클릭된 button 요소
});
실무 팁: 이벤트 핸들러 패턴
class ButtonController {
constructor() {
this.clickCount = 0;
}
handleClick() {
this.clickCount++;
console.log(`클릭 횟수: ${this.clickCount}`);
}
init() {
const button = document.getElementById("counter");
// bind 사용
button.addEventListener("click", this.handleClick.bind(this));
// 또는 화살표 함수 사용
button.addEventListener("click", () => this.handleClick());
}
}
const controller = new ButtonController();
controller.init();
this 바인딩 우선순위
JavaScript에서 this
바인딩에는 우선순위가 있습니다.
- 명시적 바인딩 (
call
,apply
,bind
) - new 바인딩 (생성자 함수)
- 암시적 바인딩 (메서드 호출)
- 기본 바인딩 (전역 호출)
function test() {
console.log(this.name);
}
const obj = { name: "객체" };
const boundTest = test.bind({ name: "바인딩" });
// 명시적 바인딩이 암시적 바인딩보다 우선
obj.test = boundTest;
obj.test(); // "바인딩" (명시적 바인딩 우선)
실무에서 자주 발생하는 this 문제와 해결책
문제 1: 콜백 함수에서 this 손실
// 문제 상황
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// 이렇게 하면 this가 전역 객체를 참조
setInterval(function() {
this.seconds++; // TypeError: Cannot read property 'seconds' of undefined
console.log(this.seconds);
}, 1000);
}
}
// 해결책 1: bind 사용
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
}
}
// 해결책 2: 화살표 함수 사용
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
문제 2: 배열 메서드에서 this 손실
// 문제 상황
const calculator = {
numbers: [1, 2, 3, 4, 5],
multiplier: 2,
calculate() {
return this.numbers.map(function(num) {
return num * this.multiplier; // this.multiplier is undefined
});
}
};
// 해결책 1: thisArg 매개변수 사용
const calculator = {
numbers: [1, 2, 3, 4, 5],
multiplier: 2,
calculate() {
return this.numbers.map(function(num) {
return num * this.multiplier;
}, this); // 두 번째 인자로 this 전달
}
};
// 해결책 2: 화살표 함수 사용
const calculator = {
numbers: [1, 2, 3, 4, 5],
multiplier: 2,
calculate() {
return this.numbers.map(num => num * this.multiplier);
}
};
마무리
JavaScript의 this
바인딩은 함수 호출 방식에 따라 동적으로 결정되는 특별한 개념입니다. 이를 제대로 이해하기 위해서는 다음 핵심 원칙들을 기억해야 합니다.
핵심 정리
- 호출 방식이 전부:
this
는 함수가 정의된 곳이 아닌, 호출되는 방식에 따라 결정됩니다. - 화살표 함수는 예외: 상위 스코프의
this
를 상속받아 바인딩이 고정됩니다. - 명시적 바인딩 활용:
call
,apply
,bind
를 활용하여this
를 명시적으로 제어할 수 있습니다. - 우선순위 이해: 명시적 바인딩 > new 바인딩 > 암시적 바인딩 > 기본 바인딩 순으로 우선순위가 적용됩니다.
this
바인딩을 완전히 이해하면 JavaScript의 객체 지향 프로그래밍과 함수형 프로그래밍을 더욱 효과적으로 활용할 수 있습니다. 실무에서는 화살표 함수와 명시적 바인딩을 적절히 조합하여 this
관련 문제를 예방하는 것이 중요합니다.