내가 보려고 만든 개발 공부 일지

자바스크립트 - this 바인딩 본문

Javascript

자바스크립트 - this 바인딩

kwangsunny 2022. 3. 23. 23:42

자바스크립트로 개발을 하다보면 this를 다룰일이 많이 있다.

자바스크립트에서 this는 현재 코드가 실행되는 상황에 따라 할당되는 값이 달라지는데, 언제 어떤 값이 this에

할당되는지 제대로 이해하지 않고 대충 얼버무리고 넘어간다면 어디가서 JS좀 한다 같은 소리는 고이 접어두자.

 

"this 바인딩 이 정도면 다 이해한듯?" 그 당시엔 이렇게 생각이 들었어도, 시간이 흐르고 this로 복잡한 작업을 하다보면 금세 또 헷갈리고 인터넷을 찾게된다. 그래서 이번엔 this에 대해 최대한 정리해 보려한다.     

 

 

엄격모드 - "use strict"

this에 대해 살펴보기 전에 우선 엄격모드에 대해 알아볼 필요가 있다.

이 엄격모드에 따라 this가 가리키는 값이 상황에따라 달라지기 때문이다. 

 

엄격모드란?

자바스크립트는 계속해서 업데이트되고 있는 언어이다. 그래서 시간이 흐름에따라 새로운 문법들과 기능들이

추가 변경 되면서 발전해왔다. (let, const, 화살표함수, 클래스, 스크립트 모듈화, 등등..)

추가된 기능이야 그렇다 쳐도 수정된 기능을 그대로 모두 적용해버리면 수많은 레거시 코드들이 오작동을 일으키게 될 것이다.

이를 방지하고자 자바스크립트 설계자는 '엄격모드' 에서만 이런 변경사항들이 적용돼도록 만들었다.

 

엄격모드를 적용하는 방법은 간단하다.

스크립트 제일 상단에 문자열 "use strict" 를 명시해주면 된다. 

그럼 그 다음 줄부터 실행되는 코드들은 모두 모던한 방식으로 작동하게 될 것이다.

만약 "use strict" 를 스크립트 최상단이 아닌 중간에 쓰면 엄격모드가 적용되지 않으니 이점을 주의하자.

 

전체 스크립트가 아니라 특정 함수에서만 엄격모드를 적용할 수도 있다.

아래 예제처럼 함수 내부의 가장 상단에 "use strict" 를 명시해주면 이 함수만 엄격모드에서 실행된다.

function test(){
    'use strict'
    // 이 함수만 엄격모드에서 수행됨
}

 

참고로 모듈 스크립트는 엄격모드가 기본값이라 따로 "use strict" 를 명시할 필요가 없다.

<script type="module">
  // 엄격모드로 실행됨
</script>

<script>
  'use strict' // 스크립트 최상단에 명시해야 엄격모드 적용됨
</script>

 

 

함수와 this

일반적인 함수를 호출했을때, 함수내에 this는 무엇을 담고 있을까.

function fn(){
    console.log(this); // (A)   -> window ? undefined ?
}
fn();

함수를 호출하면 (A)에서 전역객체 window가 출력될 것이고, 엄격모드라면 undefined 가 출력될 것이다.

이것이 this 바인딩의 가장 기본적인 상황이다.

 

그런데 자바스크립트에서 모든 함수는 생성자 함수이기도 하다. 그래서 함수를 호출할 때 앞에 new 를 명시하면

해당함수는 객체를 반환하게 되고 생성자 함수 내부에서 사용된 this는 반환된 객체를 가리키게 된다.

 

생성자 함수로 객체를 생성하는 예시를 보자.

function Person(){
    this.name = 'sunny';
    this.showThis = function(){
        console.log(this);
    }
}

let me = new Person();
me.showThis(); // (A)

this 출력 결과

(A)에서 me.showThis 를 실행하여 this를 출력해보면 this는 생성된 객체 자신인걸 확인 할 수 있다.

함수가 new를 만났을때 객체가 생성되는 과정을 좀더 자세히 보면 아래와 같다.

function Person(){
    // this = {} --> 빈 객체가 할당된 this가 암묵적으로 만들어진다.
    this.name = 'sunny';
    this.showThis = function(){
        console.log(this);
    }
    // return this; --> 암묵적으로 this를 리턴한다.
}

new Person() 이 실행되면 위 주석에 달린 설명같이 자바스크립트가 암묵적으로 빈 객체 this를 생성하고

