약 한 달 전, 원티드에서 진행한 프리온보딩 프론트엔드 인턴십 7월의 과제를 진행하면서 팀원의 코드 리뷰를 받은 일이 있었다.
당시 멘토의 코드를 참고해 작성한 터라 왜 그렇게 했는지 이유를 몰랐다. 그냥 '아, 아무 생각 없이 작성했구나' 하고 넘겼다. 그럼에도 한 달 내내 이 궁금증이 가시지 않아 드디어 찾아보게 되었다.
일단 어떤 문제가 발생하는지 궁금해서 issues를 불러오는 메서드의 bind
를 제거했다.
TypeError: Cannot read properties of undefined (reading 'httpClient')
context에서 props로 받은 issuesInstance
는 httpClient
를 인자로 받아 this.httpClient
에 할당한 후 fetch
메서드를 실행한다. 그러므로 여기서 httpClient
속성을 읽지 못하는 것은 this
에 해당 속성이 없음을 말한다.
프로젝트를 일일이 뜯어가며 재현하기는 어려워서 간단한 재현 코드를 작성했다.
class TempClass {
constructor() {
this.one = 1;
this.two = 2;
}
sum() {
return this.one + this.two;
}
}
const tempClass = new TempClass();
const abc = (tempClass) => {
const x = tempClass.sum.bind(tempClass);
return x;
};
console.log(abc(tempClass)()); // 3
정상적으로 TempClass
를 bind
한 abc
함수는 기댓값인 3
을 반환한다. 여기서 bind
를 지우면 위의 에러가 재현된다.
const tempClass = new TempClass();
const abc = (tempClass) => {
const x = tempClass.sum;
return x;
};
console.log(abc(tempClass)());
// TypeError: Cannot read properties of undefined (reading 'one')
sum
메서드의 this
를 찍어 보면 undefined
가 나온다. 인스턴스를 제대로 전달한 것 같은데 왜 에러가 발생하는 것일까? 문제의 원인은 JavaScript
의 this
호출 방식에 있었다.
여타 객체 지향 언어에서 this
는 클래스로 생성한 객체를 가리키지만, JS
의 경우는 조금 다르다고 한다. 실행 컨텍스트가 생성될 때 this
가 생성되기 때문에 함수 호출 방식에 따라 가리키는 방향이 달라진다.
전역에서 this
는 당연히 전역 객체를 가리킨다. 브라우저라면 window
, 노드라면 global
이다. 전역 변수로 선언한 x
가 있다면 이는 window.x
와 this.x
가 동일한 참조를 가진다. 자바스크립트 엔진이 전역 변수 x
를 전역 객체에 할당하고, this
는 그 전역 객체를 가리키기 때문이다.
const x = 1;
window.x // 1
this.x // 1
메서드는 객체의 속성으로 할당하고 객체의 메서드로서 호출하는 함수를 말한다.
const obj = {
x: 1,
method: function () {
return console.log(this);
},
};
obj.method();
// { x: 1, method: [Function: method] }
메서드의 this
는 점 앞의 객체를 가리킨다.
일반 함수로 호출하는 경우에는 this
가 지정되지 않고 전역 객체를 가리킨다.
function checkThis(text) {
return console.log(text, " ", this);
}
checkThis("common func"); // common func Window {window: Window, …}
이처럼 일반 함수에서 this
호출은 전역 객체가 바인딩되므로 주의해야 한다. method
로서 선언하였다 하더라도 호출 자체를 일반 함수로 한다면 this
는 객체를 가리키지 않는다.
콜백 함수에서도 마찬가지로 일반 함수로 호출된 경우 this
는 전역 객체를 가리킨다. 반면, 어떤 객체의 메서드 내에서 실행된다면 this
는 해당 객체를 가리킨다.
setTimeout(func("callback func"), 300);
// callback func Window {window: Window, …}
[1, 2, 3, 4, 5].forEach(func);
/*
1 ' ' Window {window: Window, …}
2 ' ' Window {window: Window, …}
3 ' ' Window {window: Window, …}
4 ' ' Window {window: Window, …}
5 ' ' Window {window: Window, …}
*/
document.body.innerHTML += `<button id="a">클릭</button>`;
document.body.querySelector("#a").addEventListener("click", function (e) {
console.log(this);
});
// <button id="a">클릭</button>
생성자 함수는 new
키워드를 통해 호출한 함수로, 새로운 인스턴스를 생성하며 this
는 자기자신을 가리킨다.
new checkThis("me"); // me checkThis {}
대강 this
를 살펴봤으니 재현 코드의 문제점으로 돌아가 보자.
const tempClass = new TempClass();
const abc = (tempClass) => {
const x = tempClass.sum;
return x;
};
console.log(abc(tempClass)());
// TypeError: Cannot read properties of undefined (reading 'one')
오류가 발생한 이유는 메서드에서 일반 함수로 변경되었기 때문이다. abc
내부의 x
는 tempClass.sum
참조만 하고 실행하지 않았기 때문에 TempClass
에 대한 인스턴스와 관련된 실행 컨텍스트가 생성되지 않는다. 오히려 일반 함수 실행 컨텍스트가 생성되어 TempClass
와의 연관성이 사라진다. 그렇기에 x
로부터 실행되는 sum
메소드의 this
는 아무것도 가리키지 않는 undefined
가 된다. 따라서 명시적인 바인딩을 통해 this
가 가리켜야 할 인스턴스를 알려주면 문제없이 실행된다.
class TempClass {
constructor() {
this.one = 1;
this.two = 2;
}
sum() {
console.log(this); // TempClass {one: 1, two: 2}
return this.one + this.two;
}
}
const tempClass = new TempClass();
const abc = (tempClass) => {
const x = tempClass.sum.bind(tempClass);
return x;
};
console.log(abc(tempClass)()); // 3
React
의 Context API
에서 인스턴스의 여러 메서드를 할당하고 명시적으로 바인딩한 이유를 다시 살펴봤다. 과제 코드에서는 필요한 곳에서 메서드를 실행하기 위해 참조만 변수에 할당했다. 그렇기 때문에 인스턴스와 연관성이 사라졌고 this
는 갈 곳을 잃어 에러를 뱉어냈다. 아마 Context
내에서 메서드를 모두 실행한 후 결과만 value
로 넘기는 식이었다면 명시적 바인드가 필요없었을 것이다.
참 this
는 어렵다.