객체의 메서드를 구조분해하여 호출할 때, this 값의 변화

이나리·2023년 6월 1일
0
post-custom-banner
class Count {
	constructor(value) {
		this.value = value;
	}
	
	getValue() {
		return this.value;
	}
}

const count = new Count(0);

function play() {
	const $button = document.querySelector('button');
	const $value = document.querySelector('.value');
	$button.addEventListener('click', () => {
		const { getValue } = count;
		$value.innerHTML = getValue();
	});
}

play(); // Uncaught TypeError: Cannot read properties of undefined (reading 'count')

발생한 문제를 해결하기 앞서, 먼저 위 예제코드와 관련된 this 바인딩에 대해 간단히 알아보겠습니다.

화살표 함수의 this 바인딩

이벤트 핸들러 함수 안의 this는 항상 이벤트 핸들러 함수가 부착되는 요소를 가리킵니다.

이때 전달하는 이벤트 핸들러 함수를 화살표 함수로 정의하게 되면, 화살표 함수는 this를 갖지 않으므로 항상 상위 컨텍스트에 정의된 this를 참조하게 됩니다.

위의 예제에서 전달한 이벤트 핸들러 함수 안의 this는 상위 컨텍스트인 play 함수의 this 값을 항상 참조하게 됩니다.

play 함수는 화살표 함수가 아닌 일반 함수 선언으로 정의되었으므로, 이 함수의 this 값은 play 함수가 호출되는 방식에 따라 달라집니다.

객체의 메서드로서 실행할 때, this 바인딩

현재 play 함수는 전역 함수로서 정의되었지만,
만약 객체의 메서드로서 정의된 후, obj.play() 와 같은 코드가 실행되었다면,
이때 화살표 함수로 된 이벤트 핸들러 함수 안의 this는 obj 라는 특정 객체로 바인딩됩니다.

문제 해결하기!

이제 에러가 발생했던 코드를 다시 살펴보겠습니다.

function play() {
	const $button = document.querySelector('button');
	const $value = document.querySelector('.value');
	$button.addEventListener('click', () => {
		const { getValue } = count; // 에러가 발생한 지점
		$value.innerHTML = getValue();
	});
}

play(); // Uncaught TypeError: Cannot read properties of undefined (reading 'count')

에러 발생 원인

count 객체를 구조분해하여 메서드를 별도의 변수에 선언한 후, 이를 호출했습니다.

호출한 함수는 내부 코드에 this 를 사용합니다. this 값은 항상 함수가 어떤 방식으로 호출되는 지에 따라 결정됩니다.

getValue 함수는 count 객체의 프로토타입에 정의된 메서드로서, 구조분해 문법을 이용해 일반 함수로서 호출했기 때문에 이때 getValue 함수 안의 this 값은 window 가 됩니다.

따라서, window.getValue 에는 어떤 값도 할당되어 있지 않으므로, 이를 함수로서 호출하는 것 자체가 에러입니다.

const { getValue } = count; 
// const { getValue: getValue } = count; 와 동일한 문법

const getValue = count.getValue;

위의 두 코드는 쓰여진 형태만 다를 뿐, 동일한 코드입니다.
count 객체의 getValue 함수에 대한 참조값을 새로운 변수에 담은 것이죠.

해결방법

사용하려는 메서드를 객체로부터 구조분해하지 않고, count.getValue() 를 호출하면 됩니다.

이렇게 하면, 일반 함수로서 호출한 것이 아니라 객체의 메서드로서 호출하기 때문에 this 값은 메서드 앞의 객체인 count 를 가리키게 되고, count 객체 내의 value 프로퍼티에 접근할 수 있게 됩니다.

또 다른 해결방법

1. 클래스로 객체 생성시, 메서드를 this 바인딩하기

class Count {
	constructor(value) {
		this.value = value;
		// this 값을 Count 클래스의 인스턴스 객체로 고정
		this.getValue = this.getValue.bind(this);
	}
	
	getValue() {
		return this.value;
	}
}

const count = new Count(0);

const fn = count.getValue;
console.log(fn()); // 0

메서드를 구조분해하거나, 굳이 다른 변수에 참조값을 전달해 사용해야 한다면,
객체를 생성할 때, bind 메서드를 이용해 해당 메서드의 this 값을 고정해줄 수 있습니다.

그러나 이 방법은 클래스로 생성되는 모든 인스턴스 객체에 getValue 메서드를 생성하기 때문에 주의해야 합니다.

2. 클래스로 객체 생성시, 화살표 함수로 메서드 정의

class Count {
	constructor(value) {
		this.value = value;
	}
	
	getValue = () => {
		return this.value;
	};
}

const count = new Count(0);

const fn2 = count.getValue;
console.log(f2()); // 0

화살표 함수에는 this 가 없으므로, 이 메서드의 this 값은 항상 상위 컨텍스트인 Count 클래스의 인스턴스 객체를 가리킵니다.

그러나, 이 방법 역시 첫번째 방법과 유사한 문제를 갖고 있습니다.

화살표 함수로 메서드를 정의하게 되면, 해당 메서드는 클래스 필드로 분류되어 클래스로 생성된 인스턴스 객체의 프로퍼티가 됩니다.
따라서, 객체를 생성할 때마다 프로토타입이 아닌 해당 인스턴스 객체에 메서드가 생성됩니다.

정리

class Count {
	constructor(value) {
		this.value = value;
	}

	increment() {
		this.value++;
		console.log('after increment: ', this.value);
	}

	decrement() {
		this.value--;
		console.log('after decrement: ', this.value);
	}
}

const count = new Count(0);

count.increment(); // after increment: 1;

const decrement = count.decrement;
// 또는 const { decrement } = count;
decrement(); // Cannot properties of undefined (reading 'value')

1. 객체의 메서드로서 함수를 호출하는 경우

count.increment() 는 객체의 메서드로서 호출되었기 때문에, count 객체의 value 프로퍼티를 참조할 수 있습니다.

2. 객체의 메서드를 참조로 전달하고, 일반 함수로 호출하는 경우

객체의 메서드의 참조값만 할당한 decrement 는 결국 일반적인 함수로서 호출되기 때문에,
this 값이 count 객체가 아닌 window 가 되어, window.value 에 접근해서 함수의 코드를 실행하게 됩니다.

따라서, window.value 의 값이 지정되어 있지 않을 경우, 에러가 발생할 수 있습니다.


post-custom-banner

0개의 댓글