This와 This 바인딩, 어디까지 알아?

55555-Jyeon·2024년 5월 29일
1

Deep Dive

목록 보기
4/8
post-thumbnail
post-custom-banner

This → { }

This는 Object(객체)를 참조하는 keyword입니다.

그런데 어떤 객체를 참조하는 키워드인걸까요?


전역에서

전역에서, 즉 함수 외부에서 this를 console에 찍어보게 되면 window 객체를 가리키고 있다는 것을 알 수 있습니다.

window 객체는 브라우저에 대한 정보를 갖고 있는 전역 객체입니다.

"use strict"로 엄격모드에서 같은 코드를 작성해도 전역에서의 this는 전역 객체인 window를 가리키고 있는 것을 확인할 수 있습니다.


일반 함수 내부에서

This함수를 호출한 객체입니다.

function 키워드로 선언한 함수, 즉 일반 함수는 window 객체에 등록되게 됩니다.

따라서 함수 밖에서 window를 console에 찍은 뒤 내부를 살펴보면 main이라는 이름의 함수가 등록된 것을 확인할 수 있습니다.

선언된 함수를 호출하고 console을 살펴보면 window 객체가 찍히는 것을 확인할 수 있습니다.

이는 main 함수를 호출한 부분이 window.main()과 동일하기 때문입니다.

function main(){
	console.log(this);
}

main(); // === window.main(); ⭐️

This함수를 호출한 객체입니다.
그리고 main이라는 함수를 직접적으로 호출한 객체는 window 객체(전역 객체)입니다.

따라서 일반 함수 속 this는 전역에서와 마찬가지로 window 객체를 가리키게 됩니다.


전역에서의 this와 일반 함수 내부에서의 this의 차이는 "use strict"를 사용했을 때 발견할 수 있습니다.

엄격모드에서는 동일하게 아래와 같이 코드를 작성했을 때 window 객체 대신 undefined를 내보냅니다.

undefined 대신 window 객체를 console에 찍으려면 좀 더 정확하게 코드를 작성해야 합니다.
위에서 언급한 것처럼 축약(?) 대신 window.main()을 다 작성해줘야 window 객체가 console에 찍히게 됩니다.


객체 메서드 내부에서

ES6에서 추가된 let과 const 키워드 사용 시 경고가 떠서 일단 var를 사용해 선언했습니다.

This는 함수를 호출한 객체를 가리킨다고 했습니다.
위 코드에서 main 함수를 호출한 객체는 object 객체이기 때문에 이번에는 위에서와는 다르게 object 객체를 가리키게 됩니다.

위 코드는 아래와 같이 작성해도 같은 결과로 나타납니다.

function main(){
	console.log(this);
}

const object = {
	name: 'amy',
  	main,   // main : main,
};

object.main();

const object = {
	name: 'amy',
  	smallObject : {
    	name: 'small amy',
      	main: function(){
        	 console.log(this);
        },
    },
};

object.smallObject.main();  // ???

이렇게 object 안에 smallObject를 만들어 주고, 그 안에 메서드를 만든다면 마지막 코드의 결과는 어떻게 나올까요?

this가 가리키는 것은 smallObject일까요, object일까요?

main 함수를 직접적으로 호출한 객체가 smallObject이기 때문에 this는 smallObject를 가리키게 됩니다.

this의 값은 .으로 해당 함수를 불러오는 바로 왼쪽의 객체라고 생각하면 편할 것 같습니다 🙂


이런 특징은 객체의 다른 속성에 접근할 때 유용하게 사용될 수 있습니다.

const object = {
	name: 'amy',
  	main: function(){
    	console.log(this.name); // 'amy'
    },
};

object.main();


This는 자신이 속한 객체 혹은 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(self-referencing variable) 입니다.

따라서 this를 통해 자신이 속한 객체 혹은 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있습니다.


This Binding

위에서 이해를 위해 다뤘던 부분들을 항목별로 다시 나눠 짚고 넘어가보겠습니다.

자바스크립트에서 this가 참조하는 것은 함수가 호출되는 방식에 따라 결정되는데, 이것을 This Binding이라고 합니다.

this 바인딩은 크게 아래 다섯 가지(4가지의 메인 바인딩과 화살표 함수)로 분류할 수 있습니다.

