ivory's Log
그게 무엇이라도 항상 쉬운 일이다.

ivory's DevLog

[JavaScript] - this

ivorycode 2020. 10. 4. 13:51
반응형

이 글은 JavaScript의 this에 대해 적은 글입니다. 이 포스팅을 포함해 게시된 모든 포스팅의 큰 주제는 순차적인 흐름을 지키지 않습니다. 혼란에 주의해 주세요!


this를 파헤쳐보자! 이미지 출처: www.google.com

내가 JavaScript를 처음 공부할 때, 가장 어려워했고 이해하기 힘들어했던 개념 중 하나다. 여담으로 정말 이 개념이 어려워서 부트캠프 진행하는 내내 구시렁댔던 기억이 난다. 그래서 오늘은 this에 대해 깊이 있는 공부를 해보려고 한다.

 

this

사전적 의미와 비슷한 JavaScript의 this

this는 사전적 의미로 '이것'이라는 뜻을 가졌는데, 지금부터 알아볼 this는 사전적 의미와 비슷한 기능을 가졌다. JavaScript의 this는 함수 내에서 호출 맥락(Context)을 의미하고 있다. 조금 말이 어려운데 쉽게 말하면, 함수를 어떻게 호출하냐에 따라 this가 지정하는 대상(값)이 달라진다는 의미다. 

 

console.log(this)

말로만 하면 너무 어렵고 이해하기 어려우니, 코드를 써보며 이해해보자. 먼저 this를 console로 확인해보자.

 

this는 window 객체였다. 이미지 출처: 내가 직접 쓴 코드

무작정 코드를 써본 결과 this는 window객체였다. 즉, this는 기본적으로 window객체라는 말이다. 하지만, this는 호출 맥락(Context)에 따라 값이 바뀐다고 설명했는데, this의 동작 방식을 알아보면서 정리해보자!

 

this의 동작 방식

기본 바인딩(Binding)

중복된 내용이지만, this는 기본적으로 전역 객체를 가리키고 있다. 브라우저에서는 window객체를, Node환경에서는 global객체를 가리킨다. 추가로 전역 객체는 일반 객체처럼 아무 제약 없이 동작한다. 때문에 this를 무분별하게 사용하면 전역 상태에 영향을 줄 수 있으니 반드시 유의하자! this의 기본 바인딩에 대한 정리는 이것으로 마치겠다.

김성모의 '대털' 이미지 출처: www.google.com

🤔바인딩(Binding)?

바인딩(Binding)이란 함수 호출과 실제 함수를 연결하는 방법이다. 예를 들어, 호출하는 부분에 함수가 위치한 메모리를 연결시켜주는 것을 바인딩이라고 할 수 있다. 바인딩에는 크게 2가지가 있다.

 

- 정적 바인딩(Static Binding)

실행 시간 전에 일어나고, 실행 시간에는 변하지 않은 상태로 유지되는 바인딩. ES6부터 사용 가능한 화살표 함수가 가장 대표적인 예시다. 화살표 함수는 코드상 상위 블록의 맥락(Context)을 this로 바인딩하는 규칙을 갖는다. 이 부분은 다시 정리해보겠다.

 

- 동적 바인딩(Dynamic Binding)

실행 시간에 이루어지거나 실행 시간에 변경되는 바인딩. 늦은 바인딩(Late Binding)이라고도 불림.

 

this의 암시적 바인딩(Binding)

예제 코드를 참고하며 암시적 바인딩에 대해 알아보자.

 

이미지 출처: 내가 직접 쓴 코드

암시적 바인딩은 함수 호출 시, 객체의 프로퍼티에 접근하여 실행하는 것을 말한다. 위의 코드는 obj객체 test 프로퍼티에 함수를 할당한 후 객체를 통해 함수를 호출하는 코드다. test함수가 실행될 때, 객체 프로퍼티에 접근할 경우 이 객체를 this와 바인딩하는 규칙을 갖는다. 그런데 위와 같이 객체 프로퍼티에 함수를 할당해도 this와 바인딩하지 못한 경우가 있었다.

 

이미지 출처: 내가 직접 쓴 코드

처음에 작성한 코드와 같이 obj프로퍼티에 함수를 할당했다. 하지만 testThis에 레퍼런스를 저장했더니 pizza가 아닌 hamburger를 호출하고 있었다. 아까 this의 동작 방식을 설명할 때, 기본 바인딩을 기억하는가? this는 기본적으로 전역 객체를 가리킨다고 했었다. 이렇게 전역에 생성한 변수는 전역 객체에 등록되기 때문에, test 함수this.foodpizza가 아닌 hamburger로 바뀌게 된 것이다. 정리하면 this의 기본 바인딩과 내용이 비슷한데, this를 그냥 사용하게 되면 암시적으로 전역 객체에 바인딩된다는 것이다.

 