프로퍼티 값들을 (이 예제에선 name, showThis) 할당한 후 함수 끝에서 이 this를 반환한다.

결국 new Person() 이 반환한 this가 새로운 객체가 되는 것이다.

 

그래서 객체 프로퍼티에 할당된 함수 내부에 this가 있다면(위 예시의 showThis),

이 this는 함수가 속한 객체를 가리키게 된다.

이에 대한 설명은 다음에 나오는 메서드와 this 에서 더 알아보자.

 

 

메서드와 this

자바스크립트에서 함수는 다재다능하다. 함수는 다른 함수의 인자로 전달될 수 있고,

객체 프로퍼티에 할당될 수 있고, 객체처럼 쓰일 수도 있다.

이 중에서 함수가 어떤 객체의 프로퍼티에 할당되면 이 함수를 메서드 라고 부른다.

메서드는 this를 통해 메서드가 속한 객체에 접근할 수 있다.

 

그러면 함수가 그냥 호출될때 와 메서드로써 호출될때 this는 어떻게 달라지는지 아래 예시를 보자.

var myName = 'kwang'; // 함수 바깥에서 선언된 var 변수는 전역객체의 프로퍼티가 됨.

function fn(){
    console.log(this);
    console.log(this.myName);
}

let obj = {
    myName: 'sunny',
    func: fn, // (A)
}

fn(); // (B)
obj.func(); // (C)

 

(A)에서 obj 의 프로퍼티로 함수 fn을 할당해주고 있다. 이제 obj.func 은 obj 의 메서드라고 부를 수 있다.  

그럼 fn 이 일반함수로써 호출됐을때 와 메서드로서 호출됐을때의 결과는 어떻게 나올까?

실행 결과

결과는 완전 다르게 나왔다.

우선 (B) 처럼 그냥 호출하면 this에는 전역객체 window가 할당되기 때문에 this.myName 도 

window의 myName 프로퍼티를 출력하게 된다.

반면 (C) 에서는 obj의 메서드로서 호출되었기 때문에 이때 fn 내부의 this 는 자신을 호출한

객체가 담기게 되어 this.myName 의 값은 obj.myName와 같다.

 

만약 객체 A, B가 있다고 할때, A의 메서드를 B의 프로퍼티로 할당해주면 어떻게 될까?

B에서 이 메서드를 실행하면 this 는 A, B 중 어떤 객체를 가리키게 될까?

let A = {
    myName: 'A',
    func: function(){
        console.log(this);
        console.log(this.myName);
    }, 
}

let B = {
    myName: 'B',
    func: A.func,  // (A)
}

A.func();
B.func();

실행 결과

실행결과를 보면 this는 호출된 함수가 속한 객체를 가리키고 있다. 

(A) 에서 B.fucn = A.func 를 해주고 있는데 왠지 B.func 를 실행하면 this 는 A 를 가리킬것 같지만 그렇지 않았다.

메서드는 자신을 호출한 객체를 this로 할당한다. 그래서 같은 함수를 공유한다 하더라도 this의 값은

호출되는 시점에 어떤 객체에서 호출했냐에 따라 그 메서드의 this 값은 달라질 수 있다.

 

코드상으로 봤을때 this는 점(.) 앞에 객체를 참조하게 된다.

그래서 A.func() 에서 func의 this는 점(.)앞에 있는 A 가 되고, 마찬가지로 B.func() 에서 func의 this는 B를 가리키게 된다. 

 

그러면 위처럼 메서드가 객체에 의해 실행되는게 아니라 따로 떨어져 나와 단독으로 호출되면 this에는

어떤 값이 들어있을까? 예제를 통해 알아보자.

let obj = {
    myName: 'sunny',
    func: function(){
    	console.log('name is ' + this.myName);
    }
}

let say = obj.func; // (A)
say();

setTimeout(obj.func, 1000); // (B)

실행 결과

(85는 setTimeout이 반환한 타이머값 이므로 무시!)

메서드가 단독으로 호출된다는 의미는 위의 (A), (B) 와 같은 상황들을 말한다.

 

메서드가 object.method() 와 같은 형태로 호출되지 않고 어떤 변수에 담기거나, 콜백함수로써 다른 함수에 전달되어

호출될때, 메서드는 일반함수를 호출할 때와 같은 규칙을 따르게되서 this는 전역객체를 가리키게 된다.

 

(A)에서 obj.func 메서드는 say 라는 변수에 담기고 다음 줄에서 obj.func() 형태로 obj 라는 객체에 의해 호출되는것이

