본문 바로가기
React

React에서 불변성을 지켜야 하는 이유

by 강깅꽁 2020. 9. 23.

불변성이라는 단어는 React를 관통하는 핵심 키워드라고 볼 수 있습니다. 

 

불변성은 어떤 값을 직접적으로 변경하지 않고 새로운 값을 만들어내는 것입니다.

필요한 값을 변형해서 사용하고 싶다면 어떤 값의 사본을 만들어서 사용해야 합니다.

 

JS에서 원시 타입은 괜찮지만 객체 타입의 불변성을 지키는 것은 고려해야 할 부분이 있습니다. 

 

아래와 같은 코드가 있다고 했을 때 user와 copyUser 변수는 같은 참조(Memory Address) 값을 가집니다. (객체 타입은 참조 값을 주고받습니다.) 

그렇기 때문에 user가 가지고 있는 객체의 값이 변경되면 copyUser도 같은 객체를 가지기 때문에 값이 공유됩니다.

하지만 이런 작동 방식은 값을 예측할 수 없게 하거나 버그를 유발하기 때문에 불변성을 통해서 해결합니다.

const user = { name: 'Choi', age: 25 }
const copyUser = user; // 배열의 복사가 아니라 같은 참조 값을 가짐 
user.age += 1;
/* 
user = { name: 'Choi', age: 26 }
copyUser = { name: 'Choi', age: 26 }
*/

user === copyUser  // true

*call by value, call by reference를 알고 있다면 이해하기 수월합니다.

 

새로운 객체에 ...라는 spread operator를 사용하여 복사를 합니다.  otherUser는 새로운 객체를 할당 받았기 때문에 user와 otherUser가 가리키는 객체의 참조 값은 다르게 됩니다. 

따라서 user의 값 변경은 otherUser의 객체에 영향을 주지 않습니다. (불변성이 잘 지켜졌다고 할 수 있습니다.)

예시로 어떠한 인쇄물을 프린트하면 똑같은 것이 총 두 장 생기는데 한 장의 인쇄물에 낙서를 해도 다른 인쇄물에 영향을 주지 않는 것과 같습니다. 

const user = { name: 'Choi', age: 25 }
const otherUser = { ...user };
user.name = 'Lee';
/* 
user = { name: 'Lee', age: 25 }
otherUser = { name: 'Choi', age: 25 }
*/

user === otherUser  // false 서로 다른 참조 값을 가지고 있음

 

하지만 객체의 깊이가 깊어지면 여전히 문제가 생깁니다.  spread operator는 얕은 복사(shallow copy)를 하게 되는데 완전히 다른 객체를 만들고 싶다면 깊은 복사(deep copy)를 해야 합니다.

const user = { name: 'Choi', age: 25, friends: ['Park', 'Kim']}
const otherUser = { ...user };
user.name = 'Lee';
user.friends.push('Kang');
/* 
user = { name: 'Lee', age: 25, friends: ['Park', 'Kim', 'Kang'] }
otherUser = { name: 'Choi', age: 25, friends: ['Park', 'Kim', 'Kang'] }
*/

user === otherUser  // false
user.friends === otherUser.friends // true

 

다음과 같이 불변성을 유지해 줄 수 있지만 객체의 구조가 복잡해질수록 불변성 유지가 힘들어질 것입니다.

그래서 immer과 같은 라이브러리를 사용해 불변성을 유지해 주기도 합니다. 

const user = { name: 'Choi', age: 25, friends: ['Park', 'Kim']}
const otherUser = { ...user, friends: [...user.friends] };
user.name = 'Lee';
user.friends.push('Kang');
/* 
user = { name: 'Lee', age: 25, friends: ['Park', 'Kim', 'Kang'] }
copyUser = { name: 'Choi', age: 25, friends: ['Park', 'Kim'] }
*/

user === otherUser  // false
user.friends === otherUser.friends // false

 

React 얘기로 넘어와서 React Component가 리렌더링 되는 조건은 Props, State의 값이 바뀌거나 부모 컴포넌트가 리렌더링될 때가 있습니다. 

 

여기서 React Component의 Props나 State의 값이 바뀔 때 바뀌었는지 안 바뀌었는지 어떻게 확인할까요?

이전 값과 현재 값을 전체적으로 비교한다고 가정해봅시다.

상태 객체가 다음과 같을 때 모든 요소를 하나하나 비교하는 방법은 비효율 적일 것입니다. 

  const userState = {
    name: 'Choi',
    age: '25',
    loginToken: 'asfwerasdf',
    friends: ['Lee', 'Park'],
    skills: {
      frontEnd: ['React', 'Vue', 'jQuery', 'HTML', 'CSS'],
      backEnd: ['Node.js'],
      common: ['TS'],
    }
  }

 

따라서, React에서는 값을 비교할 때는 얕은 비교를 실행하여 성능 최적화를 만들어내게 됩니다. 

그렇기 때문에 불변성을 지켜주는 일이 중요한 것입니다. 

 

 

불변성을 지키지 않는 Counter 컴포넌트

import React, { useState } from "react";

export default function Counter() {
  const [state, setState] = useState({ count: 0 });
  console.log("mounted or updated");
  return (
    <div>
      <p>{state.count}</p>
      <button
        onClick={() => {
          state.count += 1;
          setState(state);
        }}
      >
        +1
      </button>
      <button
        onClick={() =>
          setState((prevState) => ({ count: prevState.count - 1 }))
        }
      >
        -1
      </button>
    </div>
  );
}

React Component에서 setState같은 setter함수는 해당 컴포넌트의 리렌더링을 작동시킵니다. 

 

여기서 +1 버튼의 onClick 이벤트가 발생하면 state.count에 변화가 일어나고 setState함수가 실행됩니다. 

이 경우에는 Counter 함수가 리렌더링 되지 않습니다. 

 

왜나하면 state.count +=1을 통해서 값을 변경시켜주었습니다. 하지만 state가 가르키고 있는 객체의 참조 값은 여전히 같습니다. 그리고 setState에 넘겨주는 state의 참조 값 또한 같습니다. 

setState가 실행되면 가지고 있는 state와 전달 받은 state의 참조 값을 비교합니다. 결과는 참조 값이 같기 때문에 리렌더링이 실행되지 않습니다.  비교는 Object.is() 함수를 사용합니다.

 

 

불변성이 지켜지는 Counter Component

import React, { useState } from "react";

export default function Counter() {
  const [state, setState] = useState({ count: 0 });
  console.log("mounted or updated");
  return (
    <div>
      <p>{state.count}</p>
      <button
        onClick={() => {
          setState((prevState) => ({ count: prevState.count + 1 }));
        }}
      >
        +1
      </button>
      <button
        onClick={() =>
          setState((prevState) => ({ count: prevState.count - 1 }))
        }
      >
        -1
      </button>
    </div>
  );
}

 

+1 버튼 클릭 시 setState가 실행되며 콜백함수에 의해 새로운 객체가 리턴됩니다. 이렇게 리턴된 객체의 참조 값은 state가 가리키는 참조 값과 다르므로 Counter Component는 정상적으로 리렌더링 됩니다.

 

 

 

 

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description

 

Object.is()

Object.is() 메서드는 두 값이 같은 값인지 결정합니다.

developer.mozilla.org

ko.reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data

 

성능 최적화 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

컴포넌트가 두 번씩 렌더링 되는 이유

medium.com/@andreasheissenberger/react-components-render-twice-any-way-to-fix-this-91cf23961625