this의 개념은 다소 복잡하다. 어려운 이유는 this가 참조하는 대상이 실행 컨텍스트에 따라 달라지기 때문이다. 이러한 변동성은 this가 호출되는 다양한 시나리오에 대해 이해를 해야한다. 프로그래밍을 할때 혼란이 되기도 하고 오류의 원인이 될 수도 있다. 이러한 개념을 명확히 하고 적용을 직관적으로 이해하기 위해 this의 사용법을 3가지의 주요 범주로 구분을 해보자.
- 암시적 바인딩
- 명시적 바인딩
- 정적 바인딩
이러한 분류를 통해 다양한 환경에서 this를 예측하고 이해할 수 있는 구조적인 틀을 만드는 데에 도움이 될 수 있다.
this는 왜 상황에 따라 달라질까? 모든 변수는 정적으로 바인딩한다. 오직 this만이 동적으로 바인딩 된다. 바로 이런 점이 혼란을 야기한다. 함수를 만든 쪽이 아닌 함수를 호출하는 쪽이 this를 바인딩하는 것이다. JS의 this키워드는 앞서 설명한 실행 컨텍스트의 생성과 밀접하게 연관되어있다. 실행 컨텍스트는 함수가 호출될 때 생성되는 환경이다. 이 때,this의 값이 결정된다. 따라서 this의 참조 대상은 호출되는 함수의 상황과 맥락, context에 따라서 동적으로 변화한다. 이러한 메커니즘은 JS의 유연성을 강화시키는 장점이 있지만, 동시에 바인딩을 예측하기가 어려운 요인이 되기도 한다. 함수가 어떻게 호출 되는 지에 따라서 this가 가리키는 대상이 달라져서 JS의 함수 호출 패턴을 이해하는 것이 매우 중요하다. 비트겐슈타인의 가족 유사성 개념을 통해 JS의 this가 상황에 따라 달라지는 현상을 설명할 수 있다. 앞선 프로토타입 시간에 설명했듯, 비트겐슈타인은 언어의 의미가 고정된 정의에 의해 결정되는 것이 아니라, 사용되는 맥락, context와 유사한 사용 사례들 간에 (가족 유사성에 의해) 형성된다고 주장했다. 마찬가지로, JS의 this도 this가 사용되는 실행 환경, 실행 컨텍스트가 어떻게 호출이 되고, 또 어디서 호출되는지에 따라 그 의미가 달라진다. this의 의미는 고정된 정의에 의해 결정되는 것이 아니라, 다양한 실행 컨텍스트에서 유사한 사용 사례들과의 관계속에서 형성이 된다는 내용이다.
상황에 따라 달라지는 this
- 암시적 바인딩: 코드의 실행 컨텍스트에 의해 자동으로 결정되는 this의 값. this가 가리키는 대상이 코드 상에서 명시되지 않고, 실행 컨텍스트 즉, 함수가 호출되는 방식에 의해 암시적으로 결정이 된다. 예를 들어, 전역 공간에서 this는 전역 객체를 참조하며 함수가 함수로서 호출 될 때, this는 역시 전역 객체를 참조한다. 함수가 메서드로 호출될 경우, this는 메서드를 호출한 객체를 참조한다. 생성자 함수 내에서 this는 생성될 인스턴스를 참조한다.
- 명시적 바인딩: 개발자가 call, apply, bind와 같은 메서드를 사용하여 함수나 메서드를 호출할 때 this를 명시적으로 바인딩 하는 것이다. this가 가리키는 대상을 명확히 지정하기 때문에, 함수나 메서드의 실행 컨텍스트를 개발자가 직접 제어할 수 있다. 이 방법을 통해, 기본적인 암시적 바인딩 규칙을 떠나, 원하는 객체를 this에 바인딩할 수 있다.
- 정적 바인딩: 주로 화살표 함수에서 볼 수 있는 개념이다. this의 값이 함수가 작성된 위치에 의해 결정이 된다. 화살표 함수의 this는 함수가 정의된 시점의 컨텍스트를 가리키고, 함수의 실행 시점이 아니라 함수를 작성하는 시점에 의해 결정이 된다. 자신의 정의된 scope의 this를 상속 받는다.
코드로 확인
암시적 this
JS의 실행 환경에서 this 키워드는 맥락에 따라, context에 따라 다양한 대상을 참조할 수 있다. 그 중에서도 전역 실행 컨텍스트에서의 this는 전역 객체에 바인딩이 된다. JS 코드가 전역 스코프에서 실행될 때, 기본적으로 적용되는 규칙이다. 브라우저 환경에서 전역 객체는 윈도우 객체를 의미한다. Node.js 환경에서는 글로벌을 의미한다. 이러한 차이는 각 실행 환경의 전역 스코프가 서로 다른 객체를 전역 컨텍스트로 사용하기 때문이다. 예를 들어 브라우저 환경에서 this를 입력해보면, Window 객체가 출력되는 것을 확인할 수 있다.
따라서 this와 Window의 관계는 비교 연산자로 확인해보면 true를 리턴한다. this 가 전역 컨텍스트에서 윈도우 객체를 참조하고, this는 전역 객체인 Window와 동일하다고 볼 수 있다. 이를 실행컨텍스트 관점에서 객체로 표현하면
'전역 컨텍스트': {
this: window
}
함수를 함수로서 호출할 때, this는 전역 객체를 참조한다. 함수가 단독으로 호출될 때, 즉 메서드가 아닌 일반 함수로 실행 될 때, this 키워드는 전역 객체를 참조한다.
예를 들어
function a () {
console.log(this);
}
a();
위 코드를 실행하면 브라우저 환경에서 Window 객체가 출력 된다. 이유는 함수가 전역 스코프에서 호출되었기 때문이다. 이 함수 a를 실행 컨텍스트의 객체로 표현해보면,
'a 컨텍스트' : {
this: window
}
함수를 함수로 호출한 것이 아니라, 객체의 메서드로 호출될 때 this 키워드는 해당 메서드를 호출한 객체를 참조한다. 이런 특성은 객체 지향 프로그래밍의 요소 중 하나이다. 메서드 내부에서 객체에 접근할 수 있게 해주는 메커니즘 이다. 예를 들어
const func = function() {
console.log(this);
}
const obj = {
method: func
}
obj.method(); // 표시되는 this는 method가 출력된다.
method 내부의 this는 obj 객체를 참조한다. 이는 func 함수가 obj 객체의 컨텍스트 내에서 method로 호출되었기 때문이다. 이러한 방식으로 this는 method를 호출한 객체, 즉 method호출 시점에서의 객체 컨텍스트에 바인딩이 된다. 이 원리는 객체의 메서드가 해당 객체의 상태나 다른 메서드에 접근해야 할 때 유용하다. 이 this를 통해 메서드는 호출한 객체의 속성이나 다른 메서드를 자유롭게 사용할 수 있다. 이러한 패턴은 코드의 모듈성과 재사용성을 크게 향상시킬 수 있다.
JS에서 생성자 함수 내에서 this는 해당 생성자를 통해서 생성될 새로운 인스턴스를 참조한다. 이러한 메커니즘을 통해 생성자 함수는 객체의 초기 상태를 설정하고 인스턴스의 속성을 추가할 수 있다. 예시로
const Car = function(name) {
this.name = name;
}
const avante = new Car('Avante');
avante; // Car {name: 'Avante'}
생성자 함수 내부의 this는 새롭게 생성된 인스턴스를 가리키기 때문에, 이 this.name = name; 을 통해 인스턴스의 name 속성이 추가된다. 결과적으로 avante 인스턴스는 name 속성을 포함하게 되어 이 속성의 값은 대문자 A가 들어간 Avante로 설정이 된다. 이러한 방식으로 이 생성자 함수는 객체 지향 프로그래밍 패러다임에서 클래스의 생성자와 유사한 역할을 수행한다. JS에서 객체의 인스턴스를 생성하고 초기화하는데 중요한 도구로 사용이 된다. 특히, 메서드의 내부 함수에서 외부 메서드의 this를 참조하고자 할 때 유용하다. JS의 유연한 this 바인딩 특성을 활용하여 기본적으로 함수를 호출할 때, this가 전역 객체를 가리키는 것을 회피하고, 대신 원하는 특정 객체 또는 주변 환경의 this를 명시적으로 바인딩함으로써 여러 문제를 해결할 수 있다.
function showContext() {
console.log(this); // {custom: 'context'}
}
const myObj = {custom: 'context'};
showContext(myObj);
명시적 this
showContext는 함수로써 호출이 되고 있기 때문에, this는 window객체를 바라보고 있다. 이러한 상황에서 만약 showContext 함수를 전혀 수정하지 않고, 명시적인 this 바인딩을 통해 해당하는 this가 myObj를 가리키게 해야 하는 상황이 닥친다면, 어떻게 할 수 있을까? call 메서드를 사용하는 방법이 있다. showContext 함수에 call을 사용하면 된다.
function showContext() {
console.log(this);
}
const myObj = {custom: 'context'};
showContext.call(myObj);
이를 통해, 함수 호출 시에 this를 명시적으로 제어하는 방식을 확인할 수 있고, 이러한 방식을 통해 기존의 함수를 수정하지 않으면서, 원하는 대로 this를 바인딩하여 함수의 재사용성과 유연성을 향상 시킬 수 있다. call 메서드 외에 apply 메서드를 사용하여 this를 명시적으로 바인딩하는 방법이 있다. apply 메서드는 call 메서드와 유사하다. 둘 다 JS에서 함수를 호출할 때 this의 context를 명시적으로 설정할 수 있도록 해준다. call과 apply는 거의 비슷하지만 함수의 인자를 전달하는 방식에 차이가 있다.
call: 첫 번째 인자로 this로 사용할 값을 받고, 그 이후의 인자들은 호출할 함수에 직접 전달한다. 인자는 쉼표로 구분된 목록으로 제공한다. 인자의 개수가 고정되어 있고, 각 인자를 개별적으로 나열해야 할 때 유용하다.
apply: 첫 번째 인자로 this로 사용할 값을 받고, 두 번째 인자로 함수에 전달될 인자들의 배열을 받는다. 인자의 개수가 동적이거나 이미 배열 형태로 인자가 준비되어 있을 때 사용한다.
bind
call, apply와는 다르게 함수를 즉시 호출하지 않는다. 대신, this값이 설정된 새로운 함수를 반환한다. 반환된 함수는 나중에 호출할 수 있고, this값은 bind를 호출할 때 지정된 context로 고정을 한다.
function showContext() {
console.log(this);
}
const myObj = {custom: 'context'};
const boundShowContext = showContext.bind(myObj);
boundShowContext();
bind의 특징은 재사용 가능한 함수를 생성한다는 점이다. bind를 사용하여 함수의 this 값을 고정시킨 새로운 함수를 만들수 있다. 이 함수는 어디서든 재사용할 수 있고, this 값은 항상 bind 호출 시 지정한 객체를 참조한다.
정적 this
arrow function은 상위 스코프의 this를 바인딩하여 함수의 상위 스코프를 결정하는 lexical scope와 유사하여 lexical this라고 한다. 일반 함수를 사용하여 this가 제대로 바인딩되지 않는 예제를 먼저 보자
const person = {
name: 'Bob',
showNameAfterDelay() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
person.showNameAfterDelay(); // undefined
this.name은 person 객체의 name이 아니다. 함수 내에서 호출되기 때문에 전역 객체의 name을 출력하고, Window 객체에는 name이 없기 때문에 undefined가 출력이 된다.
arrow function을 사용하면 비동기 함수를 사용할 때, 일반 함수를 화살표 함수로 변경 시켜 주면 외부함수의 this 컨텍스트를 자연스럽게 상속 받아서 name을 사용할 수 있다.
const person = {
name: 'Bob',
showNameAfterDelay() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
person.showNameAfterDelay(); // Bob
console.log(this.name); 에서 this는 person 객체를 참조한다. this 컨텍스트가 정적으로 바인딩 되기 때문에, name을 가리키게 된다. 이러한 방식을 통해 화살표 함수의 정적 this, 즉 lexical this의 특성은 비동기 콜백이나 이벤트 핸들러 내에서 특히, 유용하게 사용될 수 있다. this 바인딩에 대한 추가적인 처리 없이도 바깥 스코프의 this 컨텍스트에 쉽게 접근할 수 있다.
this 없이 코드를 작성할 것을 권장하는 사람들이 있다.
함수가 일반적인 함수로 호출이 될 때, this가 전역 객체에 바인딩 되는 JS의 동작 방식 때문이다. 이러한 바인딩은 코드의 예측 가능성을 저하 시키고 의되치 않은 버그를 초래할 수 있기 때문이다. this를 사용하지 않고 프로그래밍하는 것은 어렵지 않다. this에 의존하지 않는 코딩 접근 방식은 코드의 명확성을 높이고, 함수의 동작을 컨텍스트로 부터 독립시켜 재사용성과 테스트 용이성을 향상 시킬 수 있다. 점점 tihs를 사용하지 않는 추세이다. React의 함수형 컴포넌트와 hook이 그 예시이다.
this의 동적인 특성은 함수가 어떻게 호출되는지에 따라 그 값이 달라지고 다양한 프로그래밍 패턴과 설계에서 중요한 역할을 한다. 암시적 바인딩은 함수 호출 시 함수가 속한 객체를 this로 참조하고, 전역 함수에서는 전역 객체가 this가 된다.
명시적 바인딩은 call, apply, bind 메서드를 통해 개발자가 this의 값을 직접 지정할 수 있다. 정적 this는 화살표 함수에서 자신이 정의된 렉시컬 스코프의 this를 상속받아 상위 스코프의 this를 참조하는 정적 바인딩의 특성을 가진다.
[참고]
패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.
'개발공부 > Javascript' 카테고리의 다른 글
[JS] 프로토타입 기반 상속과 클래스 기반 상속 (1) | 2025.01.31 |
---|---|
[JS] ESM, CJS 모듈 시스템 (0) | 2024.05.05 |
[JS]프로토타입 (1) | 2024.04.28 |
[JS] 이벤트 루프 (1) | 2024.04.18 |
[JS] 비동기 (0) | 2024.04.10 |