🤔콜백 함수를 사용할 때도 같은 상황이 발생한다.

만약에 콜백 함수의 인자에 다음 예시와 같이 인자를 넣었다고 가정해보자.(위의 예시 코드 이미지에서 testThis를 적용하였습니다. 예시 코드를 확인해주세요!!)

 

setTimeout(testThis, 1000)

 

이렇게 하여도 obj객체의 값을 상관하지 않고, 전역 상태의 값을 바인딩하게 되어 pizza가 아닌 다른 값을 호출하게 된다.

 

this의 명시적 바인딩(Binding)

아까 전역에서 생성한 변수들은 전역 객체에 등록되기 때문에, this의 값에도 영향을 받는다고 강조했다. 콜백 함수도 마찬가지였다. 그렇다면 이것들을 해결하는 방법은 무엇이 있을까?

 

call(), apply(), bind()

 

위의 함수들은 JavaScript의 내장 함수들이다. 이 함수들을 통해 this를 명시적으로 바인딩할 수 있다. call() 내장 함수를 대표로 예제 코드를 살펴보자.

 

이미지 출처: 내가 직접 쓴 코드

디버깅 창을 보면 알다시피, call함수를 사용하여 바인딩할 객체를 명시하고 있다. 이렇게 하여 test함수를 실행하게 되면, this는 전역 객체를 기본 바인딩하지 않고 obj 객체를 바인딩하여 this는 pizza의 값을 가지게 된다.

 

this의 new 바인딩(Binding)

JavaScript 코드를 살펴보면 함수 앞에 new라는 키워드가 붙여진 것을 본 적이 있다.(뭐가 새롭다는 건데?...) new를 붙이면 어떤 일이 일어날까?

 

- 새로운 객체를 반환한다.

- 객체의 메서드를 호출하면 this로 바인딩된다. 

 

대체 무슨 소린지 모르겠다. 예시 코드를 보며 삽질해보자.

 

오늘 점심은 피자다... 이미지 출처: 내가 직접 쓴 코드

fastfood함수 안에 food라는 변수와 this로 접근한 franchisedomino라는 값을 넣었다. 함수 안의 this부터 살펴보자. 우선 이 this는 전역 객체를 가리키고 있는 상태라서 franchise는 전역 객체에 할당된다. 그렇기 때문에 fastfood 함수를 실행하게 되면, 함수 안의 this.food는 언급한 전역 객체에 할당된 this전역 상태의 변수 food의 영향을 받아 pizza domino가 아닌 hambuger domino를 반환하게 된다. 이것은 암시적 바인딩 내용을 이해했다면, 충분이 납득이 가능하다.

 

하지만, 밑의 test 변수의 new fastfood() 부분을 살펴보자. 디버깅 결과부터 보면 undefined domino를 반환하고 있다. 이렇게 new 키워드를 붙여 선언하게 되면 this는 더 이상 전역 객체를 가리키지 않고 생성된 객체 자체를 가리키게 된다. 아래의 이미지에서 console.log(test)가 나타내는 것을 확인해보자.

 

console.log()를 생활화 해보는 습관 이미지 출처: 내가 직접 쓴 코드

결과를 보면 알겠지만, 이제 new fastfood() 객체에는 franchise값만을 가지고 있고, console.log() 안의 this.food는 참조할 수 없는 상태다. 따라서, this로 접근한 franchise는 정상적으로 값을 출력하고 있고, food는 더 이상 전역 객체가 아니므로 undefined를 반환하게 된다.

 

화살표 함수와 this

화살표 함수는 기존의 바인딩 규칙을 따르지 않는다.

(언제나 예외는 있다...) ES6부터 사용 가능한 화살표 함수는 기존의 맥락(Context) 바인딩 규칙을 따르지 않는다. 기존에 언급했던 바인딩 방식들은 실행 시점에 바인딩 규칙이 적용된다(동적 바인딩. 위에서 언급했다!!) 반면, 화살표 함수는 정적 바인딩으로 실행하지 않아도 규칙을 파악할 수 있다. 아까도 잠시 언급했지만, 화살표 함수는 코드상 상위 블록의 맥락을 this로 바인딩하는 방식을 가졌다고 했다. 예제 코드를 보며, 공부해보자.

 

이미지 출처: 내가 직접 쓴 코드

