[React] React 컴포넌트와 This 란?

Lemon·2022년 9월 13일
3

React

목록 보기
13/21
post-thumbnail

1. this란?

this자신이 속한 객체 or 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(Self-referencing)입니다.
thisthis가 바라보고 있는 객체이며, 어디서 호출하느냐에 따라 달라집니다.
자바스크립트에서 this 키워드는 다양한 방법으로 사용될 수 있습니다. 일반적으로 this 키워드는 부모를 가리키게 되어있습니다. 아무것도 없는 상태에서 this를 가리키게 된다면 window를 가리키게 됩니다.

1-1. 자신이 생성할 인스턴스를 가리킨다란 뜻은?

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  getName() {
      return this.name;
  }
};

this를 이용해서 클래스를 정의하면, 다음부터 새로 만들어지는 객체는 자기 자신을 이용합니다.
Person 클래스this를 이용해서 자기자신을 정의합니다.

const jelly = new Person('Kim', 13);
const brown = new Person('Park', 20);

jelly.getName(); // Kim
brown.getName(); // Park

jellybrownPerson 클래스를 이용해서, 독립된 객체로 만들어졌습니다.
이것은 this자신이 생성할 인스턴스를 가리킨다라고 앞에서 말한 의미와 같습니다.

1-2. this가 결정되는 시점은?

const testObejct = {
  a: '12345',
  consoleA: function() {
    console.log(this.a);
  },
};

testObejct.consoleA(); // 12345

this는 부모를 가리키기 때문에 이 this.atestObject에 있는 a를 출력하게 되는 것 입니다. 하지만 여기서 이렇게 이해하고 넘어가면 안 됩니다.

const testObejct = {
  a: '12345',
  consoleA: function() {
    console.log(this.a);
  },
};

const globalConsoleA = testObejct.consoleA;
globalConsoleA(); // undefined

메서드를 특정한 변수에 저장해두고 그 변수에 담겨진 함수를 실행시켰을 뿐인데 결과가 완벽하게 달라졌습니다.

this가 결정되는 시점은, this가 선언된 시점이 아닌 누가 실행하는 지에 따라서 결정된다는 것을 알아야 합니다.

첫 번째 예시 코드의 this 실행 시점은, testObejct의 객체인 consoleA로써 실행되었기에 this가 부모인 testObject를 가리킨 것이고,

두 번째 예제에서의 this 실행 시점은, 어떤 특정한 변수에 담겨졌기 때문에 더 이상 그 메서드에서 가리키는 this가 원래의 부모 객체인 testObject를 가리키지 않고 글로벌한 this를 가리키게 되는 것 입니다. 이것은 함수 말고도 클래스에서도 동일하게 작용합니다.

리액트에서는 이벤트에 메소드를 넘기기 때문에, 이벤트에서 실행이 될 때는 이미 this는 사라진 상태가 됩니다. 그래서 리액트에서는 this를 강제로 선언 시점에 고정해 두는 메소드를 사용합니다.

const testObejct = {
  a: '12345',
  consoleA: function() {
    console.log(this.a);
  },
};

const globalConsoleA = testObejct.consoleA.bind(testObject);
globalConsoleA(); // 12345

.bind() 문법은 this를 고정시킬 때 사용하는 방법입니다. 지금 보면 함수를 넘길 때에 bind(testObject)라는 것을 추가했는데, 이 의미는 저 함수가 가리키는 this는 항상 testObject를 가리키게 하라는 의미입니다. 이렇게 되면 this가 실종될 일이 없어지게 됩니다.
같은 예제를 이번에는 클래스로 생성하고 클래스에서의 해결법까지 알아보도록 하겠습니다.

class testClass {
  constructor() {
    this.a = '12345';
  }

  consoleA() {
    console.log(this.a);
  }
}

const testClassInstance = new testClass();
testClassInstance.consoleA(); // 12345

const globalConsoleA = testClassInstance.consoleA;
globalConsoleA(); // Error 심지어 에러까지 발생합니다.

객체에서의 해결법처럼, 클래스에서도 메소드를 넘겨줄 때에 bind문법을 사용하면 해결할 수 있습니다.

정리하자면
this는 선언 시점에서 결정되는 것이 아니고, 메소드/함수를 어떤 주체가 실행 하는지에 따라서 결정 됩니다. 이를 무시할 수 있는 방법 중 하나는 bind를 사용해서 강제로 지정하는 방법이 있습니다.


2. 암시적 바인딩

위에서 본 것처럼 this는 알아서 결정됩니다. 자바스크립트 엔진이 this를 결정하는 것입니다. 이것을 암시적 바인딩이라고 합니다.

