본문 바로가기

[자바스크립트] 상황에 따라 달라지는 this (feat 코어 자바스크립트)

 

 

들어가며

많은 기업들이 타입 스크립트와 nest.js를 활용해서 서버 개발을 하곤 합니다. 취업을 하려면, 타입 스크립트와 nest.js를 공부해서 실무를 익히는 것이 중요할 것입니다. 하지만 아직 자바스크립트의 기초도 없는 상태에서 타입 스크립트와 nest.js를 공부하는 것이 맞을까 하는 생각이 들었습니다. 빠르게 기술변화를 적응하고, 러닝 커브를 줄이기 위해 빠르게 공부해야 하는 것도 맞겠지만, 그전에 언어의 기반이 되는 자바스크립트부터 제대로 알아야 하지 않을까 하는 생각이 들었습니다. 이 기회에 자바스크립트의 기본에 대해 정리해보고자 합니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 콘텍스트가 생성될 때 함께 결정되며, 함수를 호출할 때 this가 결정됩니다. 지금부터 this에 대해 자세하게 살펴보겠습니다.

 

 

1. 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킵니다. 만약 전역 변수를 선언하면 자바스크립트 엔진은 전역 변수를 전역 객체의 프로퍼티로도 할당합니다. 아래 코드를 확인해보면 3번째 줄의 window를 global로 바꾸면 Node.js 환경에서도 같은 동작을 확인할 수 있습니다.

 

var a = 1;
console.log(a);		// 1
console.log(window.a);	// 1
console.log(this.a);	// 1

 

 

만약 사용자가 var 연산자를 이용해 변수를 선언하면 실행 콘텍스트는 변수를 수집해서 LexicalEnvrionment의 프로퍼티로 저장합니다. 이후 어떤 변수를 호출하면 LexicalEnvironment를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환합니다. 만약 전역 콘텍스트의 경우 LexicalEnvironment는 전역 객체를 그대로 참조합니다. 즉, 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역 객체의 프로퍼티로 할당합니다. 즉, 전역 공간에서의 this는 전역 객체를 가리킵니다.

 

 

 

2. 메서드로서 호출할 때 그 메서드 내부에서의 this

함수 vs 메서드

프로그래밍 언어에서 함수와 메서드는 비슷하지만 서로 다릅니다. 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행합니다. 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했습니다. 그럼, 함수와 메서드에서의 this는 어떤 차이가 있는지 알아보겠습니다. 

 

 

var func = function (x){
	console.log(this, x);
}
func(1); // Window { ... } 1

var obj = {
	method: func
};

obj.method(2); // { method: f } 2

 

 

func라는 변수에 익명 함수를 할당하고 func를 호출했더니 this로 전역 객체 Window가 출력됐습니다. 다음, obj 변수에 객체를 할당하는데, 그 객체의 method 프로퍼티에 func 함수를 할당했습니다. 그리고 obj의 method를 호출했더니, this가 obj라고 나왔습니다. obj의 method 프로퍼티에 할당한 값과 func 변수에 할당한 값은 모두 1번째 줄에서 선언한 함수를 참조합니다. 즉 익명 함수는 그대로지만 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것을 확인할 수 있습니다.

 

 

 

 

3. 함수로서 호출할 때 그 함수 내부에서의 this

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않습니다. 실행 콘텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라봅니다. 

 

 

메서드의 내부 함수에서의 this

그렇다면 각 위치에서 this는 어떤 결과를 호출할까요?

 

var obj1 = {
	outer: function() {
    		console.log(this);
        var innerFunc = function() {
        	console.log(this); 
        }
        innerFunc(); //
        
        var obj2 = {
        	innerMethod: innerFunc
        };
        obj2.innerMethod(); //
    }
};
obj1.outer(); //

 

결과적으로 obj1, WIndow, obj2.innerMethod를 가리킵니다. 그렇다면 위의 코드 흐름을 분석해보겠습니다.

 

