Javascript의 주요 개념 중 this의 개념을 학습하면서 정리한 글이다.
모던 자바스크립트 딥 다이브 22장을 참고했다.
들어가며
java와 c++에서 this는 인스턴스를 가리키는 키워드로 동작한다.
하지만 javascript에서의 this는 함수를 호출하는 방법에 따라 this의 의미가 달라진다.
이때문에 초보자들이 이해하기 어려운 개념 중 하나이기는 하나, 이러한 유연성은 javascript만의 강력한 특징이다.
따라서 이번 글에서는 javascript에서 this는 어떻게 사용하는지에 대해 알아보도록 한다.
🧐 Quiz
설명에 앞서 this와 관련된 퀴즈를 작성했다.
코드 내의 this에는 어떤 값이 할당되는지 생각하면서 답을 유추해보자.
const person = {
name: "amy",
introduce() {
setTimeout(function() {
console.log("저는 " + this.name + "입니다."); // 코드의 출력값은?
}, 1000);
}
};
person.introduce();
정답 확인
저는 입니다.
setTimeout의 콜백 함수는 일반 함수로 호출되었기 때문에 this에는 전역 객체가 바인딩된다.
하지만 전역 객체는 name이라는 프로퍼티를 가지고 있지 않기 때문에 undefined가 반환된다.
const obj = {
outer: {
inner: {
value: 10,
getValue: function() {
const printValue = () => {
console.log(this.value);
};
printValue();
}
}
}
};
obj.outer.inner.getValue(); // 코드의 출력값은?
정답 확인
10
getValue 메서드는 obj.outer.inner에 바인딩된 this를 가진다.
그리고 printValue는 화살표 함수이기 때문에 상위 스코프인 getValue에서의 this를 그대로 사용한다.
즉 obj.outer.inner 객체가 this에 바인딩되므로 inner.value인 10이 출력된다.
만약 위에 작성한 퀴즈의 정답과 풀이가 이해가 되지 않는다면 상황에 따라 달라지는 this의 특성을 이해할 필요가 있다.
이를 인지하고 글을 읽는 것을 추천한다.
This가 필요한 경우는 어떤게 있을까?
아래는 this 키워드가 쓰이는 코드의 예시이다.
const todo = {
todos: [],
name: "my todo list",
addTodo(todo) {
this.todos.push(todo);
}
};
todo 객체는 todos라는 프로퍼티를 가지며 전체 todo list를 관리한다.
그렇다면 addTodo라는 메서드를 보자.
addTodo 메서드는 프로퍼티를 통해 todo라는 매개변수를 받고, 해당 todo를 새로운 todo로 추가해야 한다.
addTodo 메서드에서 새로운 todo를 어디에 추가해야 할 지를 어떻게 알 수 있을까?
이 때 필요한 것이 this이다.
this는 "현재 자신이 속한 객체"를 반환하고, 이를 통해 내가 속한 todo라는 객체의 todos 프로퍼티에 접근할 수 있는 것이다.
만약 this가 없다면 todos 프로퍼티를 포함하는 객체에 직접 접근해야 할 것이다.
const todo = {
todos: [],
name: "my todo list",
addTodo(todo) {
// todo라는 객체에 직접 접근
todo.todos.push(todo);
}
};
그렇다면 this의 필요성을 알 수 있는 또 다른 예시를 보자.
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce() {
console.log(`안녕하세요! 이름은 ${this.name}이고, ${this.age}살 입니다.`);
}
}
const amy = new Person('amy', 26);
const tom = new Person('tom', 28);
위 예제는 Person이라는 생성자 함수가 있고,
생성자 함수의 내부에서 this는 생성자 함수로 인해 만들어질 새로운 인스턴스를 가리킨다.
즉 const amy = new Person('amy', 26); 에서 Person이라는 생성자 함수를 통해 amy라는 인스턴스를 생성했고, 해당 인스턴스 내부에서의 this는 amy 객체를 가리키게 되는 것이다.
만약 This가 없다면?
만약 this를 통해 이를 처리하지 않으면 어떻게 될까?
this가 없다면 생성한 인스턴스를 동적으로 알 수 없기 때문에
매번 수동으로 객체 리터럴 방식으로 선언한 이후에 현재 객체를 직접 참조해야 할 것이다.
const amy = {
name: 'amy',
age: 26,
introduce: function() {
console.log(`안녕하세요! 이름은 ${amy.name}이고, ${amy.age}살 입니다.`);
}
};
const tom = {
name: 'tom',
age: 28,
introduce: function() {
console.log(`안녕하세요! 이름은 ${tom.name}이고, ${tom.age}살 입니다.`);
}
};
이는 불필요한 코드의 중복을 늘리는 문제가 발생하게 되기에 생성자 함수 내부에서 this를 활용함으로써 해결할 수 있다.
정리해보면 this는 아래의 상황에서 쓰일 수 있다.
1. 메서드가 자기 자신이 속한 객체에 접근할 때
2. 생성자 함수가 생성할 인스턴스 객체에 접근할 때
This 바인딩의 4가지 경우
this는 함수가 어떻게 호출되는가에 따라 동적으로 결정된다.
참고로 this 바인딩이란 this라는 식별자가 가리킬 객체를 결정하는 과정이라고 생각하면 된다.
그리고 이러한 this 바인딩은 함수의 평가 시점이 아닌, 함수의 호출 시점에 결정된다.
그렇다면 이렇게 함수가 호출되는 경우는 어떤게 있을까?
총 4가지의 경우로 나눌 수 있다. 각각에 따라 this에 바인딩되는 값이 달라진다.
1. 일반 함수로 호출
2. 메서드로 호출
3. 생성자 함수로 호출
4. apply, call, bind 메서드 등으로 간접적인 호출
각 예시를 볼 수 있는 예제 코드이다.
const foo = function() {
console.dir(this);
}
foo(); // 1. 일반 함수로 호출 (window)
const obj = { foo };
obj.foo(); // 2. 메서드로 호출 (obj)
new foo(); // 3. 생성자 함수로 호출 (foo{})
const bar = { name: 'bar'};
foo.call(bar); // 4. 간접 호출 (bar)
지금부터 각각의 경우에 대해서 살펴보자.
1. 일반 함수로 호출한 함수 내부의 this
일반 함수로 호출된 함수 내부의 this에는 전역 객체가 바인딩
어떠한 함수가 일반 함수로 호출되었다면, 함수 내부의 this에는 전역 객체가 바인딩된다.
아래 코드를 브라우저에서 실행시키면 this가 출력하는 값은 모두 window이다.
function foo() {
console.log(this); // window
function bar() {
console.log(this); // window
}
bar();
}
foo();
또다른 예시를 보자.
const obj = {
value: 100,
foo() {
setTimeout(function() {
console.log(this); // window
}, 100);
}
}
obj.foo();
이 코드에서 foo를 메서드로 호출했기 때문에 foo 내부의 this는 모두 obj라고 착각할 수 있다.
하지만 위 코드에서는 setTimeout 내부의 첫 번째 인자를 콜백 함수는 일반 함수로서 호출했으며, 그렇기 때문에 내부의 this는 전역 객체가 바인딩된다.
2. 메서드로 호출한 메서드 내부의 this
메서드 내부의 this는 메서드를 호출한 객체가 바인딩
그리고 이는 함수를 호출하는 시점에 결정된다.
const person = {
name: 'Lee',
getName() {
return this.name;
}
}
// 여기서 getName 메서드를 호출한건 person 객체이다
console.log(person.getName()); // 'Lee'
3. 생성자 함수 내부의 this
생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스가 바인딩
function Person(name) {
this.name = name;
this.introduce = function () {
console.log(`안녕하세요 저는 ${this.name}입니다!`);
}
}
const amy = new Person('amy');
amy.introduce(); // 안녕하세요 저는 amy입니다!
위 코드에서 amy라는 객체를 생성했고, introduce 메서드 내부의 this는 amy라는 값을 가지게 된다.
4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출
1번에서 중첩 함수나, 콜백 함수를 포함한 모든 함수는 일반 함수로 호출되었을 때, 내부의 this는 전역 객체를 바인딩한다고 말했다.
하지만 보통은 중첩 함수나 콜백 함수는 외부 함수를 돕는 헬퍼 역할을 하기 때문에, 외부 함수와 중첩 함수나 콜백 함수의 this가 일치하지 않는다는 것은 중첩 함수나 콜백 함수를 헬퍼 함수로 동작하기 어렵게 만든다.
예를 들면 아래 상황에서 예상대로 동작하지 않는 문제가 발생한다.
const counter = {
count: 0,
increase: function() {
this.count++;
// 1초 후에 현재 카운트를 출력
setTimeout(function() {
console.log(this.count); // 여기서 this는 전역 객체
}, 1000);
}
};
counter.increase(); // undefined
increase 메서드 내부에 있는 setTimeout의 의도는 현재 객체의 상태를 출력하는 것이다.
그렇다면 현재 객체인 counter을 참조할 수 있어야 하나, setTimeout 내부에 있는 콜백 함수는 일반 함수로 호출되었기 때문에 this는 전역 객체가 바인딩된다.
그렇다면 이런 상황에서
메서드인 increase의 this와 setTimeout 내부의 콜백함수의 this를 일치시키기 위한 방법은 무엇일까?
이때 Function.prototype.apply/call/bind 메서드에 의한 간접 호출으로 해결할 수 있다.
아래에서 콜백 함수에 들어가는 this는 함수 정의 시 bind 함수에 넣은 increase 메서드의 컨텍스트에 해당하는 this가 바인딩된다.
const counter = {
count: 0,
increase: function() {
this.count++;
// bind 메서드로 increase 메서드에서의 this를 바인딩
setTimeout(function() {
console.log(this.count);
}.bind(this), 1000);
}
}
또는 화살표 함수를 통해 해결할 수 있다.
화살표 함수 내부의 this는 상위 스코프의 this를 가리키기 때문에 counter 객체에 접근할 수 있다.
const counter = {
count: 0,
increase: function() {
this.count++;
setTimeout(() => {
console.log(this.count);
}, 1000);
}
}
함수 호출 방식에 따라 this에 바인딩되는 값을 표로 정리하면 다음과 같다.
호출되는 유형 | this에 바인딩되는 값 |
전역 공간 | 전역 객체 (window / global) |
일반 함수 | 전역 객체 (window / global) |
메서드 | 메서드를 호출하는 주체 (메서드 앞) |
생성자 함수 | 생성자 함수로 생성한 인스턴스 |
간접 호출 | 간접적으로 넣은 this |
마무리하며
이렇게 다양한 상황에서의 this에 대해서 알아보았다.
사실 실제 개발을 하면서 다양한 상황에서 this를 사용했던 경험은 많이 없었기 때문에 이걸 이해하는게 뭐 그렇게 중요한지에 대해 감이 안올 수도 있다. (나 역시 그랬다..)
하지만 javascript 언어에 대한 이해를 높이려면 필수적으로 알아야 하는 개념이라고 생각한다.
중요한 것은 javascript에서의 this는 다른 언어와는 다르게 유동적으로 동작하며, 함수를 호출하는 방식에 따라 다양한 값이 바인딩될 수 있다는 사실을 인지하면 되는 것 같다.
참고 자료
모던 자바스크립트 Deep Dive
자바스크립트를 둘러싼 기본 개념을 정확하고 구체적으로 설명하고, 자바스크립트 코드의 동작 원리를 집요하게 파헤친다. 작성한 코드가 컴퓨터 내부에서 어떻게 동작할 것인지 예측하고, 명
www.aladin.co.kr
잘못된 내용이 있다면 이메일(tnghk9611@naver.com)또는 댓글 남겨주시면 감사하겠습니다.
'☘️ Front end > 🌱 Javascript' 카테고리의 다른 글
[Javascript] 클로저에 대해서 알아보자 (closure) (0) | 2025.01.28 |
---|---|
[Javascript] 콜백 함수 파헤치기 (callback function) (0) | 2025.01.24 |
[JavaScript] 실행 컨텍스트 이해하기 (0) | 2025.01.20 |