2-1. 일반 함수에서의 this

전역에서의 this에는 전역 객체(window)가 바인딩됩니다.
일반 함수에서도 this에는 전역 객체(window)가 바인딩됩니다.

console.log(this === window); // true
function check() {
	console.log(this);
};
check(); // window

2-2. 메서드에서의 this

메서드 내부의 this에는 메서드를 호출한 객체가 바인딩됩니다.

const myDog = {
  weight: "5kg",
  getWeight() {
    return `${this.weight} 입니다.`;
  },
};

myDog.getWeight(); //5kg 입니다.

// this에는 myDog이 바인딩 된다.

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

생성자 함수 내부의 this에는 생성할 인스턴스가 바인딩됩니다.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

이렇게 간단하게 사용할 경우에는 this에 대해 고민할 필요가 별로 없습니다. 하지만 함수들이 중첩이 된다 싶을 때부터 this는 예상한 것과 다른 것을 가리키게 됩니다.


3. this 바인딩하기

3-1. this 바인딩은 어디서 쓰일까?

자바스크립트 엔진에 의해서 알아서 정해지는 this를 명시적으로 바인딩할 수 있습니다. 명시적 바인딩을 알아야하는 이유는 this라는 것이 개발자가 원하는 객체가 아닐 수도 있기 때문입니다.

3-2. 바인딩 방법

3-2-1. call

this를 바인딩할 수 있게 print함수와 update함수를 만들어서 적용해봅시다.

function print() {
  return `hi, ${this.name}`;
}

function update(age, hobby) {
  this.age = age;
  this.hobby = hobby;
}
// 이 함수들을 새로운 객체에게 사용할 수 있습니다.

1) 이름 출력하기

const person = {
  name: "yujin"
};

console.log(print.call(person)); // hi, yujin
// print함수를 부른다. this는 person으로 바인딩한다.

2) person객체 업데이트하기

update.call(person, "11", "코딩");
// update함수를 부른다. 
// this는 person으로 바인딩하며 update인자에 ("11,"코딩")을 적용한다.
console.log(person); // {name: "yujin", age: "11", hobby: "코딩"}

3-2-2. apply

applycall과 거의 같습니다. 인자를 나열 형식이 아닌, 배열로 받는다는 차이가 있습니다.

update.apply(person, ["11", "코딩"]);
console.log(person); // {name: "yujin", age: "11", hobby: "코딩"}

3-2-3. bind

callapply는 호출 자체가 목표입니다. bind는 호출하지 않고, **this만 바인딩합니다.
만약 아래에 bind대신 call이나 apply가 있었다면 결과는 'hi yujin' 이었을 것입니다. 하지만 bind를 썻기때문에
함수의 정보가 출력**됐습니다.

bindcall/apply의 차이점을 좀 더 알아봅시다.

(1) bind

update함수에 bindthisperson으로 bind해버리고, getOld라는 이름에 담습니다.

getOld는 인수를 받을 수 있는 함수가 되어버립니다.

(2) call

update함수에 call로 thisperson으로 bind해서 변수에 담아도, 아무것도 담기지 않습니다. **call은 호출하는 메서드**이기 때문입니다.

클래스 컴포넌트를 보면 좀 더 이해하기 쉽습니다.

this바인딩을 해주지 않는다면, this는 호출될 때 결정되므로, this는 HomeContainer가 아닌 button요소가 될 것입니다.


4. React 컴포넌트와 this

React Class 컴포넌트도 this 바인딩을 이용합니다.

4-1. 컴포넌트 안에서 this를 참조할 수 있는 프로퍼티들

  1. state (+setState)
  2. props
  3. refs
  4. 컴포넌트 메서드 ⇒ 이벤트 핸들링시 예외 발생
  5. 생명주기 매서드

4-1-1. 생명주기 메서드

먼저 생명주기 메서드의 this가 어디에 바인딩되는지 알아보겠습니다.

import React from "react";

class App extends React.Component {

  componentDidMount() {
    console.log("componentDidMount() Called");
    console.log("componentDidMount의 this는", this); //App
  }

  render() {
   console.log("render() Called");
   console.log("render()의 this는", this); //App
    return (
      <div>
        <h1>Class Component this</h1>
      </div>
    );
  }
}

export default App;

이렇게 클래스 컴포넌트를 만들고 실행해보면, 콘솔에

이렇게 찍힙니다.

생명주기 메서드의 this는 메서드를 호출한 해당 컴포넌트를 가리킵니다. this가 컴포넌트를 가리키고 있으니 컴포넌트 안에 선언한 다른 메서드도 생명주기 메서드 안에서 this를 사용하면 잘 불립니다.