1번째 줄 : 객체를 생성하는데, 이때 객체 내부에는 outer라는 프로퍼티가 있으며, 여기에는 익명 함수가 연결됩니다. 이렇게 생성한 객체를 변수 obj1에 할당합니다.

 

15번째 줄 : obj1.outer를 호출합니다.

 

2번째 줄 : obj1.outer 함수의 실행 콘텍스트가 생성되면서 호이스팅 하고, 스코프 체인 정보를 수집하고, this를 바인딩합니다. 이 함수는 호출할 때 함수 명인 outer 앞에 점이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj1이 바인딩됩니다.

 

3번째 줄 : obj1 객체 정보가 출력됩니다.

 

4번째 줄 : 호이스팅 된 변수 innerFunc는 outer 스코프 내에서만 접근할 수 있는 지역변수입니다. 이 지역변수에 익명 함수를 할당합니다.

 

7번째 줄 : innerFunc를 호출합니다.

 

4번째 줄 : innerFunc 함수의 실행 콘텍스트가 생성되면서 호이스팅, 스코프 체인 수집, this 바인딩 등을 수행합니다. 이 함수를 호출할 때 함수명 앞에는 점이 없었습니다. 즉 함수로서 호출한 것이므로 this가 지정되지 않았고, 따라서 자동으로 스코프 체인상의 최상위 객체인 전역 객체가 바인딩됩니다. 

 

5번째 줄 : Window 객체 정보가 출력됩니다. 

 

9번째 줄 : 호이스팅된 변수 obj2 역시 outer 스코프 내에서만 접근할 수 있는 지역변수입니다. 여기에는 다시 객체를 할당하는데, 그 객체에는 innerMethod라는 프로퍼티가 있으며, 여기에는 앞서 정의된 변수 innerFunc와 연결된 익명 함수가 연결됩니다.

 

12번째 줄 : obj2.innerMethod를 호출합니다

 

9번째 줄 : obj2.innerMethod 함수의 실행 콘텍스트가 생성됩니다. 이 함수는 호출할 때 함수 명인 innerMethod 앞에 점이 있었으므로 메서드로서 호출한 것입니다. 따라서 this에는 마지막 점 앞의 객체인 obj2가 바인딩됩니다.

 

10번째 줄 : obj2 객체 정보가 출력됩니다.

 

7번째 줄에서는 outer 메서드 내부에 있는 함수를 함수로서 호출했습니다. 반면 12번째 줄에서는 같은 함수를 메서드로서 호출했습니다. 같은 함수임에도 7번째 줄에 바인딩되는 this와 12번째 줄에 의해 바인딩되는 this의 대상이 서로 달라진 것입니다. 즉, this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경은 중요하지 않고 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 중요합니다. 그렇다면, 메서드의 내부 함수에서의 this를 호출 주체의 유무에 따라 변경되지 않도록 하는 방법은 무엇인지에 대해 알아보겠습니다.

 

 

 

 

 

메서드의 내부함수에서의 this를 우회하는 방법

만약 호출 주체가 없을 때는 자동으로 전역 객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있으려면 어떻게 해야 할까요? ES5에서 this를 우회 상속하는 방법은 변수를 활용하는 것입니다. 

 

 

var obj = {
	outer: function() {
    		console.log(this); // outer
        var innerFunc1 = function() {
        	console.log(this); // Window
        }
        innerFunc1(); 
        
        var self = this;
        
        var innerFunc2 = function() {
        	console.log(self); // outer
        };
        innerFunc2();
    }
};
obj.outer();

 

위 예제의 innerFunc1 내부에서 this는 전역 객체를 가리킵니다. 한편 outer 스코프에서 self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력됩니다. 그저 상위 스코프의 this를 저장해서 내부 함수에서 활용하면 우회 상속을 할 수 있습니다. 

 

 

 

this를 바인딩하지 않는 함수

