본문 바로가기

javascript

TIL no.58 - this 왜 헷갈릴까? : JavaScript

javascript 에 대한 이해를 높이기 위해 `You don't know JS` 책을 참고하여 이해를 돕기 위해 공부한 내용을 정리했습니다.
이전에 `코어 자바스크립트` 책을 읽으며 this 에 대해서 이해하고 구글링을 더해 헷갈리는 부분을 확실히 하려고 했었습니다.
다시 이 책을 통해 this 에 대한 개념을 공부하는데 이전에 공부했던 부분을 복습하는 차원에서 더 확실하고 명료하게 정리할 수 있을 것 같습니다.

 

 

this 는 자바스크립트에서 가장 헷갈리는 키워드 중 하나입니다.

항상 같은 곳을 가리킨다면 헷갈릴 이유도 없겠지만 this 는 여러 방법에 따라 다른 곳을 가리킵니다.

 

이전에 this 에 대해 블로그를 작성했었는데 그 글에서도 다루었듯이 많은 사람들이 this 를 단어 자체의 의미로 해석해 자기 자신이라고 생각하여 많은 오류를 범하기도 합니다.  

 

이렇게 헷갈리는 this 를 왜 ? 사용해야 하는지 먼저 알아보겠습니다.

function identify() {
	return this.name.toUpperCase();
}

function speak() {
	let greeting = "Hi, I'm" + identify.call(this);
    console.log(greeting);
}

let me = {
	name: 'Haneul';
}

let you = {
	name: 'Jam';
}

identify.call( me );  // HANEUL
identify.call( you );  // Jam

speak.call( me );  // Hello, I'm Haneul
speak.call( you );  // Hello, I'm Jam

위의 코드를 보는 것과 같이 idenfify 와 speak 두 함수를 객체별로 따로따로 함수를 작성할 필요없이 다중 콘텍스트 객체인 me 와 you 모두에서 재사용할 수 있습니다.

 

function identify(context) {
	return context.name.toUpperCase();
}

function speak(context) {
	let greeting = "Hi, I'm" + identify( context );
    console.log( greeting );
}

identify( you );  // JAM
speak( me );  // Hi, I'm Haneul

비록 바로 위와 같이 콘텍스트 객체를 명시해주는 방식으로 작성해줄 수도 있습니다.

 

저는 위 방법을 주로 사용했던터라 헷갈리는 this 를 왜 사용해야 하는지 더 궁금했습니다. 

사용 패턴이 복잡해지면서 콘텍스트 객체를 명시해주는 방식으로 하면 오히려 코드의 흐름을 보기가 어렵고 지저분해집니다.
뒷부분에서 객체와 프로토타입을 배우고 나면 더 실감하게 된다고 하니 그 부분을 공부하고 내용을 더 추가하도록 하겠습니다.

 

`코어 자바스크립트` 를 통해 this 를 공부했을 때는 this 가 자기 자신이 아니라는 점과 호출자에 따라서 this 가 변한다는 사실을 깨닫고 this 가 무엇인지에 대해서 알게 되었습니다.

그럼에도 불구하고 this 가 그래서 왜 중요한지 왜 잘 알고 있어야 되는지를 몰랐었습니다. 그런데 이 부분을 읽으며 제가 왜 this 를 안일하게 생각했었는지 알 수 있었습니다.

 

function foo(num){
	console.log("foo: " + num);
    
    // foo가 몇 번 호출되었는지 추적
    this.count++;
}

foo.count = 0;

let = i;

for(i=0; i<10; i++) {
	if(i > 5) {
    	foo(i);
    }
}

console.log(foo.count);
// 그래서 foo.count 가 몇일까?

위 코드에서 foo.count 의 수를 예측하겠지만 안타깝게도 숫자를 예측할 필요가 없이 0이 나옵니다.

우리가 this 가 무엇인지 명확하게 알고 있지 않기 때문에 원하는 답을 얻기 위한 도구로 this 를 사용할 수 없는 것입니다.

 

그래서 우리는 우회적인 방법으로 this 대신에