import React from "react";

class App extends React.Component {

  // 메서드 선언
  componentMethod() => {
    console.log("method called");
    console.log("메서드에 바인딩된 this는", this); //App
  };

  componentDidMount() {
    console.log("componentDidMount() Called");
    this.componentMethod();
  }

  render() {
    return (
      <div>
        <h1>Class Component this</h1>
      </div>
    );
  }
}

export default App;

이렇게 컴포넌트에서 선언한 메서드 역시 해당 컴포넌트가 this로 바인딩되어 있는 것으로 보입니다.

4-1-2. 컴포넌트 메서드 예외

컴포넌트에서 선언한 메서드들은 기본적으로 해당 컴포넌트를 this로 바인딩하고 있습니다. 하지만 JSX 이벤트 핸들러의 콜백 함수로 컴포넌트에서 선언한 메서드를 전달할 때는 상황이 좀 달라지는데요. 화면의 버튼을 클릭하면 숫자가 증가하는 매우 간단한 리액트 앱을 짜보겠습니다.

import React from "react";

class App extends React.Component {
  // 상태값 num 선언
  state = {
    num: 0
  };

  // 버튼을 누르면 this.state.num을 1만큼 증가시킴
  increase() {
    this.setState(current => ({ num: current.num + 1 }));
    console.log("increase메서드의 this는", this);
  }

  render() {
    return (
      <div>
        <h1>Class Component this</h1>
        <h2>{this.state.num}</h2>
	{/* 클릭 이벤트 핸들러의 콜백으로 컴포넌트 메서드 전달 */}
        <button onClick={this.increase}>증가</button>
      </div>
    );
  }
}

export default App;

이렇게 onClick이벤트의 인자로 this를 붙인 메서드를 그대로 넘기면 숫자가 증가하지 않고 this.setState is not a function에러가 발생합니다.

메서드가 실행될 때 바인딩된 this로 setState() 함수를 참조할 수 없기 때문입니다. 이상하죠. 아까 살펴봤던 것처럼 컴포넌트에서 선언한 메서드의 this는 해당 컴포넌트에 바인딩되어 있었으니, 그대로 그 this를 가지고 setState()함수를 참조할 수 있어야 할텐데요.
이유를 알아보기 위해 일단 에러의 원인이 되는 setState()행을 주석으로 처리 해보겠습니다.

increase() {
    // this.setState(current => ({ num: current.num + 1 }));
    console.log("increase메서드의 this는", this);
}

이렇게 하면 클릭 이벤트 발생 시 increase()메서드가 호출될때 this가 어디에 바인딩되어있는지 알 수 있습니다. 콘솔을 보시죠.

신기하게도 전역 객체인 window에 this가 바인딩되어 있습니다! 아까 컴포넌트에서 선언한 메소드의 this가 해당 컴포넌트에 바인딩되어 있었다는 것을 확인했는데도 말이죠. 왜 이런 일이 일어나는 걸까요?

아마 아래와 같은 상황이 일어났으리라 추측해볼 수 있을 것 같습니다. 아래 선언한 클래스를 보시죠.

class App {
  constructor(state){
    this.state = state
  }
  
  showState(){
    console.log(this.state);
  }
}

const app = new App('num');
app.showState(); // num

const showState = app.showState; 
showState(); // TypeError: Cannot read property 'state' of undefined

클래스로 선언한 인스턴스에서 메서드를 호출하는 건 문제가 없지만, 메서드를 다른 변수에 옮겨 호출하면 TypeError가 발생합니다. 이는 메서드가 this를 잃어버렸기 때문입니다. 인스턴스에 .을 붙여 클래스 메서드를 호출하면 this는 정상적으로 클래스에 바인딩되어 멤버변수인 this.state를 참조할 수 있게 됩니다. 하지만 다른 변수에 메서드를 할당해 호출하면 this는 원래 바인딩되어 있었던 클래스를 잃어버리고, 전역객체인 windows 혹은 global에 바인딩됩니다. 따라서 this.state도 참조할 수 없는 것이죠.

그렇다면 우리는 조심스럽게 리액트가 이벤트를 감지하고 콜백 함수를 호출하는 과정에서 컴포넌트 메소드가 다른 변수에 할당되어 따로 호출되는 것과 상응하는 일이 일어나 메소드가 기존의 this 바인딩인 상위 컴포넌트를 잃고 전역객체에 바인딩되었다고 추측할 수 있겠습니다. 그렇다면 이런 상황은 어떻게 해결할까요?

해결방안 1) bind()

