혹시라도 여러분들은 다른 프로그래밍 언어를 사용해 본 경험이 있나요? 저는 처음 배운 프로그래밍 언어가 C++이었는데, 제가 기억하고 있던 this
는 클래스로부터 생성되는 인스턴스 중 현재 객체를 가리키는 것으로 알고 있었습니다.
하지만 JavaScript에서는 this
의 의미가 기존에 알고 있던 것과는 좀 다르게 사용됩니다.
따라서 이번 포스트에서는 JavaScript에서 this
는 무엇이며, 어떻게 사용하는지에 대해 알아보도록 하겠습니다.
그때 그때 달라요
JavaScript에서 this의 값은 결정되어 있지 않습니다. 문맥에 따라 그 값이 바뀌죠.
MDN 문서를 보시면 아래와 같은 부분을 확인할 수 있습니다.
대부분의 경우 this의 값은 함수를 호출한 방법이 결정합니다. 실행하는 중 할당으로 설정할 수 없고 함수를 호출할 때 마다 다를 수 있습니다.
이 말은, 일반적으로 JavaScript에서는 this
가 무식하게 하나로 정해져 있는 게 아니라는 것입니다. 즉 this
의 값이 내가 어디에서 어떻게 호출하느냐에 따라 변한다는 것!
따라서 this
의 값이 달라질 수 있는 경우들을 하나씩 살펴보도록 하겠습니다.
전역 범위
전역 범위(Global context), 그러니까 JavaScript에서 가장 평범하게 일반적으로 this
를 호출한다면, this
는 window
라는 전역 객체를 가리킵니다(Node.js에서는 Global
).
참고로 Window
객체라는 것은, 현재 실행되고 있는 JavaScript의 모든 변수, 함수, 객체, DOM 등을 포함하고 있는 객체로, 만물의 근원이 되는 객체입니다.
// 1. 전역 범위에서 호출
console.log(this); // Window {...}
함수 범위
함수 내에서 this를 사용하는 것은 머리를 지끈거리게 만듭니다. 문맥을 알아야 하거든요!
하지만 this
를 함수 내에서 호출한다면 현재 실행되고 있는 코드의 문맥(Context)에 따라 this
가 달라집니다. 이것이 JavaScript에서 this
가 직관적으로 이해하기 어려운 문법이 된 계기죠.
단순 함수 호출
// 1. 일반 함수 범위에서 호출
function outside() {
console.log(this); // this는 window
// 2. 함수 안에 함수가 선언된 내부 함수 호출
function inside() {
console.log(this); // this는 window
}
inside();
}
outside();
우선 가장 일반적인 방법으로 함수를 선언한 후 호출하는 경우, this
는 Window
객체입니다.
함수 안에서 또 함수를 선언하더라도, this
는 여전히 Window
입니다.
객체의 메소드(Method)
// 1. 객체 또는 클래스 안에서 메소드를 실행한다면 this는 Object 자기 자신
var man = {
name: 'john',
hello: function () {
// 2. 객체이므로 this는 man을 가리킴
console.log('hello ' + this.name);
},
};
man.hello(); // 3. hello john
이제부터 this
가 바뀌기 시작합니다. 그 첫 번째 시작은 바로 객체(Object)나 클래스 내부에 선언된 메소드 함수입니다. MDN에서는 이렇게 정의하고 있네요.
함수를 어떤 객체의 메소드로 호출하면 this의 값은 그 객체를 사용합니다.
함수를 객체 외부에서 선언하고, 객체 안에서 호출하는 경우에도 this
는 해당 객체의 this
를 참조합니다. 위의 예시에 이어 아래 예시를 보세요.
// 3. 일반 함수 welcome을 선언
function welcome() {
// 4. 여기서 this는 전역 객체 Window이므로, 만약 실행시키면 undefined가 뜸
console.log('welcome ' + this.name);
}
// 5. man 객체의 welcome 속성에 일반 함수 welcome을 추가
man.welcome = welcome;
// 6. welcome이 man 객체에서 호출되었으므로 welcome john이 출력됨
man.welcome();
이와는 반대로, 객체의 함수를 외부에서 호출할 때 this
는 Window
가 됩니다.
// 7. man 객체의 thanks 속성에 함수를 선언
man.thanks = function () {
// 8. man.thanks()를 호출하면 thanks john이 출력
console.log('thanks ' + this.name);
};
// 8. 이 함수를 객체 외부에서 참조
var thankYou = man.thanks;
// 9. 객체 외부이므로 this가 Window 객체가 되어서 thanks (undefined)가 출력
thankYou();
// 10. 그럼 또 다른 객체에서 이 함수를 호출하면 어떻게 될까?
women = {
name: 'barbie',
thanks: man.thanks, // 11. man.thanks 함수를 women.thanks에 참조
};
// 12. this가 포함된 함수가 호출된 객체가 women이므로 thanks barbie가 출력
women.thanks();
잠깐, 여기서 조심해야 할 점이 있습니다. 바로 메소드에서 내부 함수를 선언하는 경우는 this
가 어떻게 될까요?
var man = {
name: 'john',
// 1. 이것은 객체의 메소드
hello: function () {
// 2. 객체의 메소드 안에서 함수를 선언하는 것이니까 내부 함수
function getName() {
// 3. 여기서 this가 무엇을 가리키고 있을까?
return this.name;
}
console.log('hello ' + getName()); // 4. 내부 함수를 출력시키고
},
};
man.hello(); // 메소드를 실행시키면 undefined가 뜬다! this는 Window였던 것
객체의 메소드에서 this
가 객체를 가리키고 있던 것과는 다르게, 내부 함수에서 this
는 Window
객체를 가리키고 있습니다. 내부 함수는 엄밀히 말해 메소드가 아니기 때문에, 단순 함수 호출 규칙에 따라 Window
를 가리키고 있다는 점에 유의해야 합니다.
call(), apply(), bind()
그럼 위의 코드를 우리가 의도한 결과가 나올 수 있도록 할 수는 없을까요? JavaScript에서는 각가 다른 문맥의 this
를 필요에 따라 변경할 수 있도록 함수를 제공합니다. call(), apply(), bind()
등이 있는데, 여기서는 call()
을 한 번 사용한 예시를 보도록 하겠습니다.
// 1. 이것은 객체의 메소드
var man = {
name: 'john',
// 2. 객체의 메소드 안에서 함수를 선언하는 것이니까 내부 함수
hello: function () {
function getName() {
// 3. 여기서 this가 무엇을 가리키고 있을까?
return this.name;
}
// 4. 이번에는 call()을 통해 현재 문맥에서의 this(man 객체)를 바인딩해주었다
console.log('hello ' + getName.call(this));
},
};
// 이번에는 메소드를 실행시키면 john가 뜬다!
// this가 man 객체로 바인딩 된 것을 확인할 수 있다
man.hello();
콜백 함수
// 1. 콜백함수
var object = {
callback: function () {
setTimeout(function () {
console.log(this); // 2. this는 window
}, 1000);
},
};
콜백 함수에서는 this
가 Window
를 가리킵니다. 객체 안에 메소드로 선언되어 있어도요.
생성자
함수 앞에 new
키워드가 붙이고 선언할 때, this
를 해당 객체에 바인딩합니다.
// 1. 클래스 역할을 할 함수 선언
function Man() {
this.name = 'John';
}
// 2. 생성자로 객체 선언
var john = new Man();
// 3. this가 Man 객체를 가리키고 있어 이름이 정상적으로 출력된다
john.name; // => 'John'
ECMAScript 6 문법인 class
를 이용해 작성할 수도 있습니다.
// 1. Class Man 선언
class Man {
constructor(name) {
this.name = name;
}
hello() {
console.log('hello ' + this.name);
}
}
// 2. 생성자 실행
var john = new Man('John');
john.hello(); // 3. hello John 출력
여기서 주의할 점은 new
키워드를 붙이지 않을 경우 this
가 해당 객체로 바인딩 되지 않기 때문에 Window
객체를 건드리는 일이 발생할 수 있습니다. 따라서 new
키워드를 꼭 써주도록 합시다.
화살표 함수(Arrow function)
실행 환경에 따라 의미가 달라지니까 머리 아프다! 도와줘요 화살표 함수!
화살표 함수는 ECMAScript 6에서 새로 추가된, 함수를 축약해서 사용할 수 있는 문법입니다.
하지만 단순히 함수를 축약해서 사용하는 것 뿐만이 아니라 this
를 외부 스코프에서 정적으로 바인딩된 문맥(정적 컨텍스트, Lexical context)을 가진다는 특징을 갖고 있습니다. 이게 무슨 소리일까요.
우선 이번 포스트의 주제는 this
이기 때문에, 정적 컨텍스트의 필요 없는 불필요한 정의를 제외하고 짧게 요약하자면 다음과 같습니다.
The name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined.
정적 컨텍스트(Lexical context)는 소스코드가 작성된 그 문맥의 실행 컨텍스트나 호출 컨텍스트에 의해 결정된다.
컨텍스트는 개념이 복잡하기 때문에, 우선은 JavaScript 코드를 실행하기 위한 변수, 함수 등의 정보를 담고 있는 환경이라고만 이해하셔도 됩니다.
즉, 정적 컨텍스트는 함수가 실행된 위치가 아닌, 정의(defined)된 위치에서의 컨텍스트를 참조한다는 이야기입니다. 이 코드가 어디서 실행되고 그런 것 따질 필요 없이, 그냥 정의된 부분에서 가까운 외부 함수의 this
만 보면 됩니다.
예제 코드를 통해 이를 살펴보도록 합시다.
// 1. 화살표 함수
var obj = {
a: this, // 2. 일반적인 경우 this는 window,
b: function () {
console.log(this); // 3. 메소드의 경우 this는 객체
},
c: () => {
console.log(this);
// 4. 화살표 함수의 경우 정적 컨텍스트를 가짐, 함수를 호출하는 것과 this는 연관이 없음
// 5. 따라서 화살표 함수가 정의된 obj 객체의 this를 바인딩하므로 this는 window
},
};
obj.b(); // 6. obj
obj.c(); // 7. window
일반적인 방법으로 함수를 선언(function () { ... }
)하면, 일반적인 함수는 함수가 실행될 때 자체적으로 this
를 할당하게 되는데, 이 함수는 메소드 함수이므로 this
가 메소드를 포함하는 객체로 바인딩됩니다.
하지만 화살표 함수는 this
가 없기 때문에, 부모 스코프의 this
를 바인딩하는데 위의 예시에서 이는 곧 Window
객체를 의미합니다. 따라서 메소드로 화살표 함수를 쓰면, this
를 이용한 부모 객체에 접근할 수 없습니다.
마무리
음… 그렇군… 그래서… 뭐 어쨌다고?
여기까지가 this
에 대한 간단한(?) 정리였습니다. 함수 내부에서 this
를 사용하는 경우에 따라 값이 바뀔 수 있다 정도로만 이해하셔도 좋다고 생각합니다. 크게 와닿지 않을 수도 있습니다. 저 역시도 JavaScript에서 this
를 쓸 일이 그렇게 많지는 않았었기 때문입니다.
사실 저는 ECMAScript 6 문법인 화살표 함수에 대해서 알아보다가 this
에 대해 궁금한 점이 생겨 이렇게 정리하는 글을 작성하게 되었습니다. ES6 문법과 화살표 함수는 다들 많이 쓰잖아요? 그래서 화살표 함수에서 가장 중요하다는 this
바인딩을 살펴볼 겸 해서 아예 this
에 대해 알아보는 시간을 가졌습니다.
물론 실행 컨텍스트, 스코프 체인 등 보다 더욱 깊은 개념을 접하고 멘붕에 빠지긴 했지만요…
마지막으로 혹시라도 이 글을 참고하시는 여러분께 더 도움이 되시라고 제가 참고한 글들을 올리며 마무리하겠습니다.