아니라 say() 와 같이 단독으로 호출되고 있다.

 

(B)의 경우 setTimeout의 콜백으로 obj.func 메서드가 전달되고 있는데, setTimeout 은 obj라는 객체가

있는지는 전혀 알지 못하고 단지 obj.func 이 가리키는 함수만 알고 있을 뿐이다. 

그래서 1초 후에 실행되는 콜백은 일반함수처럼 실행된다.

 

두 실행 결과가 모두 sunny가 아닌 undefined로 나온 이유도 메서드가 일반함수처럼 호출되어 this가 window 객체를

가리키므로 window.myName --> undefined 가 되기 때문이다.

(엄격모드에서 실행했다면 this == undefined 이므로 에러가 발생할 것이다.)

 

그럼 여기서 문제.

'use strict'
function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser(); // (가)

console.log(user.ref.name); // (A)

(A) 에서 출력되는 값은 뭘까? ( 참고로 엄격모드로 실행했다 -> 'use strict' )

.

.

.

정답은 에러가 발생한다.

출력 : Cannot read properties of undefined (reading 'name')  

 

위 코드에서 makeUser 함수는 객체를 반환하고 있고, 이 객체는 ref 프로퍼티 값으로 this를 가지고있다.

(가) 에서는 makeUser함수를 호출하고 있다.

이때 makeUser는 객체에 의해 호출된것이 아니라 일반 함수로써 호출된 것이기 때문에, this는 (엄격모드 이므로) undefied를 가리키게 될 것이고, 변수 user는 { name: 'John', ref: undefined } 가 될것이다.

그래서 (A)에서 에러가 발생하게 되는것이다.

 

위 문제 예시는 아래 링크를 참고했다. this 바인딩에 대해 어설프게 알고 있었다면 꽤나 헷갈릴만한 좋은 예시이다.

링크에는 문제에 대한 해설이 더 자세히 나와있으니 잘 읽어보면 좋을것 같다.

https://ko.javascript.info/task/object-property-this 

 

객체 리터럴에서 'this' 사용하기

함수 makeUser는 객체를 반환합니다. 이 객체의 ref에 접근하면 어떤 결과가 발생하고, 그 이유는 뭘까요? function makeUser() { return { name: "John", ref: this }; }; let user = makeUser(); alert( user.ref.name ); // 결과가

ko.javascript.info

 

 

 

 

this의 명시적 바인딩

위에서 메서드가 단독으로 어떤 변수에 담겨서 호출되거나, 콜백함수로써 호출되면

일반 함수가 호출될때처럼 this에는 전역객체 (엄격모드에선 undefined) 가 할당된다고 했다.

그럼 이런 경우에 this가 바뀌지 않도록 고정해줄 수 있는 방법은 없는걸까?

 

어떻게 하면 메서드가 단독으로 호출돼도 this를 잃지 않고 자신이 속한 객체를 this로써 사용할 수 있을지   

아래 예시를 통해 살펴보자.

 

1. 외부 렉시컬 환경 이용하기

function test(){
    let obj = {
        myName: 'sunny',
        func: function(){
            console.log('name is ' + this.myName);
        }
    }
    
    setTimeout(obj.func); // (A)
    
    setTimeout(function(){ // (B)
    	obj.func();
    });
}

test();

실행 결과

가장 간단한 방법은 외부 렉시컬 환경에서 해당 객체에 직접 접근하는 것이다.

함수는 호출될때 렉시컬 환경이 생성되고 이 렉시컬 환경은 외부의 렉시컬 환경의 참조값을 가지고 있어서

외부 변수에 접근할 수 있는데 이 원리를 이용하는 것이다.

 

(A)에서 setTimeout의 콜백으로 메서드를 바로 넘겨준것과 달리 (B) 에서는 익명함수를 넘겨주고 있다.

이 익명함수는 자신의 외부 렉시컬 환경인 test함수의 렉시컬 환경을 알고 있으므로 obj에 접근할 수 있다.

그래서 obj.func() 형태로 메서드가 호출되었기 때문에 (B)의 실행 결과는 undefined 가 아니라 sunny가 출력되게 된다.

 

 

2. 바인딩 메서드 이용하기

두번째 방법은 this에 어떤 객체를 바인딩할지 직접 명시해주는 것이다.

함수 내장 메서드인 bind / call / apply 를 사용하면 호출된 메서드 내의 this가 가리키는 값을 직접 정해줄 수 있다.

함수의 내부 구조

