main

리액트 딥다이브 살펴보기 I

Log
12

본 내용은 리액트 딥다이브 1장 내용을 기반으로 작성하였습니다.🐤

📌 리액트는 shallow한 비교를 한다.

1장-1에서 리액트는 얕은 비교만 한다고 나와있어서 실제로 한번 실험을 해보았습니다.

import { useState, useEffect } from 'react'
 
function App() {
  const [count, setCount] = useState({
    counter: 0,
  })
  const [num, setNum] = useState(0);
  
  // 두 개의 useEffect
  useEffect(() => {
    console.log('후덜덜 객체가 변했다.')
  }, [count])
 
  useEffect(() => {
    console.log('원시값이 변했다.')
  }, [num])
 
  return (
    <div>
      <h2>현재 숫자는 {count.counter} 입니다.</h2>
      <button onClick={() => setCount({
        counter: 2
      })}>객체가 오르는 버튼</button>
      <button onClick={() => setNum(2)}>숫자가 오르는 버튼</button>
    </div>
  )
}
 
export default App

코드를 보면 두개의 버튼이 있는데요, 하나의 버튼은 counter이라는 프로퍼티를 가진 객체의 state 값이 변하게 하고, 다른 하나는 원시값인 num의 state를 변경하는 버튼입니다.
그리고 각 state마다 useEffect를 설정해주었는데요, 각 값이 변화할때만 useEffect 내부의 콘솔 함수가 출력됩니다.

먼저, 원시값의 num state부터 예상하자면, 버튼을 누른 처음에만 console.log('원시값이 변했다.')가 출력 될 것입니다. 그 이후에는 값이 2로 고정되기때문에, state의 얕은비교를 통해 같다고 판별되어 useEffect의 콜백함수가 실행되지 않을 것으로 예상됩니다.
실제로도 그렇게 작동됩니다!

그렇다면, 객체인 state는 어떻게 작동할까요?
먼저, 버튼을 누르게 되었을 때 count객체는 { counter: 2 }로 바뀌게 되고, 값이 변동했기 때문에 useEffect의 콜백함수가 실행됩니다. 그 다음에 눌렀을 때도 { counter: 2 }로 값은 동일할텐데요. 하지만 얕은 비교를 하기 때문에 계속 값이 변했다고 판별하여 매번 useEffect의 콜백함수가 실행되게 됩니다.

따라서 렌더링 방지나, 최적화를 위해서 state는 최대한 객체가 아닌 원시값으로 활용되는 것이 가장 좋을 것 같다고 생각이 들었습니다.


📌 그 놈의 망할 this

this에 관해서 예전에 블로그에 블로깅한 적이 있었는데, 다시 마주하니 새하얘진 기분이 들어, this에 대해서 확실하게 정리해두고자 합니다.

기본적으로 this는 아래만 기억하면 됩니다.

  • this 타파하기

    • 일반함수에서 thiswindow 객체를 가리킨다.
    • 화살표 함수 내에서 this는 원래 아예 존재하지 않는다. 그래서 상위환경 this를 찾아댕긴다.
  • 외워야할 것

    • 생성자 함수에서 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
    • 메서드 호출은 메서드를 호출한 인스턴스다.
    • apply, call, bind는 메서드의 첫번째 인수로 전달한 객체가 this다.

오늘은 타입스크립트와 함께 이 this라는 놈을 한번 타파해볼 생각입니다.


🧐 실험1

function Daeun() {
  console.log(this);
}
 
Daeun() // window

먼저 기본적으로 일반 함수 내에서 thiswindow 객체를 가르키게 됩니다.

function Daeun() {
  console.log(this); // window
  function Kim() {
    console.log(this); // window
  }
  Kim()
}
 
Daeun() // window, window
 
 
const obj = {
  a: function() {
    function a() {
      console.log(this); // window
    }
    a()
  }
}
 
obj.a() // window

위 사항도 모~두 window객체를 가르키게 됩니다. 먼저 일반 함수 내에서는 모두! window객체를 가르키게 되기 때문입니다.

하지만 메서드인 경우는 뭐라고 했죠? 그 호출한 인스턴스를 가르키게 됩니다.


🧐 실험2

const obj = {
  a: function() {
    console.log(this);
    },
   b() {
    console.log(this);
   }
}
 
obj.a() // {a: a(), b: b()}
obj.b() // {a: a(), b: b()}

위의 a, b 메서드 모두 메서드로 사용되고 있기 때문에 this가 인스턴스를 가리키고 있습니다.
하지만 메서드 내부에 일반 함수를 정의하게 되면 그때는 thiswindow 객체를 가르키게 됩니다.

const obj = {
  a: function() {
    console.log(this); // {a: a(), b: b()}
    function aa() {
      console.log(this); // window
    }
    aa();
    },
   b() {
    console.log(this); // {a: a(), b: b()}
    function bb() {
      console.log(this); // window
    }
    bb();
   }
}
 
obj.a() 
obj.b()

아래와 같이 콜백함수들도 모두 window를 가르키게 됩니다.

const obj = {
  value : 200;
  foo() {
    console.log(this); // {value : 200, foo : f}
 
    function bar() {
      console.log(this); // window
    }
    bar();
  },
};
 
