본문 바로가기

Programming/Javascript

8. Javascript 함수 호출 (3) - 함수를 호출할 때 this 바인딩

 

전역 객체의 이해

이번에는 지난 아티클인 [객체 메서드]와 다른 개념인, 함수 호출 시의 this 바인딩에 대해서 살펴보려고 합니다. 이를 이해하기 위해서는 Javascript에서의 전역 객체에 대한 이해가 우선 필요합니다. 우선 Javascript를 브라우저에서 실행하게 될 때, 기본적으로 전역 객체는 window 객체가 됩니다. (Node.js의 경우 global이 전역 객체) 특별히 명시되지 않는다고 하더라도, 기본적으로 Javascript에서 선언되는 전역 변수들은 모두 이 전역 객체의 프로퍼티가 됩니다. 

 

아래 예제 코드를 통해 전역 객체를 살펴보겠습니다. 

 

var testValue = "Test String";

console.log(testValue);
console.log(window.testValue);

// 브라우저 실행 시 실행 결과
// Test String
// Test String

 

위의 코드를 브라우저에서 실행하면(VS Code 등에서 실행 시 Reference Error 발생), testValue와 window.testValue 모두 동일하게 출력됩니다. 여기서 전역 변수로 선언된 testValue 변수는 전역 객체의 프로퍼티로서도 동일하게 접근이 가능하다는 사실을 먼저 기억하도록 합시다. 

 

 

함수에서의 this 바인딩

본론으로 들어가 보겠습니다. 우선 기억해야 할 것은, 어떤 함수를 호출하는 경우 함수 내부 선언문에서 사용되는 this는 전역 객체에 바인딩된다는 사실입니다. 다른 언어를 공부하신 분들이라면, 약간 와닿지 않는 개념일 수 있습니다. 인스턴스 기반으로 새로운 객체를 생성하는 경우와 비슷한 케이스는 new 키워드 사용 시의 this 바인딩을 통해 따로 살펴볼 예정이니, 우선은 아래 예제를 통해 전역 객체에 바인딩되는 경우를 살펴보겠습니다. 

 

var testStr = "I could do that.";
console.log(window.testStr);

var printStr = function() {
    console.log(this.testStr);
};

printStr();

// 브라우저 실행 시 결과
// I could do that.
// I could do that.

 

위의 예제에서는 전역 객체인 testStr이 선언되었고, 함수 printStr에서는 this.testStr을 출력하도록 정의되었습니다. 위에서 설명하였듯이, 함수 내부에서의 this는 전역 객체에 바인딩되므로 결과적으로는 testStr 즉 window.testStr이 출력되는 결과를 얻을 수 있습니다. 

 

 


 

 

내부 함수에서의 this 바인딩(this 바인딩의 한계)

this 바인딩도 용도에 따라 적절히 사용하면 충분히 효용이 있을 것으로 보입니다. 하지만, 이 기능은 어느 정도의 한계점을 가지고 있습니다. 바로, 함수의 내부 함수에서 사용될 경우입니다. 만일 함수의 내부 함수에서 this가 사용된다면, 객체 메서드에서의 this 바인딩과 바인딩 지점이 혼용되기 때문에 의도하지 않은 동작이 발생할 수도 있습니다. 

 

아래 예제를 살펴보도록 하겠습니다. 만일 우리가 전역 객체인 testValue가 있고, testObj에 testValue라는 프로퍼티가 있다고 가정합시다. 우리의 의도는, testObj의 함수와 내부 함수에서 testObj의 프로퍼티인 testValue에 계속 누적하여 1을 더하는 것이라고 가정해 보도록 하겠습니다.

 

var testValue = 87;

var testObj ={
    testValue : 1,
    func1: function(){
        this.testValue += 1;
        console.log('func1 called. this.testValue = ' + this.testValue);

        // 내부함수 func2
        func2 = function(){
            this.testValue += 1;
            console.log('func2 called. this.testValue = ' + this.testValue);

            // 내부함수 func3
            func3 = function() {
                this.testValue += 1;
                console.log('func3 called. this.testValue = ' + this.testValue);
            }
            
            func3();
        }

        func2();
    }
};

testObj.func1();

// 브라우저 실행결과
// func1 called. this.testValue = 2
// func2 called. this.testValue = 88
// func3 called. this.testValue = 89

 

위에서 보면, func1이 호출되었을 때는 의도한대로 testObj.testValue에 1이 더해졌습니다. 하지만, func2와 func3가 실행되었을 때는 의도와 달리 전역 객체인 window.testValue에 1이 더해져 88, 89가 출력되는 현상이 발생하였습니다. 이런 동작을 하는 이유는, Javascript에서 내부 함수에 대한 호출 패턴이 정의되어있지 않기 때문입니다. 결국, func2와 func3는 특정 객체의 프로퍼티가 아닌 독립적인 함수로 취급을 한 상태가 되어버립니다. 

 

이런 경우 일반적으로 func1 메서드를 정의할 때, this를 다른 변수에 대입하여 사용하는 방식으로 이러한 예상치 못한 동작을 우회하게 됩니다. 

 

var testValue = 87;

var testObj ={
    testValue : 1,
    func1: function(){
        var that = this;
        this.testValue += 1;
        console.log('func1 called. this.testValue = ' + that.testValue);

        // 내부함수 func2
        func2 = function(){
            that.testValue += 1;
            console.log('func2 called. this.testValue = ' + that.testValue);

            // 내부함수 func3
            func3 = function() {
                that.testValue += 1;
                console.log('func3 called. this.testValue = ' + that.testValue);
            }
            
            func3();
        }

        func2();
    }
};

testObj.func1();

// 브라우저 실행결과
// func1 called. this.testValue = 2
// func2 called. this.testValue = 3
// func3 called. this.testValue = 4

 

위의 예제는 크게 변한 것은 없습니다. 다만, 객체 메서드 func1에서 that이라는 변수를 하나 선언하고, 여기에 this를 대입했습니다. 이 부분에서 this는 무엇을 의미할까요? 객체 메서드 내부의 this이기 때문에 메서드를 호출하는 객체를 의미합니다. 즉, that은 이 코드 안에서는 testObj를 의미하게 됩니다. 

 

이후 내부 함수에서 this를 사용했던 키워드를 모두 that으로 바꾸어주면 결국 부모 함수의 변수를 그대로 사용하는 개념이기 때문에, this와 같은 방식의 전역 객체 바인딩이 일어나지 않습니다. 그대로 testObj를 의미하게 되므로 우리가 원하는 결과를 얻게 되는 것입니다. 

 

 


 

지금까지 살펴본 함수에서의 this바인딩은 전역 객체를 일괄적으로 바인딩하는 성질을 갖고 있어, 생각보다 많은 한계점을 가지고 있습니다. Javascript에서는 이 한계를 극복하기 위해 call / apply라는 메서드를 제공하고 있습니다. 또한, 일부 라이브러리에서는 bind 메서드를 통해 사용자가 원하는 객체를 this로 바인딩하는 기능들을 제공하게 됩니다. 이는 추후 포스팅할 아티클에서 디테일하게 살펴보도록 하겠습니다.