function foo(num){
	console.log("foo: " + num);
    
    // foo가 몇 번 호출되었는지 추적
    data.count++;
}

let data = {
	count: 0
}

let = i;

for(i=0; i<10; i++) {
	if(i > 5) {
    	foo(i);
    }
}

console.log(foo.count);

 다른 객체의 프로퍼티로 옮겨 count 의 값을 얻으려고 하거나?

 

function foo(num){
	console.log("foo: " + num);
    
    // foo가 몇 번 호출되었는지 추적
    foo.count++;
}

	foo.count = 0;

let = i;

for(i=0; i<10; i++) {
	if(i > 5) {
    	foo(i);
    }
}

console.log(foo.count);

foo 렉시컬 스코프에 의지해 위와 같이 문제를 해결합니다.

 

저는 위와 같은 해결책들이 근본적인 해결이 아니라고 생각을 못했었습니다.

하지만 이 글을 읽고 좀 더 명확히 앞으로 어떻게 해야겠구나를 알게 되었습니다.

책의 코드를 그대로 옮겨오면서까지 설명한 이유는 this 의 사용의 의미를 몰랐던 이유에 대해 위 예시로 기록하고 싶었기 때문

 

 

 

그래서 

어떻게 ?

 

this 가 가리키는 것은 함수의 호출부에 따라 결정됩니다. 

 

기본 바인딩

function Haneul() {
		console.log(this.jam);
}

let jam = 4;

Haneul(); 

위 코드에서는 jam 이 전역 스코프에 정의된 전역 변수로 this 는 기본 바인딩을 하여 전역 스코프를 참조하므로 4 를 가져옵니다.

하지만 strict mode 에서는 전역 객체가 기본 바인딩 대상에서 제외되므로 undefined 를 반환하게 됩니다.

 

 

암시적 바인딩

function Haneul() {
		console.log(this.jam);
}

let obj = {
	jam: 4,
    	Haneul: Haneul
};

obj.Haneul(); 
// 호출부

호출부에 따라 this 가 결정된다는 말처럼 호출부가 적혀진 코드를 보겠습니다.

obj 뒤에 Haneul 을 호출하는 것을 볼 수 있습니다. 호출부 바로 앞에 객체가 있을 경우 해당 객체를 바인딩하여 4 를 가져옵니다.

오직 호출부에서만 암시적 바인딩이 적용되는 것이기 때문에 obj.Haneul 이라고 참조되는 경우에 착각하지 않아야 합니다.

 

 

명시적 바인딩

이 부분은 이전에 this 글에도 작성하였듯이 한 눈에 보기에 명시적으로 this 가 연결되는 것을 보게 작성하기 위해서 사용됩니다.

function Haneul(){
	console.log(this.jam);
}

let obj = {
	jam: 4
};

Haneul.call( obj );  // 4
Haneul.apply( obj );  // 4

위와 같이 call 이나 apply 로 지정한 객체 obj 를 참조하도록 명시적으로 바인딩하는 방법이 있습니다. 

이렇게 작성한다면 우리가 원하는 값을 항상 참조할 수 있게 됩니다.

항상 원하는 값을 참조하는 명시적 바인딩만을 사용하지 않는 이유는 이 방법의 단점 또한 존재하기 때문입니다.

자바스크립트의 라이브러리를 사용하는 경우 this 가 강제적으로 이벤트를 유발하는 DOM 의 요소를 가리키도록 지정해놓은 경우 우리가 지정한 this 가 암시적 소실되어 예상치 못한 결과를 반환하는 경우가 생기며 여러 과정에서 암시적 소실이 발생하기도 합니다.

 

 

new 바인딩

생성자 함수를 통해 새로운 객체를 생성하는 경우 새로운 객체는 참조한 객체에 this 를 바인딩하게 됩니다.

 

 

 

** 명시적 바인딩이 암시적 바인딩보다 우선순위가 더 높습니다. 명시적 바인딩 > 암시적 바인딩  암시적 바인딩을 하기 전에 명시적 바인딩이 적용된 곳이 없는지 확인해야 암시적 바인딩이 명시적 바인딩으로 오버라이딩되어 원치 않는 값을 가리키는 것을 막을 수 있겠습니다.

