자바스크립트를 배우다 보면 누구나 한 번쯤은 this 때문에 머리를 싸매게 됩니다.
어떤 때는 객체를 가리키고, 어떤 때는 전역 객체를 가리키고, 심지어는 undefined가 되기도 하죠.
도대체 this는 어떤 규칙으로 바인딩되는 걸까요?
이 글에서는 자바스크립트의 this 바인딩 원칙과 상황별 동작 방식을 체계적으로 정리해보겠습니다.
자바스크립트의 this는 실행되는 환경에 따라 기본적으로 달라지는 특성을 가지고 있습니다.
대표적인 예로, 브라우저와 Node.js에서는 전역 컨텍스트에서의 this가 서로 다른 객체를 가리킵니다.
console.log(this); // window 객체가 출력됨
console.log(window); // window 객체가 출력됨
console.log(this === window); // true
console.log(this); // global 객체가 출력됨
console.log(global); // global 객체가 출력됨
console.log(this === global); // true
자바스크립트에서 함수와 메서드는 겉보기에는 유사하지만, 호출 방식과 내부의 this 바인딩에서 중요한 차이가 있습니다.
함수는 독립적으로 정의되고 실행되며, 자신을 호출한 객체가 없는 경우에는 기본적으로 전역 객체를 this로 가집니다.
strict mode에서는 undefined
function slow() {
console.log(this);
}
slow(); // 브라우저: window, Node.js: global
반면, 메서드는 객체의 프로퍼티로 정의된 함수로, 해당 객체를 통해 호출될 때 그 객체 자체가 this로 바인딩됩니다.
const user = {
name: "Alice",
sayHello: function () {
console.log(this.name);
}
};
user.sayHello(); // Alice
var func = function (x) {
console.log(this, x);
}
var obj = {
method: func,
};
obj.method(2); // { method: [Function method] } 2
여기서 핵심은 호출 방식입니다.
함수() 처럼 독립적으로 호출하면, this는 전역 객체객체.메서드() 처럼 객체의 메서드로 호출하면, this는 그 객체즉, 함수를 어떻게 호출했느냐에 따라 this가 참조하는 대상이 달라지게 됩니다.
이처럼 호출 방식이
this를 결정하는 핵심 요소라면,
그 호출을 "누가 했는지", 즉 호출의 주체를 파악하는 것이 매우 중요합니다.
다음 예제에서는 객체 내부의 메서드를 호출할 때 this가 어떻게 동작하는지를 확인해 보겠습니다.
var obj = {
methodA: function () { console.log(this); },
inner: {
methodB: function() { console.log(this); }
}
};
obj.methodA(); // this === obj
obj['methodA'](); // this === obj
obj.inner.methodB(); // this === obj.inner
obj.inner['methodB'](); // this === obj.inner
/* 호출의 다른 경우의 수는 생략함 */
위와 같이, 객체의 메서드로 함수를 호출할 경우 this는 그 메서드를 호출한 객체를 참조하게 됩니다.
이제 객체를 통해 호출하지 않고, 일반 함수처럼 호출하는 경우에는 this가 어떻게 바뀌는지 살펴보겠습니다.
객체를 통해 호출되는 메서드와 달리, 함수를 그 자체로 호출하는 경우, 해당 함수는 "호출 주체가 없는 상태"에서 실행됩니다.
프롤로그에서 일반 함수를 호출할 경우 this가 전역 객체를 참조한다는 것을 살펴봤습니다.
이번에는 그 개념을 실제 코드에 적용해 보겠습니다.
var obj1 = {
outer: function () {
console.log(this); // this === obj1
var innerFunc = function () {
console.log(this); // this === 전역 객체 (브라우저: window)
};
innerFunc(); // 독립 호출 → 전역 객체
},
};
obj1.outer();
위 코드에서 outer는 obj1 객체의 메서드로 호출되었기 때문에, 그 안에서의 this는 obj1을 가리킵니다. 하지만 outer 내부에서 정의된 innerFunc는 객체의 메서드가 아니라 일반 함수로 선언되었고, 이후 별도의 객체 없이 독립적으로 호출되고 있습니다. 이 경우 자바스크립트는 호출 주체를 찾을 수 없기 때문에, this는 전역 객체에 바인딩됩니다. 브라우저 환경에서는 window, Node.js에서는 global이 되죠.
내부 함수가 메서드 안에 정의되어 있기 때문에 this도 바깥 메서드처럼 동작할 것이라 생각하기 쉽지만, 실제로는 호출 방식이 다르기 때문에 this가 달라집니다.
함수가 어디에서 정의되었는지는 중요하지 않고, 어떻게 호출되었는지가 this를 결정짓는 핵심이라는 점을 꼭 기억해야 합니다.
앞서 우리는 일반 함수에서 this가 전역 객체를 참조하는 상황을 확인했습니다.
이번에는 이런 한계를 우회하거나 해결하는 방법들을 살펴보겠습니다.
일반 함수에서 this를 잃어버리는 대표적인 상황은 메서드 내부의 중첩 함수 호출입니다.
이 문제를 해결하는 가장 고전적인 방법은 this 값을 변수에 저장해두는 방식입니다.
var obj1 = {
outer: function () {
console.log(this); // obj1
// AS-IS
var innerFunc1 = function () {
console.log(this); // 전역 객체
};
innerFunc1();
// TO-BE
var self = this;
var innerFunc2 = function () {
console.log(self); // obj1
};
innerFunc2();
},
};
obj1.outer();
중첩 함수에서 this를 직접 사용하면 전역 객체를 가리키게 되지만,
바깥 메서드에서 this를 self라는 변수에 저장하고 내부 함수에서 참조하면
우회적으로 바깥의 this를 유지할 수 있습니다.
ES6에서 도입된 화살표 함수는 this를 바인딩하지 않는 함수입니다.
즉, 함수가 실행될 때 this를 새로 바인딩하지 않고,
바깥 스코프에 있는 this를 그대로 따라갑니다.
var obj = {
outer: function () {
console.log(this); // obj
var innerFunc = () => {
console.log(this); // obj
};
innerFunc();
},
};
obj.outer();
이 예제에서 innerFunc는 화살표 함수이므로 this를 바인딩하지 않습니다.
결과적으로 outer 함수의 this인 obj를 그대로 참조합니다.
이 방식은 중첩 함수 내에서 this 문제를 해결할 수 있는 간결하고 현대적인 방법입니다.
콜백 함수 역시 일반 함수이기 때문에, 별도로 this를 지정하지 않으면 전역 객체를 참조합니다.
setTimeout(function () {
console.log(this); // 전역 객체
}, 300);
[1, 2, 3].forEach(function (x) {
console.log(this, x); // 전역 객체
});
setTimeout이나 Array.prototype.forEach는 콜백을 호출할 때
특정한 this를 지정하지 않기 때문에, 내부의 this는 전역 객체가 됩니다.
하지만 모든 콜백 함수가 this를 잃는 것은 아닙니다.
addEventListener와 같이 this를 명시적으로 바인딩하는 메서드도 존재합니다.
document.body.innerHTML += '<button id="a">클릭</button>';
document.querySelector('#a').addEventListener('click', function (e) {
console.log(this, e); // this === button
});
이 경우 this는 이벤트를 바인딩한 DOM 요소(button)를 가리킵니다.
addEventListener는 내부적으로 콜백을 호출할 때 자신의 컨텍스트(this)를 유지하도록 설계되어 있기 때문입니다.
자바스크립트에서 생성자 함수는 new 키워드와 함께 호출되어
새로운 객체를 생성합니다. 이때의 this는 생성될 인스턴스를 가리킵니다.
var Cat = function (name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
var choco = new Cat('초코', 7); // this === choco
var nabi = new Cat('나비', 5); // this === nabi
위 코드에서 Cat은 생성자 함수이고, new Cat()을 통해 각각, choco, nabi라는 인스턴스가 생성됩니다.
생성자 함수 내부의 this는 해당 인스턴스를 가리키며, 프로퍼티를 설정하는 데 사용됩니다.
call, apply, bind
자바스크립트에서 this는 호출 방식에 따라 결정되지만, 경우에 따라선 직접 내가 원하는 객체로 this를 바인딩해야 할 일이 생깁니다.
이럴 때 사용할 수 있는 대표적인 메서드가 바로 call(), apply(), bind()입니다.
this를 직접 지정하고, 즉시 실행합니다.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
// 기본 호출 - this는 전역 객체
func(1, 2, 3);
// 명시적 바인딩
func.call({ x: 1 }, 1, 2, 3); // this === { x: 1 }
call은 첫 번째 인자로 this로 사용할 객체를 받고, 이후 인자들은 함수에 전달할 실제 인자입니다.
메서드에도 똑같이 적용할 수 있습니다.
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
}
};
obj.method(2, 3); // 1 2 3
obj.method.call({ a: 4 }, 5, 6); // 4 5 6
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
function Student(name, gender, school) {
Person.call(this, name, gender);
this.school = school;
}
function Employee(name, gender, company) {
Person.call(this, name, gender);
this.company = company;
}
중복되는 속성 초기화를 Person으로 분리하고, call을 통해 공통 코드를 재사용하는 방식입니다.
call과 거의 같지만 인자를 배열로 전달합니다.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func.apply({ x: 1 }, [1, 2, 3]); // this === { x: 1 }
apply는 call과 거의 동일하지만, 함수 인자들을 배열로 받는다는 점에서만 다릅니다.
가변 인자 함수에 인자를 묶어서 전달할 때 매우 유용합니다.
const numbers = [3, 7, 2, 9, 5];
// 잘못된 방식 - 배열 그대로 전달
console.log(Math.max(numbers)); // NaN
// apply를 사용하여 배열을 펼쳐 전달
console.log(Math.max.apply(null, numbers)); // 9
위 코드와 같이, 인자의 개수가 정해져 있지 않고 배열로 받는 값을 그대로 함수에 넘겨줘야 하는 상황에서는 apply가 훨씬 유용하다는 것이 명확히 드러납니다.
apply 대신 ... 스프레드 연산자 (ES6+)를 사용하면 더 간편합니다.
apply는 ES5 환경이나 고전 코드에서 여전히 유효한 해결책입니다.
console.log(Math.max(...numbers)); // 9
this가 고정된 새 함수를 만들어 반환합니다.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8
부분 적용도 가능합니다.
var bindFunc2 = func.bind({ x: 1 }, 9, 10);
bindFunc2(11, 12); // { x: 1 } 9 10 11 12
함수 이름도 바뀌어 추적하기 쉽습니다.
console.log(bindFunc1.name); // bound func
자바스크립트에서 함수 내부적으로 name이라는 읽기 전용 속성을 가지고 있습니다.
function greet() {}
console.log(greet.name); // "greet"
이름이 있는 함수는 name 속성으로 그 이름을 참조할 수 있고, 익명 함수는 "" (빈 문자열) 또는 상황에 따라 엔진이 자동으로 붙인 이름을 가질 수 있습니다.
bind()는 기존 함수를 기반으로 새로운 함수를 반환합니다.
이 새함수는 다음 두 가지 특징을 가집니다.
this가 고정되어 있음name이 "bound "로 시작하는 이름을 가진다.function sayHello() {
console.log('hello');
}
const boundFunc = sayHello.bind(null);
console.log(sayHello.name); // "sayHello"
console.log(boundFunc.name); // "bound sayHello"
이름을 통해 디버깅하거나 로깅 시 함수 추적이 쉬워집니다.
예를 들어, 여러 개의 콜백이나 이벤트 핸들러를 등록했을 때, bound ... 형태의 이름이 있으면 어떤 함수가 바인딩된 것인지 쉽게 구분할 수 있습니다.
중첩 함수나 콜백 안에서 this가 전역 객체로 바인딩되는 문제는 매우 흔합니다.
이를 해결하기 위해 call, bind는 다음과 같이 응용됩니다.
var obj = {
outer: function () {
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this); // this === obj
}
};
obj.outer();
var obj = {
outer: function () {
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc(); // this === obj
}
};
obj.outer();
setTimeout이나 forEach 등은 내부 콜백 호출 시 this를 별도로 넘겨주지 않기 때문에, 전역 객체가 this로 설정됩니다.
setTimeout(function () {
console.log(this); // 전역 객체
}, 300);
[1, 2, 3].forEach(function (x) {
console.log(this, x); // 전역 객체
});
이 문제는 bind를 활용해 쉽게 해결할 수 있습니다.
var obj = {
logThis: function () {
console.log(this);
},
logThisLater: function () {
setTimeout(this.logThis.bind(this), 1000); // this === obj
}
};
obj.logThisLater();
this를 바인딩하지 않는 특성 때문에, 화살표 함수는 this 바인딩 문제를 깔끔하게 해결해줍니다.
var obj = {
outer: function () {
var innerFunc = () => {
console.log(this); // this === obj
};
innerFunc();
}
};
obj.outer();
화살표 함수는 실행될 때 this를 새로 바인딩하지 않고, 상위 스코프의 this를 그대로 사용합니다.
그래서 콜백이나 중첩 함수에서 매우 유용합니다.
| 방식 | 특징 |
|---|---|
call(thisArg, ...args) | this를 지정해 함수 즉시 호출 |
apply(thisArg, [args]) | call과 동일, 인자를 배열로 전달 |
bind(thisArg, ...args) | this를 고정한 새 함수 반환 (지연 호출) |
화살표 함수 | this를 바인딩하지 않음 → 상위 스코프의 this 사용 |
this는 자바스크립트에서 가장 유연하지만, 가장 혼란스러운 개념이라 생각합니다.
바인딩 방식을 명시적으로 선택하는 방법을 알아두면, 다양한 상황(=레거시 코드를 마주했을 때)에서 안정적으로 코드를 작성할 수 있다고 생각합니다!
감사합니다.