[JS] Context에 전달된 인스턴스 bind한 이유

찐새·2023년 8월 15일
0

Javascript

목록 보기
9/11
post-thumbnail

약 한 달 전, 원티드에서 진행한 프리온보딩 프론트엔드 인턴십 7월의 과제를 진행하면서 팀원의 코드 리뷰를 받은 일이 있었다.

context bind

당시 멘토의 코드를 참고해 작성한 터라 왜 그렇게 했는지 이유를 몰랐다. 그냥 '아, 아무 생각 없이 작성했구나' 하고 넘겼다. 그럼에도 한 달 내내 이 궁금증이 가시지 않아 드디어 찾아보게 되었다.

1. TypeError: Cannot read properties of undefined

일단 어떤 문제가 발생하는지 궁금해서 issues를 불러오는 메서드의 bind를 제거했다.

TypeError: Cannot read properties of undefined (reading 'httpClient')

context에서 props로 받은 issuesInstancehttpClient를 인자로 받아 this.httpClient에 할당한 후 fetch 메서드를 실행한다. 그러므로 여기서 httpClient 속성을 읽지 못하는 것은 this에 해당 속성이 없음을 말한다.

2. 재현

프로젝트를 일일이 뜯어가며 재현하기는 어려워서 간단한 재현 코드를 작성했다.

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

정상적으로 TempClassbindabc 함수는 기댓값인 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가 나온다. 인스턴스를 제대로 전달한 것 같은데 왜 에러가 발생하는 것일까? 문제의 원인은 JavaScriptthis 호출 방식에 있었다.

3. this

여타 객체 지향 언어에서 this는 클래스로 생성한 객체를 가리키지만, JS의 경우는 조금 다르다고 한다. 실행 컨텍스트가 생성될 때 this가 생성되기 때문에 함수 호출 방식에 따라 가리키는 방향이 달라진다.

3-1. 전역에서의 this

전역에서 this는 당연히 전역 객체를 가리킨다. 브라우저라면 window, 노드라면 global이다. 전역 변수로 선언한 x가 있다면 이는 window.xthis.x가 동일한 참조를 가진다. 자바스크립트 엔진이 전역 변수 x를 전역 객체에 할당하고, this는 그 전역 객체를 가리키기 때문이다.

const x = 1;
window.x // 1
this.x // 1

3-2. 메서드에서의 this

메서드는 객체의 속성으로 할당하고 객체의 메서드로서 호출하는 함수를 말한다.

const obj = {
  x: 1,
  method: function () {
    return console.log(this);
  },
};

obj.method();

// { x: 1, method: [Function: method] }

메서드의 this는 점 앞의 객체를 가리킨다.

3-3. 함수에서의 this

일반 함수로 호출하는 경우에는 this가 지정되지 않고 전역 객체를 가리킨다.

function checkThis(text) {
  return console.log(text, " ", this);
}

checkThis("common func"); // common func   Window {window: Window, …}

이처럼 일반 함수에서 this 호출은 전역 객체가 바인딩되므로 주의해야 한다. method로서 선언하였다 하더라도 호출 자체를 일반 함수로 한다면 this는 객체를 가리키지 않는다.

3-4. 콜백 함수에서의 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>

3-5. 생성자 함수에서의 this

생성자 함수는 new 키워드를 통해 호출한 함수로, 새로운 인스턴스를 생성하며 this는 자기자신을 가리킨다.

new checkThis("me"); // me   checkThis {}

4. 메서드에서 일반 함수로

대강 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 내부의 xtempClass.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

5. 결론

ReactContext API에서 인스턴스의 여러 메서드를 할당하고 명시적으로 바인딩한 이유를 다시 살펴봤다. 과제 코드에서는 필요한 곳에서 메서드를 실행하기 위해 참조만 변수에 할당했다. 그렇기 때문에 인스턴스와 연관성이 사라졌고 this는 갈 곳을 잃어 에러를 뱉어냈다. 아마 Context 내에서 메서드를 모두 실행한 후 결과만 value로 넘기는 식이었다면 명시적 바인드가 필요없었을 것이다.

this는 어렵다.


참고
코어 자바스크립트 - this
ChatGPT

profile
프론트엔드 개발자가 되고 싶다

0개의 댓글