1️⃣ 기본 바인딩
2️⃣ 암시적 바인딩
3️⃣ 명시적 바인딩
4️⃣ new 바인딩
+ 화살표 함수(ES6)

1️⃣ 기본 바인딩 (Default Binding)

기본 바인딩이 적용될 경우 this는 전역 객체에 바인딩되며 브라우저 환경인 경우 window, Node.js 환경인 경우 global을 가리키게 됩니다.

function printName() {
  const name = "Amy";
  console.log(this.name);
}

printName(); // undefined

위의 코드에서 this는 전역객체에 바인딩되는데 전역객체에는 name이라는 프로퍼티가 없기 때문에 콘솔에는 undefined가 찍히게 됩니다.

window.name = "Amy";

function printName() {
  console.log(this.name);
}

printName(); // Amy

코드를 일부 수정했습니다.
위와 같이 전역객체에 name이라는 프로퍼티가 있다면 해당 name 프로퍼티의 값이 찍힙니다.

단, 엄격모드에서는 기본 바인딩 대상에서 전역객체는 제외됩니다.

'use strict'
window.name = "Amy";

function printName() {
  console.log(this.name);
}

printName(); // TypeError

전역객체를 참조해야할 this가 있다면 그 값은 undefined가 되므로 주의해야 합니다.


2️⃣ 암시적 바인딩 (Implicit Binding)

암시적 바인딩(Implicit Binding)은 함수가 객체의 메서드로서 호출되는 상황에서 this가 바인딩되는 것을 말합니다.
이때 this는 해당 함수를 호출한 객체, 즉 콘텍스트 객체에 바인딩됩니다.

const person = {
  name: "Amy",
  birthYear: 1990,
  CalcAge: function () {
    console.log(this.birthYear); 
  }
}

person.CalcAge(); // 1990

암시적 바인딩을 사용할 때 발생할 수 있는 문제는 위와 같은 상황에서 함수를 매개변수(콜백)로 넘겨서 실행하는 것입니다.

예를 들어, 위의 코드에 이어 아래와 같이 객체에 정의되어있는 함수의 레퍼런스를 매개변수로 전달한다면 어떤 결과가 나올까요?

setTimeout(person.CalcAge, 1); // ?

정답은 undefined입니다.

이런 결과가 나온 이유는 setTimeout 함수 안에 전달한 콜백은 CalcAge 함수의 레퍼런스일 뿐, person의 콘텍스트를 가지고 있지 않기 때문입니다.

이런 상황을 암시적 바인딩이 소실되었다고 합니다.

따라서 setTimeout 내부에서 호출되는 콜백은 person 객체의 콘텍스트에서 실행되는 것이 아니기 때문에, this는 기본 바인딩이 적용돼서 전역 객체에 바인딩 됩니다.

function setTimeout(callback, delay) {
  // delay에 전달된 시간이 흐른 뒤 callback을 실행
  callback(); // 기본 바인딩
}

3️⃣ 명시적 바인딩 (Explicit Binding)

자바스크립트의 모든 Function은 call(), apply(), bind()라는 프로토타입 메소드를 갖고 있습니다. 이 3가지 메서드 중 하나를 호출함으로써 this 바인딩을 코드에서 명시하는 것을 명시적 바인딩이라고 합니다.

이때 this는 내가 명시한 객체에 바인딩됩니다.

apply, call
const person = {
  age: 20,
}

function printAge() {
  console.log(this.age);
}

printAge.call(person); // 20
printAge.apply(person); // 20

printAge의 함수 프로토타입 메서드 call, apply의 매개변수로 바인딩할 객체를 넘겨줌으로써 printAge 함수를 실행할 때의 this 컨텍스트가 person이 되도록 직접 바인딩 해주었습니다.

call과 apply의 동작은 같지만 두번째 매개변수로 객체의 인자를 전달해주는데
call은 매개변수의 목록, apply는 배열을 받는다는 차이점이 있습니다.

bind

계속 변경되는 this의 값을 특정 객체가 되도록 지정하기 위해(고정시키기 위해) bind()할 수 있습니다.

bind 메서드는 새로운 바인딩된 함수를 만듭니다. 바인딩된 함수를 호출하면 일반적으로 해당 함수가 래핑하는 함수(대상 함수)가 실행되게 됩니다. 그리고 이때 바인딩된 함수는 this와 인수를 포함하는 전달된 매개변수를 내부 상태로 저장합니다.