위 그림에서 볼 수 있듯이 함수는 new Function() 에 의해 만들어진 객체이고, Function의 프로토타입에 bind, call, apply 가 이미 정의되어 있기 때문에 모든 함수는 bind, call, apply 메서드를 기본으로 사용할 수 있다.

 

bind 메서드

func.bind(obj) 와 같이 bind 메서드의 첫번째 인자에 객체를 넘겨주면, this가 obj 로 고정된 새로운 함수가 반환된다.

function fn(){
    console.log(this);
}
let boundFunc = fn.bind({a: '123'});

boundFunc(); // (A)
fn() // (B)

위 코드의 (A) 부분에서 출력되는 내용은 아래와 같다.

bind 사용에 따라 this의 값이 달라졌다

(B)처럼 그냥 함수를 호출한 경우 당연히 this는 전역객체를 가리키게 되지만,

(A)에서는 this가 bind 의 첫번째 인자로 넘겨준 객체 {a: '123'} 을 가리키고 있다.

이런 식으로 bind 를 사용하면 메서드가 단독으로 사용될때 처럼 this값이 변하는 상황에 명시적으로 this를 

고정해줄 수 있게 된다. 

 

이 방법으로 setTimeout의 콜백으로 함수를 넘겨줄때도 bind 메서드를 사용하면 this 값을 의도한대로 쓸 수 있다.

function test(){
    let obj = {
        myName: 'sunny',
        func: function(){
            console.log('name is ' + this.myName);
        }
    }
    setTimeout(obj.func.bind(obj)); // 출력 : name is sunny
}

test();

 .bind(obj)를 통해 반환된 함수는 this가 obj로 고정되어 원하는대로 잘 실행되었다.

 

참고로 bind는 첫번째 인자 뒤에 다중 인자를 받을 수 있다.

bind(context, arg1, arg2, arg3, .... ) 이런 식으로 사용할 수 있는데, 두번째부터 넘겨지는 인자들은 bind가 반환한

함수가 실행될때 순서대로 함수의 인수로 들어온다.

function fn(a, b, c){
    console.log(`${this} / ${a} / ${b} / ${c}`);
    console.log(arguments);
}
let test = fn.bind(null, 1, 2); // (A)
test('test'); // (B)

바인딩 함수 실행 결과

(A)에서 fn의 바인딩 대상으로 null 을 넘겨주고 있는데 이땐 일반함수를 호출할때와 같이 this에는 전역객체가

바인딩된다. 또 그 뒤로 1, 2 를 차례대로 넘겨주고 test 에 바인딩된 새로운 함수가 할당된다.

 

(B)에서 바인딩된 test 함수를 실행하면 위와 같은 결과를 얻을 수 있는데,

test함수를 호출할때 넘겨준 test보다 bind 메서드를 호출할때 넘겨준 1, 2 가 더 먼저 위치하게되는 모습을 볼 수 있다.

이 기능을 이용하면 this 뿐만아니라 인자들도 미리 고정시킨 새로운 바인딩 함수를 만들어 사용할 수 있다.

function add(a, b){
    console.log(a + b);
}
let add3 = add.bind(null, 3);
let add5 = add.bind(null, 5);

add3(10); // 3 + 10 = 13
add5(10); // 5 + 10 = 15

위 add 함수를 bind 메서드의 인수 고정을 이용해 특정 기능을 수행하는 함수를 만들어낼 수 있고,

이를 부분적용 이라고 부른다.  

   

마지막으로, bind 메서드를 통해 반환된 함수는 다시 bind를 해도 this값은 바뀌지 않는다.

두 번 bind를 실행한 결과

위 그림에서 볼 수 있듯이, bind 를 통해 반환된 함수가 다시 bind로 다른 객체를 넘겨주어도 this의 값은

바뀌지 않고 처음 바인딩된 객체를 가리킨다.

(인수도 넘겨주었다면, 인수들도 역시 처음 바인딩된 상태에서 바뀌지 않는다.)

 

call, apply 메서드

call 과 apply 메서드는 한 가지만 빼고 bind 와 똑같다.

차이점은 바로 bind 메서드가 새로운 함수를 반환하는것과 달리 call, apply 메서드는 그 자리에서 함수를 호출한다는 것이다.

call 실행 모습

실행 결과를 보면 bind와 달리 call은 해당 함수를 그 자리에서 바로 호출한다.

apply 메서드는 call과 똑같은데 추가 인자를 넘기는 방식만 다르다.

call 은 bind 메서드처럼 추가 인자들을 하나씩 콤마로 구분지어 넘겨주는데 반해, apply 메서드는 두번째 자리에