new 바인딩은 암시적 바인딩보다 우선 순위가 높습니다. 고로  new 바인딩 > 암시적 바인딩  이라고 할 수 있습니다.

 

하지만 이 어느것도 무시할 수 있는 것이 있습니다. 그것이 바로 우리가 this 하면 흔히 떠올리고 사용했던, Function.prototype.bind() 입니다. 어떤 종류든 바인딩을 무시하고 주어진 바인딩을 적용하여 하드코딩 된 새 래퍼함수를 생성합니다.

 

 

하드 바인딩 bind()

function Haneul(something) {
	this.jam = something;
}

let obj = {};

let bar = Haneul.bind( obj );
bar( 4 );
console.log( obj.jam );  // 4

let bab = new bar( 2 );
console.log( obj.jam ); // 4
console.log( bab.jam )  // 2

명시적 바인딩의 하나의 형태인 하드 바인딩이 new 바인딩 보다 우선순위가 높다고 볼 수 있습니다.
하지만 바인딩 함수를 new 연산자로 생성한 경우 무시됩니다. 그렇기 때문에 new 연산자로 오버라이딩하여 원하는 this 를 가리키게 할 수 있습니다.

이전에 가짜로 bind 헬퍼를 만들어 바인딩 해주었던 call, apply 를 생각했을 때 , 이들로 작성한 코드를 new 바인딩으로 오버라이딩할 수 없기 때문에 다음 코드들에서 원하는 this 를 가리키기 어려운 상황이 펼쳐지기도 합니다.

 

 

지금까지 바인딩의 종류와 우선순위에 대해서 정리해봤습니다. this 가 무엇을 가리키는지 짐작이라도 하게 되었다면 이제 바인딩 예외로 작용하는 것들에 대해서 알아보겠습니다.

 

call, apply, bind 메서드의 첫 번째 인자로 null 이나 undefined 를 넘기면 this 바인딩이 무시되고 기본 바인딩 규칙이 적용됩니다.

바인딩을 무시하고 기본 바인딩을 하려면 굳이 왜 명시적 바인딩을 이용하는지 궁금하실 것입니다.

apply 는 함수 호출 시 두번째 인자로 다수의 인자를 배열 값으로 보내는 용도로 자주 쓰입니다. bind 또한 유사한 방법으로 인자들을 커링하는 메서드로 사용합니다. 그렇기 때문에 바인딩에 첫 번째 인자를 null 로 주어 명시적 바인딩이 아닌 기본 바인딩이 작용되도록 하는 것입니다.

 

 

어떠한 규칙에서 this 가 무엇을 가리키는지 공부했지만 여러 가지 예외의 상황과 상위 코드들로 예상치 못한 this 를 가리키게 되는 경우를 볼 수 있습니다. 그래서 자바스크립트에서는 ES6 부터 이러한 this 의 헷갈리는 개념을 대체하고자 하는 방안으로 화살표 함수를 사용하도록 했습니다.

 

 

화살표 함수는 this 를 확실히 보장하는 수단으로 bind 를 대체할 수 있기 때문에 그러한 용도로 많이 사용하지만 결과적으로 더 잘 알려진 렉시컬 스코프를 쓰겠다고 기존의 this 의 체계를 버려둔다는 것을 의미하기도 합니다.

자바스크립트에서 원하는 객체를 가리키기 위해서 사용되는 this , 헷갈리는 개념이기 때문에 이를 헷갈리지 않게 고정시키는 개념들도 많이 등장했고 이를 대체할 수 있다고 하는 방법도 공부해봤습니다. 하지만 자바스크립트에서 빠질 수 없는 중요한 개념이기 때문에 this 의 개념과 사용법 , 사용하는 이유에 대해 기억하면서 더 체계적인 자바스크립트 코드를 짤 수 있게 되었으면 좋겠습니다. :)