(구분선 기준) 위의 코드와 아래의 코드의 차이점은 함수 선언 방식이다. 위의 코드는 일반적인 방법으로 함수를 선언했고, 아래의 코드는 화살표 함수를 사용하였다. 코드를 잠시 살펴보면, 객체 안의 test에서 this.textforEach함수를 통해 this.text 접근하려 하고 있다. 하지만, 결과는 다른 값들을 반환하고 있다.

 

먼저, 일반 함수부터 알아보자. test에서 바로 접근한 this.text는 정상적으로 바인딩하여 text값을 정상적으로 불러오지만, forEach함수this.text에서는 다시 전역 객체를 가리키기 때문에 undefined를 출력하게 된다.

 

화살표 함수는 일반 함수와 다르게 forEachthis.text값을 정상적으로 출력하였다. 그 이유는 화살표 함수의 내부에서 this는 전역 객체를 가리키지 않고, 부모 객체(위의 코드에선 obj가 되겠다.)를 가리키기 때문이다.

 

이런 이유로 React Vue에서 하위 컴포넌트로 함수를 전달하기 위해 상위 컴포넌트의 맥락(Context)을 사용해야 하는 경우 화살표 함수를 사용한다.

 

this 바인딩의 우선순위와 this를 참조하는 방법

알아야 할 내용이 또 있다... 이미지 출처:  www.google.com

위의 예시들로만 코드를 작성한다면, this의 개념이 정말 쉬울 것이다. 하지만, 인생이 쉽지 않듯이 this도 호락호락하지 않다. this 바인딩을 섞어 쓰게 된다면 발현되는 일반적인 우선순위와 this를 참조하는 방법을 알아보자.

 

this 바인딩 우선순위

 

1. new 바인딩

2. 명시적 바인딩

3. 암시적 바인딩

4. 기본 바인딩

 

this가 무엇을 가리키는지 참조하는 법

 

#1. 함수의 호출 부분을 확인한다.

#2. 객체를 통하여 함수를 호출 중이라면, this의 값은 해당 객체를 참조하고 있는 값이다.

#3-1. #2의 경우가 아니라면 다음 내장 함수가 선언되었는지 확인한다.

 

call(), apply(), bind()

 

#3-2. 위의 내장 함수의 첫 번째 인자가 바로 this가 참조 중인 값이다.

#4. #3의 경우가 아니라면, 함수가 new 키워드를 통해 호출되는지 확인한다. 만약 그렇다면, this는 새롭게 생성된 객체의 값을 참조 중이다.

#5. #4도 아니라면, 화살표 함수 안에 함수 안에 this가 있는지 찾아보자. 화살표 함수 안에 있다면, 부모 스코프에서 this가 무엇을 참조하는지 알 수 있다.

#6-1. #5의 경우가 아니라면, strict mode인지 확인한다.

 

🤔함수 내부에서 strict mode 사용
함수 내부에서 strict mode를 사용한다면 함수 내부에서 this는 전역 객체를 바인딩하지 않는다.

function test() {
'use strict';
console.log(this === window) // false
}

test
()
console.log(this === window) // true

🤔함수 외부에서 strict mode 사용
함수 외부에서 strict mode를 사용한다면 모든 함수 내부의 this는 전역 객체를 binding 하지 않는다.

'use strict';
function test() {
console.log(this === window) // false
}

function test2() {
console.log(this === window) // false
}

test()
test2()
console.log(this === window) // true

 

#6-2. strict mode를 사용했다면, this는 undefined가 할당된 상태다.

#7. #6도 아니라면, this는 최종적으로 전역 객체를 참조 중인 상태다. 브라우저라면 window를 Node 환경이라면 global!!

 

정리하며...

this를 공부하며 맥락(Context)에 대한 용어를 자주 마주칠 수 있었다. 잠시 알아봤는데 실행 컨텍스트(Execution Context)scope, hoisting, this, function, closure 등의 동작원리를 담고 있는 JavaScript의 핵심원리였다. 물론 이 실행 컨택스트를 깊게 알지 못해도 this의 기본적인 개념을 공부 못하는 것은 아니었지만, 한계가 있었다. 그래서 이번 포스팅은 다른 포스팅보다 더 힘들었던 것 같다.(부족한 부분을 또 알게 되었다.)

 

이번 포스팅은 여기서 마치지만, 좀 더 여유가 된다면 언젠가 실행 컨택스트를 깊이 있게 공부하고 정리해보도록 하겠다.(언제가 될지 모르겠다...)

반응형

'ivory's DevLog' 카테고리의 다른 글

Webpack  (0) 2020.10.28
Babel  (0) 2020.10.18
CORS란?  (2) 2020.10.02
SSR(Sever Side Rendering)과 CSR(Client Side Rendering)  (1) 2020.09.30
Cookie? Local, Session Storage??  (0) 2020.09.25