유사배열 객체를 받는다.

apply 메서드로 추가 인자를 넘기는 모습

call 메서드가 추가 인자를 하나씩 넘기는것과 달리 유사배열 객체(프로퍼티 키가 숫자고, length 프로퍼티를 가진 객체) 를 넘겨주고 있지만 실행 결과는 두 메서드가 똑같다. 

 

 

이벤트 콜백과 this

html 요소 div 가 있다고 하자. 이 div를 클릭했을때 호출되는 핸들러 함수에서 this는 무엇을 가리키고 있을까?

 

html 요소에 이벤트를 핸들러를 할당하는 방식은 세 가지가 있다.

1. html 속성값으로 할당

2. on<event> 프로퍼티로 할당

3. addEventListener 로 할당 

 

위 세 가지 방식중 뭘 사용하든 이벤트 콜백에서 this는 이벤트가 발생한 요소를 가리키게된다.

즉, this === event.currentTarget 이 된다.

그럼 진짜 그렇게 되는지 확인해보자. 

<button onclick="clickA(event)">A</button>
<button id="btnB">B</button>
<button id="btnC">C</button>

<script>
    function clickA(e){
    	console.log(this);
        console.log(e.currentTarget === this);
    }
    
    btnB.onclick = function(e){
    	console.log(this);
        console.log(e.currentTarget === this);
    }
    
    btnC.addEventListener("click", function(e){
        console.log(this);
        console.log(e.currentTarget === this);
    });
</script>

이벤트 핸들러 할당 방식 1, 2, 3 순서대로 버튼 A, B, C 를 만들어 보았다.

그리고 각각의 핸들러에서는 this값과 해당 이벤트가 발생된 요소(currentTarget)가 같은 놈인지를

출력해줄 것이다.

 

한 번씩 클릭한 결과는 아래와 같다.

각 버튼을 클릭한 결과

on<evnet> 방식과 addEventListener 방식의 콜백함수에서 this는 이벤트가 발생한 요소를 가리키고 있는

모습을 확인할 수 있다.

그런데 html 속성으로 할당해준 핸들러의 this는 왜 전역객체를 가리키는걸까..?

 

그 이유는 간단하다.

html 속성으로 할당해준 이벤트는 해당 이벤트가 발생했을때 속성값을 본문으로 삼는 새로운 함수를

만들어 실행하기 때문이다.

즉, <button onclick="clickA(event)">A</button> 이 버튼은 클릭 이벤트가 발생되면

아래 코드와 같이 속성값을 실행하게 된다. 그래서 clickA 는 일반함수가 호출될때와 같은 규칙을 따라 this에

전역객체 할당되어 위 결과에서 window 객체가 출력된 것이다.

(function(){
    clickA(event);
})()

우리가 html 속성으로 이벤트 함수를 할당할때 onclick="func" 가 아니라 onclick="func()" 이렇게 괄호를 붙여서

바로 호출하는 형태로 써주는것도 바로 이 이유때문이다.

 

 

화살표 함수와 this

마지막으로 화살표 함수에서 this는 어떤값을 가리킬지 알아보자.

 

화살표 함수는 위의 예시들과는 달리 this를 바인딩하지 않는다. 

지금까지 봤던 this 바인딩은 함수가 호출되는 시점에 발생하는 동적 바인딩 이지만, 화살표 함수는 정적 바인딩이다.

정적 바인딩이란, 함수가 선언된 순간 코드상으로 바로 바깥쪽에 있는 스코프의 this를 사용한다는 뜻이다.

화살표함수에서 this의 값

call 메서드를 이용해 객체{a: 'aaa'} 를 this값으로 바인딩하여 test함수를 호출하고있다.

그리고 setTimeout의 콜백으로 화살표 함수를 넘겨주고 있다.

 

호출된 test 함수의 this는 {a: 'aaa'} 객체를 가리키고 있으므로 setTimeout의 콜백으로 넘겨진 화살표 함수는

코드상 자신이 만들어진 위치의 바깥범위(test 함수) 의 this를 그대로 사용하게 된것이다.

 

만약 화살표함수가 아닌 일반함수를 콜백으로 넘겨줬다면 window 객체가 출력됐을것이다.

화살표함수와 다른 결과

 

 

 

상황에따라 달라지는 this값을 총정리 해봤는데, 역시 만만치 않다.. 피곤하다.

 

 

 

잘못된 내용이 있다면 댓글 주세요 :)

 

 

Comments