bind 메서드가 매개변수로 전달받은 오브젝트로 this가 바인딩된 함수를 반환하는 것을 하드 바인딩(hard binding)이라고 합니다. 하드 바인딩된 함수는 이후 호출될 때마다 처음 정해진 this 바인딩을 가지고 호출됩니다.

함수에 원하는 객체를 묶어 고정시키기 위해서는 새로운 변수를 만들어줘야 합니다. 새로운 변수에 함수를 우선 할당해줍니다. 그리고 고정시킬 객체를 인자로 받는 bind()를 할당한 함수 뒤에 연결합니다.

// before bind
function main(){
	console.log(this);  // Window Obj
}

main();

// after binding
function main(){
	console.log(this);  // {name: "Amy"}
}

const mainBind = main.bind({name: "Amy"});

mainBind();

함수를 바인딩할 때 주의해야할 점은 한 번 바인딩된 함수는 다시 바인딩될 수 없다는 것입니다.

function main(){
	console.log(this);  // {name: "Amy"}
}

const mainBind = main.bind({name: "Amy"});

const BindAgain = mainBind.bind({name: "something else"});  // ignored

BindAgain();

새로운 변수를 만들어 적용할수도 있지만 아래와 같이 작성할 수도 있습니다. 🙂

const outerObject = {
	name: "outer object",
  	main: function(){
    	console.log(this); // outerObject {...}
    },
};

object.main();

처음 사용했던 예제를 활용해보자면 :
객체 안의 함수에 바로 bind 메서드를 연결해 사용할 수도 있습니다.

const outerObject = {
	name: "outer object",
  	main: function(){
    	console.log(this); // {name: "inner object"}
    }.bind({name: "inner object"}),
};

object.main();

4️⃣ new 바인딩 (new Binding)

자바스크립트의 new 키워드는 함수를 호출할 때 앞에 new 키워드를 사용하는 것으로 객체를 초기화할 때 사용하는데, 이때 사용되는 함수를 생성자 함수라고 합니다.

그리고 생성자 함수에서는 this 키워드를 해당 생성자를 이용해 생성할 객체에 대한 참조로 사용합니다.

function PrintAge() {
  this.age = 20;
}

const printAge = new PrintAge();

console.log(printAge.age); // 20

① PrintAge 함수가 new 키워드와 함께 호출되는 순간 새로운 객체가 생성되고, 새로 생성된 객체가 this로 바인딩 됩니다.

② 생성된 객체의 age라는 프로퍼티에 20이라는 값이 할당되고, 해당 객체는 printAge 변수에 할당됩니다.


+ ES6 화살표 함수 (Arrow Function)

ES6에 추가된 화살표 함수(Arrow Function)는 this를 바인딩할 때 앞서 설명한 규칙들이 적용되지 않고, this에 어휘적 스코프(Lexical scope)가 적용됩니다.

const obj = {
  name: 'John Doe',
  greet: () => {
    console.log(this.name);
  }
};
obj.greet(); // undefined (외부 스코프의 this를 사용)

즉, 화살표 함수를 정의하는 시점의 컨텍스트 객체가 this에 바인딩됩니다.

const person = {
  age: 20,
  printAge: function () {
    setTimeout(() => {
      console.log(this.age);
    }, 1);
  }
}

person.printAge(); // 20

setTimeout의 콜백인 화살표 함수의 선언 시에 this는 person 객체를 가리키고 있기 때문에(렉시컬 스코프), 콜백이 실행될 때 this는 person을 가리키게 됩니다.

화살표 함수로 선언 시에 렉시컬 스코프를 통해 바인딩된 this는 apply, bind등의 함수나 new 함수로 오버라이드할 수 없기 때문에 주로 콜백 함수로 사용할 때 유용하게 사용됩니다.


else, lexical scope

apply, call, bind와 같은 바인딩 메소드, 또는 화살표 함수가 나오기 전에는 골치아픈 this 바인딩을 렉시컬 스코프로 해결해왔습니다.

호출 시 결정되는 this를 렉시컬 스코프를 이용해 선언시에 정해주는 효과를 주는 것이죠.

위에서도 한 번 확인했던 내용으로 아래와 같은 상황에서 setTimeout에 콜백으로 넘겨진 함수의 this는 setTimeout에 의해 실행될 때 전역객체에 바인딩이 됩니다.