const obj = {
  foo() {
    console.log(this);  // obj 객체결과나옴
 
    setTimeout(function () {
      console.log(this);  // window
    }, 1000);
  },
};

🧐 실험3

그렇다면 방금 사용했던 모든 것들은 화살표 함수로 바꿔보면 어떨까요?

const Daeun = () => { console.log(this) };
 
Daeun(); // window

위에서 Daeun 화살표 함수는 전역에서 정의되었고, 상위로 타고 가면서 결국 window를 가리키게 됩니다.

const Daeun = () => {
  console.log(this); // window
  const daeun = () => {
    console.log(this); // window
  }
  daeun()
}
 
Daeun()

위의 화살표 함수 두개 모두 window객체를 가르키게됩니다. 화살표 함수는 기본적으로 this가 없으며 this를 찾기 위해 계속 상위환경으로 올라가기 때문입니다. 결국 마지막인 window를 가르키게 되는 것입니다.

그렇다면 객체 내부 메서드로 화살표 함수를 쓰게 된다면 어떨까요?

const obj = {
  a: () => {
    console.log(this);
  }
}
 
obj.a() // window

원래 아까 일반함수라면 인스턴스를 가르키게 되었을 텐데 위는 window객체를 가르키게됩니다. 메소드의 this는 자신을 호출한 객체가 아니라 함수 선언 시점의 상위 스코프인 전역객체를 가리키게 되기때문입니다. 아래도 모두 같은 원리입니다.

const obj = {
  a: () => {
    console.log(this); // window
    const b = () => { console.log(this)}; // window
    b()
    },
}
 
obj.a();

이외에도 이벤트 핸들러내부의 콜백함수에서도 확인할 수 있습니다.

const button = document.getElementById('myButton');
 
button.addEventListener('click', () => {
  console.log(this);	// Window
  this.innerHTML = 'clicked';
});
 
button.addEventListener('click', function() {
   console.log(this);	// button 엘리먼트
   this.innerHTML = 'clicked';
});

위의 화살표함수 내부의 this는 window를 가르키게됩니다. 왜냐면 this가 없기 때문에 상위 환경을 타고 올라가기 때문입니다. 반면 아래의 addEventListener의 콜백함수에서는 this에 해당 이벤트 리스너가 호출된 엘리먼트가 바인딩되도록 정의되어 있습니다.

🧐 실험4

var obj1 = {
  name: 'Lee',
  sayName: function() {
    console.log(this.name);
  }
}
 
var obj2 = {
  name: 'Kim'
}
 
obj2.sayName = obj1.sayName;
 
obj1.sayName(); // Lee
obj2.sayName(); // Kim

메소드를 호출하게 되면 호출한 객체에 바인딩됩니다.
위의 예제는 꽤나 쉬운 것 같습니다.

function test() {
  console.log(this);
}
 
const obj = {
  a: test
}
 
obj.a() //  {a: test()}

위의 예제와 같이 호출한 객체 obj를 가리키고 있는 것을 확인할 수 있습니다.


🧐 실험5

function Person(name) {
  this.name = name;
}
 
Person.prototype.getName = function() {
  return this.name;
}
 
var me = new Person('Lee');
console.log(me.getName()); 
// Lee
// 우선 프로토타입에서 name프로퍼티를 찾는다. 없으니 체이닝에 의해 me 객체에서 찾아서 반환
 
 
Person.prototype.name = 'Kim';
console.log(Person.prototype.getName()); 
// Kim
// 우선 프로토타입에서 name프로퍼티를 찾는다. 찾았으니 반환.

생성자 함수라면 그 인스턴스를 가르키게뒵니다.

프로토타입 객체 메소드 내부에서 사용된 this도 일반 메소드 방식과 마찬가지로 해당 메소드를 호출한 프로토타입 오브젝트 객체에 바인딩된다.
그래서  this의 프로퍼티를 찾을 때 우선, 직접 바인딩 되어있는 프로토타입 오브젝트에서 찾고, 없으면 체이닝에 의해 new생성자로 생성된 객체에서 찾게 됩니다.


정리

따라서 결론적으로 this의 바인딩을 제대로 잡아주고 싶다면 call, bind, apply를 사용하면 됩니다.
일반 함수는 메서드로 사용되거나, call, bind, apply 메서드를 사용하지 않는이상 모두 window 객체를 가르키게 됩니다. 메서드로 사용되면 인스턴스를 가르키게 됩니다. 그리고 call, bind, apply를 사용하게 되면 호출한 그 대상을 가르키게 됩니다.

하지만 화살표함수는 늘 그 상위 환경을 가르키게 됩니다.

  • this 값은 런타임에 결정
  • 함수를 선언할 때 this를 사용할 수 있다.
  • 다만, 함수가 호출되기 전까지 this엔 값이 할당되지 않는다.

    따라서 함수는 정의된 즉시 정적으로 스코프가 결정되지만, this같은 경우에는 동적으로 결정된다고 볼 수 있겠습니다.

김다은 이모지
Daeun Kim
Junior Frontend Engineer