Function객체의 메서드인 bind()는 어떤 함수의 this를 명시하는 역할을 합니다. 이 메서드가 호출되면 인자로 받은 객체를 함수의 this로 하는 새로운 함수를 리턴합니다. Constructor()에 작성하거나, 이벤트 핸들러의 인자로 bind()를 호출한 메서드를 넘깁니다.

render() {
    return (
      <div>
        <h1>Class Component this</h1>
        <h2>{this.state.num}</h2>
        {/* 기존 increase메소드에 render()의 this인 App을 바인딩함 */}
        {/* this를 명시적으로 작성해줬기 때문에 this를 잃어버릴 일이 없음 */}
        <button onClick={this.increase.bind(this)}>증가</button>
      </div>
    );
  }

해결방안 2) 화살표 함수

화살표 함수로 작성할 경우 다른 함수와 달리 따로 this를 가지고 있지 않고, this가 항상 상위 스코프의 this를 그대로 참조합니다.

render() {
    return (
      <div>
        <h1>Class Component this</h1>
        <h2>{this.state.num}</h2>
        {/* 컴포넌트 메소드 앞에 붙는 요 this는 App을 가리킨다*/}
        <button onClick={() => this.increase()}>증가</button>
      </div>
    );
  }

React class component에서는 클래스 필드에 화살표 함수를 정의하면, this가 상위 스코프인 constructor 내부의 this로 바인딩됩니다. 이걸 lexical this라고 합니다.

이벤트 핸들러의 인자로 넘긴 화살표 함수는 상위 스코프인 render() 메서드의 this인 App을 바인딩하고, 컴포넌트 메소드를 리턴하는데 이때 컴포넌트 메소드 앞에 붙는 this는 화살표 함수의 this, 즉 App을 가리킵니다.


5. 함수형 컴포넌트의 this

함수형 컴포넌트에서의 this는 클래스형 컴포넌트에서만큼 중요하지 않습니다.

import React, {useState} from "react";

function App() {
  const [num, setNumber] = useState(0);

  const increase = () => {
    setNumber(num+1);
    console.log("increase메소드의 this는", this); //window
  }

  return (
    <div>
      <h1>Class Component this</h1>
      <h2>{num}</h2>
	{/* 딱히 뭘 해줄 필요가 없다! */}
      <button onClick={increase}>증가</button>
    </div>
  );
}

export default App;

일단 함수형 컴포넌트의 상태값은 useState훅으로 관리되기 때문에 컴포넌트의 this로부터 자유롭습니다. 또한 함수형 컴포넌트 자체와 함수형 컴포넌트 안에서 선언한 함수들 모두 전역 객체를 this로 가지기 때문에 애초에 this가 다 같습니다. 그래서 이벤트 핸들러에 콜백 함수를 넘기는 상황에도 딱히 신경 쓸 필요가 없습니다.

함수형 컴포넌트는 클래스형 컴포넌트보다 눈으로 보기에 자연스러운 로직을 가지고 있는 것 같습니다. 클래스형 컴포넌트들은 생성, 갱신될때 생명주기 메소드가 실행되는데, 이 메소드들은 컴포넌트에 명시하지 않아도 자동으로 실행됩니다. 이런 구조는 어쩌면 명시적이지 않습니다.

하지만 함수형 컴포넌트는 훅을 포함해서 함수형 컴포넌트 내에 선언한 함수나 변수들이 순서대로 작동하고, this바인딩을 신경 쓸 필요도 없습니다. 클래스형 컴포넌트보다 명시적인 구조를 가지고 있다는 생각이 드네요.


6. this 관련 용어 정리

이름설명
this자신이 속한 객체 혹은 자신이 생성할 인스턴스를 가리키는 변수
this 바인딩this가 정해지는 현상
call, applythis를 원하는 것으로 바인딩하면서 호출하는 메서드
bindthis를 원하는 것으로 바인딩하는 메서드
일반 함수일반적인 함수. 중첩함수 혹은 콜백함수도 일반 함수이며, 일반 함수의 this는 전역 객체(window)
화살표 함수this가 상위 스코프로 자동 바인드되는 함수 선언 방식
클래스 필드클래스 내부에서 변수처럼 사용되는 것 (private)

🔗 참고 링크
https://velog.io/@jellybrown/ReactJS-this-이해하기-class-component
https://ljh86029926.gitbook.io/coding-apple-react/undefined/this
https://maxkim-j.github.io/posts/react-component-this
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this
https://ibrahimovic.tistory.com/29
This is why we need to bind event handlers in Class Components in React
mdn - Function.prototype.bind()

profile
프론트엔드 개발자 가보자고~!!

0개의 댓글