this
는 함수 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다(모던 자바스크립트 딥다이브 p343). 이는 다른 클래스 기반 객체 지향 언어들의 this
가 반드시 생성된 인스턴스를 가리키는 것과는 많은 차이가 있다. 함수가 호출되는 방식에 따라 동적으로 결정되기 때문에 유연하다는 장점은 있지만, this
의 바인딩을 예측하기 어렵다는 문제가 있다.
자바스크립트의 this
바인딩은 어떠한 경우에서도 일반 함수에서는 전역, 메서드에서는 호출된 객체, 생성자 함수에서는 인스턴스를 가리킨다. 이렇게만 생각하면 어렵지 않은 것 같지만, 사실 사용된 시점의 함수(또는 메서드)가 세 개 상황 중 어떤 상황에 해당하는지를 추론하는 건 생각보다 어려운 일이다.
그래서, 그런 상황들 중 몇 가지를 추려서 정리해 봤다. 전부는 아니겠지만, 공부하면서 this
가 어떻게 작동하는지와,'콜백 함수'가 어떤 식으로 동작하는지에 대해 많이 배울 수 있던 내용이라 남겨둔다.
이벤트 핸들러 콜백 함수의 this
자바스크립트에서 이벤트 핸들러를 붙이는 방법은 HTML 태그의 어트리뷰트로 붙이는 방법, DOM 요소 객체의 프로퍼티로 등록하는 방법, addEventlistener
를 사용하는 방법까지 세 가지이다. 그리고 각각의 방식에 따라 콜백 함수에서의 this
가 바인딩하는 대상에 약간의 차이가 있다.
HTML 어트리뷰트 방식
아래 코드에서 buttonClick
함수 내부의 this
는 전역을 가리킨다.
<body>
<button id="btn" onclick="buttonClick()">클릭</button>
<script>
function buttonClick() {
console.log(this) // window
}
</script>
<body>
이유는, HTML 태그의 어트리뷰트로 등록된 이벤트 핸들러 함수는 onclick
이라는 함수로 한 번 감싸지기 때문이다. pseudo 코드로 나타내보면 대략 이런 식이다.
const $button = {
// ... 다른 프로퍼티와 메서드들
onclick: function onclick() {
function buttonClick() {
console.log(this);
}
}
실제로 HTML 어트리뷰트 방식으로 이벤트 핸들러 콜백 함수를 등록한 요소를 document.getElementId('btn')
으로 선택한 후 console.dir
로 내부 프로퍼티들을 확인해보면 아래와 같이 onclick
이라는 함수가 onclick
이벤트 프로퍼티에 등록되어있는 게 확인된다.
위의 pseudo 코드의 형태가 맞다고 했을 때, buttonClick
함수는 객체의 메서드 내부에 일반 함수로 정의된 중첩 함수이다. '일반 함수의 this
는 전역 객체를 가리킨다'는 규칙에 따라 this
가 전역(브라우저 환경에서는 window
)을 가리킨다.
DOM 요소의 이벤트 프로퍼티 방식
아래 코드에서 buttonClick
함수의 this
는 이벤트 핸들러가 등록된 HTML 태그를 가리킨다(정확하게는, DOM에 있는 HTMLElement
객체를 가리킨다). buttonClick
은 분명 일반 함수로 정의됐는데 이상하다(호출이 다른 방식으로 된 거다).
<body>
<button id="btn">클릭</button>
<script>
function buttonClick() {
console.log(this); // <button id="btn">클릭</button>
}
const $button = document.getElementById('btn');
$button.onclick = buttonClick();
</script>
<body>
해당 코드를 실행하면 pseudo 코드로 아래와 같은 형태가 된다.
const $button = {
// ... 다른 프로퍼티와 메서드들
onclick: function buttonClick() {
console.log(this);
}
이렇게 바꿔놓고 보니 꽤 명확하다. 일반 함수로 정의한 buttonClick
은 실제론 HTMLElement
객체인 $button
의 onclick
프로퍼티에 등록되어 메서드로 호출된다. 실제로 console.dir
로 $button
요소의 프로퍼티를 출력해 보면 onclick
프로퍼티에 buttonClick()
함수가 등록되어 있는 게 확인된다(HTML 어트리뷰트 방식에서 onclick
함수가 등록되고, 내부 함수로 buttonClick
함수가 들어있던 것과는 차이가 있다)
addEventListener
방식
addEventListener
의 두 번째 인자로 넘긴 콜백 함수의 this
는 DOM 요소의 이벤트 프로퍼티 방식과 동일하게 이벤트 핸들러가 달린 HTMLElement
객체를 가리킨다.
<body>
<button id="btn">클릭</button>
<script>
function buttonClick() {
console.log(this); // <button id="btn">클릭</button>
}
const $button = document.getElementById('btn');
$button.addEventListener('click', buttonClick);
</script>
<body>
addEventListener
는 eventTarget
이라는 프로토타입에 정의되어 있으며, 우리가 document
객체를 통해 접근하는 HTMLElement
는 eventTarget
을 상속받아 addEventListener
메서드를 사용하게 된다. 그리고, addEventListener
로 추가한 이벤트 핸들러는 HTMLElement
객체의 프로퍼티로 등록되지 않기 때문에, 사용자가 쉽게 접근해서 값을 확인하기 어렵다.
단, 구글 크롬 개발자 도구의 '요소'탭 '이벤트 리스너' 부분을 보면 대략적으로 이벤트 핸들러가 어떻게 등록되어 있는지를 파악할 수 있다.
handler
라는 프로퍼티에 buttonClick
이라는 함수가 등록되어 있다. 대략, 이런 식이지 않을까 싶다.
const $button = {
// ... 다른 프로퍼티와 메서드들
handler: function buttonClick() {
console.log(this);
}
}
당연히 객체의 메서드로 등록되어 호출되기 때문에, 호출된 객체인 $button
이 this
에 바인딩된다.
이렇게, 이벤트 핸들러의 콜백 함수는 함수의 형태가 일반 함수더라도 실제론 메서드와 같이 호출이 되기 때문에 this
의 값이 주로 전역이 아니라 다른 값을 갖는다. 하지만, 이벤트 핸들러의 콜백 함수가 실제로 어떤 식으로 동작하는지 알지 못한다면 콜백 함수에서의 this
가 무엇을 바인딩하는지 추론하기 어려울 수 있다.
setTimeout
콜백 함수에서의 this
아래와 같은 코드에서 this
는 myObj
를 가리킨다.
const myObj = {
name: 'Ko',
func: function() {
setTimeout(() => {
console.log(this); // myObj
}, 2000);
}
}
myObj.func();
console.log('hi'); // hi <-- 이 코드가 먼저 실행
화살표 함수는 this
바인딩 값을 갖지 않고, 상위 스코프의 this
를 참조하기 때문에, setTimeout
함수 내부의 콜백 함수가 상위 스코프인 myObj.func
스코프의 this
에 바인딩되는 것은 자연스러워 보일 수 있다. 하지만, 처음 이 코드를 봤을 때 'setTimeout
의 콜백이 시작될 땐 실행될 땐 메모리에서 myObj.func
의 실행 컨텍스트가 해제된 이후이기 때문에 정상적으로 this
가 myObj
를 가리키지 못할 것 같다는 생각을 했었다.
하지만, 실제로는 실행 중인 실행 컨텍스트와 상관없이 this
가 myObj
를 잘 가리키고 있었다. 이유를 생각해 보면, setTimeout
의 콜백 함수 내부의 this
가 클로저(Closure)로 동작한 게 아닐까 싶다.
실행 순서를 생각해보면 이렇다. 먼저, 전역 실행 컨텍스트가 콜 스택에 쌓이고, myObj.func
가 호출된다. 그리곤 myObj.func
의 실행 컨텍스트가 생성되어 콜 스택에 쌓이고 '실행 중인 실행 컨텍스트'가 된다. 그 다음으론 setTimeout
의 실행 컨텍스트가 생성되지만, 해당 실행 컨텍스트는 내부 콜백 함수를 이벤트 큐에 밀어넣은 뒤 곧바로 종료된다(해당 이벤트 큐는 2000ms 이후에 다시 호출된다). 그 다음 myObj.func
실행 컨텍스트가 종료되고, 외부에 있는 console.log('hi')
의 실행 컨텍스트가 생성되어 실행된 후 소멸한다. 그 다음 2000ms 후에 console.log(this)
의 실행 컨텍스트가 콜 스택에 올라가고, this
가 호출된다. 마지막에 this
가 호출되고 난 이후엔 전역 실행 컨텍스트가 해제되고 실행이 종료된다.
마지막 전역 실행 컨텍스트가 종료되기 전에 console.log(this)
가 실행될 땐 myObj.func
의 실행 컨텍스트는 소멸한 이후이다. 하지만, console.log(this)
가 myObj
를 잘 참조하고 있는 것은 this
에 대한 정보가 myObj.func
실행 컨텍스트 종료 이후에도 살아있다는 뜻이고, 바꿔 말하면 클로저로 동작하게 된 셈이다.
이렇게 실행 순서가 바뀌더라도 this
에 대한 정보는 클로저로 정보를 캡처하고 저장하기 때문에, '화살표 함수는 상위 스코프의 this
를 참조한다'는 것은 유지된다.
화살표 함수로 지정한 메서드의 this
this
바인딩이 흔들리는 것 때문에 화살표 함수 사용을 권장하는 걸 봤다. 특히, 콜백 함수에서는 화살표 함수를 사용하여 상위 스코프의 this
로 this
를 고정하는 게 더욱 권장되는 것 같다.
하지만, 모든 상황에서 화살표 함수가 왕도는 아니다. 예를 들어서, 아래와 같이 메서드를 화살표 함수를 사용해 선언할 경우 this
는 전역을 가리킨다. 화살표 함수에서의 this
는 상위 스코프의 this
를 의미하는 것이기 때문에, 메서드 실행의 상위 스코프인 전역이 참조되는 것이다.
const myObj = {
func: () => {
console.log(this);
}
}
myObj.func() // window
실제로, MDN 문서에서 화살표 함수를 설명하면서 아래와 같은 문장을 사용하였다.
Arrow functions don't have their own bindings to this, arguments, or super, and should not be used as methods.
화살표 함수는 메서드를 정의할 때에는 사용하지 않는 게 바람직하다.
결론
this
의 바인딩이 헷갈리는 몇 가지 상황에 대해 살펴보았다. 전반적으로 this
를 추측하기 어려운 상황은 함수가 어떠한 형태로 사용되는지 알기 어려운 때라는 생각이 든다. 자바스크립트는 태생이 유연성을 강조하고, 재사용성을 넓은 폭으로 허용하기 때문에, 일반 함수로 정의된 함수도 메서드가 될 수 있고, 메서드도 일반 함수가 될 수 있다. 이러한 언어적 특성을 고려해서, 정확하게 개념을 이해하고 쓸 수 있게 공부의 깊이를 깊게 파야겠다는 생각이 든다. 아무튼, 실전에서 this
때문에 애먹지 않게 앞으로도 틈틈이 케이스들을 잘 정리해 둬야겠다.
'JavaScript' 카테고리의 다른 글
순수 함수에 대해서 (1) | 2024.12.06 |
---|---|
Date 객체 사용하기 (2) | 2024.11.15 |
프로토타입 톺아보기 (1) | 2024.11.13 |
241102 TIL (1) | 2024.11.03 |
자바스크립트 엔진과 런타임 (2) | 2024.10.30 |