하지만 ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했습니다. 화살표 함수는 실행 콘텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다. 

 

 

var obj = {
	outer: function(){
    	console.log(this);	   // { outer: f }
        var innerFunc = () => {
        	console.log(this); // { outer: f }
        };
        innerFunc();
    }
};
obj.outer();

 

내부 함수를 화살표 함수로 바꾸면, 우회 법이 불필요해집니다. 그 밖에도 call, apply 등의 메서드를 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법이 있습니다. 

 

 

 

 

 

 

4. 콜백 함수 호출 시 그 함수 내부에서의 this

만약 콜백 함수를 호출할 때, this가 어떻게 변경되는지 알아보겠습니다. 함수 A의 제어권을 다른 함수 B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다. 이때 함수 A는 함수 B의 내부 로직에 따라 실행되며 this 역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정됩니다. 콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다. 

 

다음은 대표적인 콜백 함수입니다. 각각 어떤 값이 출력되는지를 예측해보고 결과와 비교해봅시다. 

 

 

setTimeout(function() { console.log(this); }, 300);

[1, 2, 3, 4, 5].forEach(function(x) {
	console.log(this, x);
})

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
	.addEventListener('click', function(e) {
    	console.log(this, e)
    })

 

1. setTimeout 함수는 300ms 만큼 시간 지연을 한 뒤 콜백 함수를 실행하라는 명령입니다. 0.3초 뒤 전역 객체가 출력됩니다.

2. forEach 메서드는 배열의 각 요소를 앞에서부터 차례로 하나씩 꺼내어 그 값을 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령입니다. 전역 객체와 배열의 각 요소가 총 5회 출력됩니다.

3. addEventListener는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령입니다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력됩니다. 

 

setTimeout 함수와 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않습니다. 따라서 콜백 함수 내부에서의 this는 전역 객체를 참조합니다. 한편 addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있습니다. 즉, 콜백 함수의 제어권을 갖는 함수가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역 객체를 바라봅니다. 

 

 

 

 

5. 생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다. 객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 합니다. 

 

프로그래밍적으로 생성자는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다. 틀을 이용해 개별 인스턴스를 만들 수 있습니다. 자바스크립트는 함수에 생성자로서의 역할을 함께 부여했습니다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 됩니다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다. 

 

생성자 함수를 호출(new 명령어와 함께 함수를 호출)하면 우선 생성자의 prototype 프로퍼티를 참조하는 _proto_라는 프로퍼티가 있는 객체를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다. 예를 통해 알아보겠습니다.

 

 

var Cat = function (name, age) {
	this.bark = '야옹';
    this.name = name;
    this.age = age;
};
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

/* 결과 
Cat { bark: '야옹', name: '초코', age: 7}
Cat { bark: '야옹', name: '나비', age: 5}
*/

 

Cat이란 변수에 익명 함수를 할당했습니다. 이 함수 내부에서는 this에 접근해서 bark, name, age 프로퍼티에 각각 값을 대입합니다. 6, 7번째 줄에서 new 명령어와 함께 Cat 함수를 호출해서 변수 choco, nabi에 각각 할당했습니다. 8번째 줄에서 choco와 nabi를 출력해보니 각각 Cat 클래스의 인스턴스 객체가 출력됩니다. 즉 6번째 줄에서 실행한 생성자 함수 내부에서의 this는 choco 인스턴스를, 7번째 줄에서 실행한 생성자 함수 내부에서의 this는 nabi 인스턴스를 가리킴을 알 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

마치며

자바스크립트의 기본에 대해 공부하면서, 기본도 정확하게 알지 못하고, 앞으로 나아가려 했구나 하는 생각이 들었습니다. 기초이기에 지금 공부하는 내용을 더 잘 이해해야겠다고 생각했습니다. 기본에 충실할 수 있는 개발자가 되고 싶습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

출처

 

싸니까 믿으니까 인터파크도서

 

book.interpark.com