this
란?
this
는 함수 내부에서 특정한 객체를 가리키는 식별자다. 자세히 살펴보자.
먼저, this
는 식별자다. 식별자란 데이터가 올려진 메모리에 붙여진 이름이다(또는, 데이터가 저장된 메모리 주소 그 자체이다). 변수, 함수, 함수의 파라미터 등이 식별자에 해당한다. 마찬가지로, this
도 메모리에 올려진 어떠한 데이터(객체)의 메모리 주소를 가리킨다(참조한다). 물론, 특별한 메서드를 사용하지 않으면 사용자가 직접 this
의 값을 지정할 수 없다는 점에서(호출 시 동적으로 바인딩 대상이 결정된다) 다른 식별자들과의 차이점이 있긴 하다. 하지만, 식별자의 본질인 '데이터가 저장된 주소를 참조하는' 역할은 동일하게 수행한다.
this
는 함수에 위치하고 객체를 가리킨다. 실제로, this
에 대한 정보는 실행 컨텍스트의 렉시컬 환경(Lexical Environment) 에 존재한다. 렉시컬 환경에는 환경 레코드를 구성하는 VariableEnvironment
컴포넌트와 LexicalEnvironment
컴포넌트, 그리고 this
에 대한 바인딩 정보로 구성된다. 자세한 내용은 포스트 참고).
this
는 함수 내부에서 사용돼야 하는 특정한 객체를 자기 참조하며, 왜 이런 동작이 필요한지를 이해하는 게 중요하다. 특히, this
는 함수(또는 메서드)가 호출되는 방식에 따라 다른 값을 가리킨다(실행 컨텍스트에 생성될 당시엔 this
바인딩은 값을 갖지 않고, 함수가 호출될 당시의 문맥에 다라 바인딩 값을 다르게 갖는다. 마치, 상자를 열어봐야 살아있음을 확신할 수 있는 슈뢰딩거의 고양이 같은 느낌).
this
는 다양한 코드에서 굉장히 자유 보이기 때문에 중요한 반면, 동적으로 값을 바인딩한다는 특성 때문에 값의 예측이 힘들어서 사용에 어려움도 있다. 그래서, this
의 개념과 활용에 대해선 꼼꼼하게 잘 공부해두는 게 필요하다.
자기 참조가 필요한 상황 예시
아래는 this
를 활용하는 예시다. this
는 함수 내부에 위치하며, 특정 객체를 가리킨다고 했다. 아래 예제에서의 this
는 callName
이라는 메서드(메서드는 객체 내부에서 정의된 함수를 의미한다) 내부에 존재한다. 그리고, this
가 가리키는 객체는 자기 자신인 myObj
이다.
let myObj1 = {
name: 'Jay',
callName: function () {
console.log(`Hi ${this.name}!`); // Hi Jay!
}
}
myObj1.callName(); // Hi Jay!
이미지로 표현해보면 다음과 같다. this가 myObj
의 스택 메모리 주소를 직접 참조하기 때문에, myObj
객체를 통해 호출된 callName
메서드(함수) 내부에 있는 this
의 this.name
은 Hi ${myObj.name}!
이 되는 셈이다.
해당 상황만 보면 this
의 필요성이 와닿지 않는다. 실제로 callName
메서드 내부에서 Hi ${myObj.name}!
이라고 객체 자기 자신의 이름을 그대로 사용해도 동일한 결과가 확인되기 때문이다.
myObj1 = {
name: 'Jay',
callName: function () {
console.log(`Hi ${myObj1.name}!`); // Hi Jay!
}
}
myObj1.callName();
그냥 자기 자신의 변수명을 그대로 참조해도 동일한 결과가 나옴에도 불구하고 this
를 사용하는 이유는, 해당 메서드를 다른 객체에서 복제해 사용해 보면 알 수 있다.
let myObj1 = {
name: 'Jay',
callName: function () {
console.log(`Hi ${myObj1.name}!`); // this가 아닌 자기 자신의 변수명으로 자기 참조
}
}
let myObj2 = {
name: 'Ko'
}
myObj2.callName = myObj1.callName // 메서드 복제
myObj2.callName(); // Hi Jay!
myObj2
에서 myObj1
의 메서드를 복제해서 사용했을 때 동작 의도는 myObj2
객체에 사용된 name
프로퍼티의 값을 사용해서 Hi Ko!
라는 출력이 되는걸 기대했을 것이다(다른 객체의 name
을 참조해서 값을 출력하기보단, 객체 내부에서 자체적으로 독립적인 값-동작 쌍이 대응되는 편이 자연스럽다). 하지만, 예상과 달리 메서드의 결과는 Hi Jay!
가 출력된다. 메서드에서 myObj1
객체를 직접 자기 참조하고 있기 때문에, 다른 객체에서 메서드를 복제해서 사용하려고 할 때 문제가 생긴 것이다.
이처럼, 메서드는 다른 객체에서 복제돼서 사용될 가능성이 있다. 그리고, 복제된 객체의 메서드에서도 의도한 대로의 '자기 참조'를 동일하게 잘 구현하기 위해선 변수명으로 직접 참조하는 것이 아니라, 문맥에 따라 '자기 자신인 객체'를 다르게 가리킬 수 있는 별도의 식별자가 필요하다. 그리고, this
가 해당 역할을 한다. 위의 예시는 아래와 같이 바꿔보자.
myObj1 = {
name: 'Jay',
callName: Function() {
console.log(`Hi ${this.name)!`); Hi Ko!
}
}
myObj2 = {
name: 'Ko'
}
myObj2.callName = myObj1.callName;
myObj2.callName(); // console.log(`Hi ${myObj2.name)!1)과 같은 동작
myObj1
에서 변수명으로 했던 자기 참조를 this
로 변경하면 기대했던 대로 Hi Ko!
라는 문자열이 콘솔에 잘 출력된다. 이렇게, this
는 함수 내의 로직이 자기 자신인 객체의 값을 참조해야 하고(위의 경우 name
프로퍼티), 생성한 함수가 다른 곳에 복제해야 해서 단순 변수명으로 자기 참조를 할 경우(예를 들어, myObj1.name
과 같이 변수명을 직접 참조한 경우) 유용하게 활용된다.
this
의 동적 바인딩
메서드에서의 this
바인딩: 호출한 객체
위에 언급한대로, this
는 함수 내부에서 특정 값을 자기 참조로 사용해야 하고, 해당 함수를 다른 곳에서 복제해서 사용하는 경우가 존재해 함수(또는 메서드)의 자기 참조 값이 복제될 때마다 변경이 필요한 경우 유용하다. 그리고, 해당 필요가 있는 대표적인 상황 중 하나가 위에서 활용한 객체 내의 메서드로 사용되는 경우이다.
객체 내부에 선언된 메서드는 객체 내부에 있는 다른 프로퍼티들을 참조할 수 있다. 객체라는 게 프로퍼티와 메서드를 조합하여 어떠한 개념(정의)을 구분 짓는 것이기 때문에, 특정 객체의 고유한 특성을 나타내는 데 있어서 메서드와 프로퍼티의 유기적인 참조와 활용이 필요하다.
이렇게, 객체 내부의 메서드에서 활용된 this
의 경우, 메서드를 호출한 주체가 되는 객체가 this
에 바인딩된다. 위의 예시에서 myObj1
내부에 최초로 등장에서 사용된 callName
메서드는 myObj1.callName()
이라는 식으로 myObj1
객체에 의존돼서 사용되기 때문에, 이럴 경우 callName()
메서드의 this
바인딩엔 myObj1
이 참조된다. 마찬가지로, myObj2
객체에서 callName
메서드를 복제한 후 myObj2.callName()
형태로 사용하면 메서드의 this
바인딩은 myObj2
를 참조한다. 메서드는 메서드를 호출한 객체를 this
로 가리킨다.
참고로, myObj2.callName = myObj1.callName;
형태로 객체의 메서드를 직접 할당하면 두 객체는 같은 함수를 메서드로 참조한다(얕은 복사와 깊은 복사 참고). 결국, myObj1.callName()
과 myObj2.callName()
은 같은 함수를 참조하고, 동일한 명령을 실행하는 것이다. 하지만, myObj1.callName()
과 myObj2.callName()
의 실행 컨텍스트가 다르고, 렉시컬 환경에서의 this
바인딩 정보가 다르기 때문에, 둘은 다른 동작을 수행한다. 즉, 함수가 호출되어 생성된 실행 환경에 따라 같은 코드도 다른 결과가 호출될 수 있는 것이다.
생성자 함수에서의 this
바인딩: (미래에 생성할) 인스턴스
생성자 함수는 말 그대로 '객체를 생성하는 함수'다. 그리고, 위에서 얘기한 '함수 내에서 자신의 객체를 참조해야 하고, 복제된 값을 사용해서 this
의 참조가 변경돼야 하는 경우에 정확하게 해당한다. 아래는 대표적인 생성자 함수의 this
사용 예시다.
function Person(name, age) {
this.name = name;
this.age = age;
}
let person1 = new Person('Jay', 20);
console.log(person1); // Person {name: 'Jay', age: 20}
위의 상황에서 사용된 this
는 생성자 함수인 Person
을 가리키는 게 아니라, 미래에 생성될 객체인 person
을 가리킨다. 그리고, 생성자 함수로 다른 객체를 생성할 때마다 this
는 새롭게 생성된 인스턴스를 가리킨다(확인을 위해 생성자 함수에 console.log
로 생성자 함수 호출 시마다 this
가 가리키는 값을 출력해 봤다).
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this, this.name, this.age)
}
let person1 = new Person('Jay', 20); // Person {name: 'Jay', age: 20} 'Jay' 20;
let person2 = new Person('Ko', 21); // Person {name: 'Ko', age: 20} 'Ko' 21;
this
가 가리키는 값이 인스턴스로 확인됐다. 이렇게, 생성자 함수는 this
를 통해 자신이 생성할 인스턴스를 가리키고, 해당 인스턴스에 추가할 프로퍼티에 대한 정의를 수행한다. 미래에 생성될 인스턴스를 가리킬 식별자'라는 컨셉을 가진 식별자는 this
뿐이기 대문에, this
없이 해당 과정을 수행한다는 건 불가능하다.
// 아래 코드는 SyntaxError가 발생합니다
fuction Person(name, age) {
let name = name; // let으로 선언한 변수는 미래에 생성할 인스턴스가 참조할 수 없음
'age': age;
}
자바스크립트의 this
는 동적 바인딩이라는 특성 때문에, 함수가 호출되는 상황에 따라 this
에 바인딩되는 값이 다르다. 하지만, 다른 객체 지향 언어들에서 this
와 같이 자기 참조를 하는 변수들은 클래스 생성 시 사용된다. 그리고, 생성자 함수가 유사하게 객체를 생성하는 생성자(원형)를 의미한다는 점과, 자바스크립트에 추가된 클래스 문법도 생성자 함수를 확장했다는 점을 봤을 때, 가장 this
의 정의를 명료하게 이해할 수 있는 대표적인 사용 사례가 생성자 함수를 사용한 경우인 게 아닌가 생각한다.
중첩 함수에서의 this
객체 안의 메서드에서 함수를 선언하는 경우가 있다. 아래는 예시다.
let myObj = {
name: 'Jay',
myMethod: function () {
console.log(this.name); // Jay
function myFunc () {
console.log(this.name); // '' (빈 문자열)
}
myFunc();
}
}
myObj.myMethod();
myMethod
메서드는 myObj
객체로부터 호출됐기 때문에, myMethod
의 this
는 myObj
다. 객체의 메서드가 갖는 this
는 메서드를 소유한 객체를 가리킨다.
하지만, myMethod
메서드 내부에서 선언된 myFunc
의 this
는 전역 객체다(일반 함수로 선언되고 호출된 모든 함수의 this
는 전역을 가리킨다). 만약에 myMehtod
메서드 내부의 함수 선언문 myFunc
에서 상위 스코프 함수의 this
를 참조해야 하는 경우라면 이러한 동작은 문제가 된다.
let myObj = {
firstName: 'Jay',
lastName: 'Ko',
fullName: function() {
let getFullName = function() {
return `${this.firstName}` + ' ' + `${this.lastName}`;
}
console.log(getFullName());
}
}
myObj.fullName() // undefined undefined
fullName
메서드의 내부 함수 getFullName
은 일반 함수로 호출됐기 때문에 this
가 전역을 가리킨다(브라우저일 경우 window
를 가리킨다). 그리고, 전역 객체엔 firstName
과 lastName
이라는 프로퍼티가 없는 상황이기 때문에, 값으로 undefined undefined
가 출력된다. 이는, 중첩 함수에서의 this
바인딩이 다른 상황이 문제가 되는 하나의 예시다(예시를 위해 만든 상황이고, 좋은 코드는 아닌 것 같다).
해당 상황을 해결하려면 중첩 함수로 선언한 getFullName
함수에 명시적으로 fullName
메서드와 같은 this
를 바인드 해줘야 한다. 이를 위해 bind
, apply
, call
메서드를 사용할 수 있다. bind
는 this
를 바인딩만 하고 함수 호출은 하지 않는다.
// bind는 this 바인딩만 해주고 함수 호출은 하지 않는다
myObj = {
firstName: 'Jay',
lastName: 'Ko',
fullName: function() {
let getFullName = function() {
return `${this.firstName}` + ' ' + `${this.lastName}`;
}.bind(this); // getFullName 함수에 fullName 메서드의 this를 binding
console.log(getFullName());
}
}
myObj.fullName(); // Jay Ko
apply
와 call
은 둘 다 함수를 호출하면서 this
를 바인딩한다(apply
와 call
은 원래 태생이 함수를 호출하는 동작을 하는 거고, 해당 과정에 this
바인딩 정보를 전달하는 인자를 이용하는 것이다). 둘은 아규먼트로 전달할 인자의 데이터 형식에만 차이가 있고, 실제 동작은 동일하다.
// apply는 함수를 호출하면서 첫번째 인자의 값을 this에 바인딩한다
// apply는 인자로 배열을, call은 문자열을 전달한다
myObj = {
firstName: 'Jay',
lastName: 'Ko',
fullName: function() {
let getFullName = function() {
return `${this.firstName}` + ' ' + `${this.lastName}`;
}
console.log(getFullName.apply(this, []));
}
}
myObj.fullName(); // Jay Ko
화살표 함수(Arrow Function)
bind
, apply
, call
메서드를 사용해 명시적으로 this
를 바인딩하는 것 말고도, 화살표 함수를 사용해서도 중첩 함수의 this
바인딩 문제를 해결할 수 있다.
화살표 함수의 this
는 함수가 정의되는 시점에 결정된다. 그리고, 함수 호출 시점에 따라 this
가 가리키는 대상이 동적으로 바뀌지 않는다. 따라서, 화살표 함수를 사용하면 this
의 값이 유동적으로 변경해 다른 대상을 지칭하여 발생할 수 있는 문제를 일부 해결할 수 있다.
그리고, 함수 정의 시점에 화살표 함수의 this
바인딩되는 객체는 상위 스코프의 this
다. 즉, 중첩 함수가 선언된 경우 화살표 함수로 선언된 함수의 this
는 상위 함수의 this
를 그대로 참조하게 되고, 손쉽게 중첩된 함수의 this
를 일치시킬 수 있다.
위에서 활용한 예제를 화살표 함수를 활용해 변경해 보면 다음과 같다.
// 화살표 함수 getFullName은 fullName의 this를 참조한다
myObj = {
firstName: 'Jay',
lastName: 'Ko',
fullName: function() {
let getFullName = () => {
return `${this.firstName}` + ' ' + `${this.lastName}`;
}
console.log(getFullName());
}
}
myObj.fullName(); // Jay Ko
화살표 함수가 자바스크립트의 비교적 최신 문법이라는 점과, 화살표 함수의 this
바인딩 방식이 정적이면서 동시에 가리키는 대상을 보다 명확하게 할 수 있다는 걸 고려했을 때, 화살표 함수의 등장 자체가 this
바인딩 값 추론의 어려움을 해소하기 위한 방향으로 설계된 게 아닌가 싶다. 아무튼, 필요한 상황에서 화살표 함수를 잘 사용해서 동작이 예측 가능학 코드를 짤 수 있도록 노력해야겠다.
결론
짧은 개발 경험이지만, 그 속에서도 this
가 가리키는 값이 헷갈리는 경우를 몇 번 겪었다. 그런 의미에서 좀 더 공부해 보고 내용을 정리해 둔다. 다양한 케이스 경험해 보면서 더 적합한 방식으로 this
바인딩을 처리할 수 있게 노력해야겠다. 끝.
'JavaScript' 카테고리의 다른 글
241102 TIL (1) | 2024.11.03 |
---|---|
자바스크립트 엔진과 런타임 (2) | 2024.10.30 |
실행 컨텍스트 톺아보기: 실행 컨텍스트 구성 요소 (0) | 2024.10.22 |
실행 컨텍스트 톺아보기: 기본 개념, 메모리 사용 방식 (8) | 2024.10.19 |
프로퍼티 톺아보기: 데이터 프로퍼티와 접근자 프로퍼티 (0) | 2024.10.17 |