main

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

Log
7

오늘 모던 리액트 딥다이브 스터디를 진행하면서 저희 스터디원들끼리 40분가량 논의되었던 내용들에 대해서 정리하고자 포스팅합니다...🧐


🥲 리액트의 동등비교?

먼저 1-1-3장 내용 중에서, 리액트는 Object.isshallowEqual과 함께 사용해 의존성 배열이나 props를 비교해 렌더링한다는 내용이 있습니다.

그런데 useState를 사용해서 useEffect의 콜백함수가 실행되도록 하는 트리거를 만든다고 가정해보겠습니다.

function App() {
  const [count, setCount] = useState({
    counter: 0,
  })
 
  const [num, setNum] = useState(0);
  
  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>
  )
}

위와 같은 예시로 버튼을 클릭 시, state의 값이 변동되게 한다면, 원시값을 가진 num state는 버튼을 한번 클릭한 이후에 계속 2로 값을 고정하기 때문에 console.log('원시값이 변했다.')이 한번만 출력되고 그 이후로는 출력되지 않습니다.

하지만 console.log('후덜덜 객체가 변했다.')는 누를 때마다 출력되게 되게 됩니다. 그래서 저는 useEffect의존성 배열이 Object.is를 기반으로 비교를 할 것이라고 생각했습니다.

왜냐면 count의 state로 매번 새로운 객체를 넣고 있기 때문에, 이를 다르다고 판별하여 매번 useEffect의 콜백함수를 실행하고 있기 때문입니다.

그렇다면 리액트의 율법(?)을 약간 어기고, 새로운 객체가 아닌 기존 객체의 참조값을 가져와 수정하면 어떻게 될까요?

function App() {
  const [count, setCount] = useState({
    counter: 0,
  })
 
  const [num, setNum] = useState(0);
 
  // 기존 count 객체를 가져와 프로퍼티를 추가하는 등의 설정
  const onclickHandler = () => {
    const count2 = count;
    count2.counter += 1;
    count2.name = "daeun";
    setCount(count2);
  }
  
  useEffect(() => {
    console.log('후덜덜 객체가 변했다.')
  }, [count])
 
  useEffect(() => {
    console.log('원시값이 변했다.')
  }, [num])
 
  return (
    <div>
      <h2>현재 숫자는 {count.counter} 입니다.</h2>
      <button onClick={onclickHandler}>객체가 오르는 버튼</button>
      <button onClick={() => setNum(2)}>숫자가 오르는 버튼</button>
    </div>
  )
}

위와 같이 원래의 state 객체 즉, counte의 객체의 참조값을 가져와 그 안의 값을 수정하거나 프로퍼티를 추가해주는 핸들러 함수를 만들어 버튼에 달아주었습니다. 그 다음 버튼을 클릭했을 때 결과는 어떨까요?

아무리 버튼을 눌러도 useEffect의 콜백함수가 실행되지 않았습니다.
따라서 제가 내린 결론은 아무리 객체의 참조값이 같다면 프로퍼티나 값들이 수정되어도 리액트는 변하지 않았다고 판단합니다.
그래서 위의 실험들을 통해 결론적으론 Object.is로 비교를 먼저 하는구나. 라고 생각했습니다.

책의 30장부터 나오는 shallowEqual의 함수를 기반으로 먼저 동등비교를 한다면, 아까의 새로운 객체를 넣어주었을 때, useEffect의 콜백함수는 실행되어선 안됩니다. 왜냐면 이전 객체와 똑같은 프로퍼티를 갖고 있기 때문입니다.


그래서 리액트는 Object.is를 기반으로 동등비교를 하냐? 아니면 직접 추가적으로 구현되어 사용되는 shallowEqual 함수를 기반으로 동등비교를 하냐? 에 대한 논쟁이 있었습니다.

결론적으로 이야기하자면, 책의 32페이지에 정답이 존재했습니다.

리액트에서의 비교를 요약하자면 Object.is로 먼저 비교를 수행한 다음, Object.is에서 수행하지 못하는 비교, 즉 객체 간 얕은 비교를 한번 더 수행하는 것을 알 수 있다.

Object.is로 판별했을 때 true로 나온다면, 즉 참조값이 같다고 나온다면 다음으로 shallowEqual함수의 판별을 생략합니다.
shallowEqual함수로 판별하는 경우에는 Object.is가 false 값으로 나왔을 때만 즉, 참조값이 다르다고 판별되었을 때에 shallowEqual동등비교를 하게 되는 것입니다.


😔 그렇다면 Props 객체는 어떤식으로 동등비교를?

props 객체도 의존성 배열과 똑같은 순서로 동등비교를 진행합니다. props 객체가 이전 렌더링과 변한 것이 없다면 이전 props객체와 똑같은 참조 값을 가지기 때문에 렌더링 되지 않습니다. 하지만 props가 변했을 경우에, 새로운 props 객체를 넘겨주기 때문에 참조값이 달라 렌더링 된다고 알 수 있습니다.

결론

  • 리액트의 동등비교 순서
    • Object.is로 동등비교를 한다.
    • true면 이전 값과 동일하다고 판단하여 렌더링 하지 않는다.
    • Object.is가 false라면 값이 다르다고 판단해 shallowEqual함수를 통해 동등비교를 한다.
    • shallowEqual이 true면 렌더링 하지 않는다.
    • shallowEqual이 false로 반환되면 렌더링이 된다.

김다은 이모지
Daeun Kim
Junior Frontend Engineer