const person = {
  age: 20,
  printAge: function () {
    setTimeout(function () {
      console.log(this.age);
    }, 1);
  }
}

person.printAge(); // undefined

이 때, setTimeout의 콜백 내부에서 해당 setTimeout을 호출한 객체에 접근하기 위해서 어떻게 해야할까요?

const person = {
  age: 20,
  printAge: function () {
    const _this = this;
    setTimeout(function () {
      console.log(_this.age);
    }, 1);
  }
}

person.printAge(); // 20

위와같이 printAge 메서드 실행 시에 해당 메서드를 호출한 객체(person)에 대한 참조를 _this라는 변수에 할당합니다.

그러면 setTimeout의 콜백이 실행될때 렉시컬 스코프에 의해 _this에 접근할 수가 있게 되므로 이를 통해 setTimeout을 호출한 객체(person)에 접근이 가능해집니다.



이벤트 처리와 this

함수를 DOM 요소의 이벤트 처리기로 사용할 때 this 값은 어떤 식으로 설정될까요?

버튼을 클릭하거나 스크롤을 내리고 올릴 때 발생하는 이벤트를 처리할 때 이벤트 리스너(EventListener)와 this를 사용한 경험이 있을 겁니다.

const button = document.getElementbyId("btn");

button.addEventListener("click", function(){
	console.log(this);
})

index.html 파일 안에 btn이라는 아이디의 버튼이 있다고 가정하고 위와 같이 script 파일을 작성했습니다.

이제 버튼을 클릭할 때마다 위의 함수가 실행됩니다.

버튼을 클릭하면 this 값으로 버튼 요소가 콘솔에 찍히는 것을 확인할 수 있습니다.
이렇게 함수를 DOM 요소의 이벤트 처리기로 사용하게 될 경우, this는 이벤트가 적용된 요소를 가리키게 됩니다.

const button = document.getElementbyId("btn");

button.addEventListener("click", function(e){
	console.log(e.target === this); // true
})


conclusion

왜 this와 this 바인딩에 대해 알아야 할까?

이러한 this 바인딩 규칙을 이해하고 활용하면 자바스크립트 코드의 유지보수성을 높이고 객체 지향적인 설계를 효율적으로 구현할 수 있습니다.

아래 내용을 정리하자면 :


1️⃣ 유연한 컨텍스트 관리

this를 통해 함수가 호출된 컨텍스트에 접근할 수 있으므로, 다양한 객체에서 동일한 함수나 메소드를 재사용할 수 있습니다. 이는 코드의 중복을 줄이고 재사용성을 높이는 데 유리합니다.

function introduce() {
  console.log(`My name is ${this.name}`);
}

const person1 = { name: 'John Doe', introduce };
const person2 = { name: 'Jane Doe', introduce };

person1.introduce(); // 'My name is John Doe'
person2.introduce(); // 'My name is Jane Doe'

2️⃣ 명확한 코드 구조

메소드 내에서 this를 사용하면 객체 지향 프로그래밍 패러다임을 따르기 쉬워집니다. 객체의 속성과 메소드를 함께 정의하여 관련 있는 데이터를 논리적으로 묶을 수 있습니다.


3️⃣ 콜백 함수의 컨텍스트 유지

bind 메소드를 사용하면 콜백 함수가 호출될 때의 this를 미리 지정하여 예상치 못한 컨텍스트 변경을 방지할 수 있습니다.

const button = document.getElementById('btn');
const obj = {
  name: 'Jane Doe',
  handleClick: function() {
    console.log(this.name);
  }
};

button.addEventListener('click', obj.handleClick.bind(obj)); // 'Jane Doe'

4️⃣ 화살표 함수의 lexical this

화살표 함수를 사용하면 외부 컨텍스트의 this를 그대로 사용할 수 있어, 특히 콜백 함수나 비동기 코드에서 유용합니다.




References.

📚 Books.

  • 이웅모의 모던 자바스크립트 Deep Dive
  • 카일 심슨의 You Dont’t Know JS (this와 객체 프로토타입, 비동기와 성능)

🌎 Official Sites.

  • MDN web docs's this
  • MDN web docs's bind

👩🏻‍💻 posts.

🎥 videos.


profile
🥞 Stack of Thoughts
post-custom-banner

0개의 댓글