리액트의 상태 업데이트는 항상 불변적으로 수행되어야 한다.

  • mutate(돌연변이한)한 값의 대상과 위치에 따라 컴포넌트가 렌더링 되지 않을 수 있다.
  • 데이터가 실제로 업데이트 된 시기와 이유에 대해 혼란을 겪을 수 있다.

불변성이란?

  • Immutability(불변성)는 상태나 값을 변경하지 않는 것이다.
  • JavaScript에서 예를 들자면,
    • JavaScript의 원시 타입의 값들은 불변성을 갖고 있다.
      • 데이터 타입
        • 원시 타입(string, number, bigint, boolean, undefined, ES6 부터 추가된 symbol)
          • 변경이 불가능한 값(읽기전용)
          • 원시 값을 변수에 할당하면 확보된 메모리 공간(주소)에는 실제 값이 저장됨
          • 원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 원시값이 복사되어 전달된다.
        • 참조 타입(Object, Array, Function)
          • 변경이 가능한 값
          • 객체를 변수에 할당하면 확보된 메모리 공간에 참조값이 저장됨
          • 객체를 가르키는 변수를 다른 변수에 할당하면 원본의 참조값이 복사되어 전달된다.
      • 즉, 원시타입은 재할당을 통해서만 변수에 저장한 값을 변경할 수 있지만, 객체타입은 재할당 없이 객체를 직접 동적으로 추가, 삭제 갱신 등이 가능하다는 것이 큰 차이점이다.

추가로 JavaScript에서 얕은 복사와 깊은 복사

  • 얕은 복사(Shallow copy)
    • 객체를 프로퍼티 값으로 갖는 객체의 경우에 한 단계 까지만 복사하는 것을 말한다.
    • 즉, 첫 객체는 새로운 메모리 주소를 가지지만 내부에 있는 중첩된 객체는 얕은 복사를 한 경우에 같은 메모리 주소를 가진다는 의미가 된다.
    • 얕은 복사 방법으로는 Object.assign(), 전개연산자(spread)
    • 얕은 복사는 하위 중첩되어 있는 객체까지 복사하지 않으므로 공유에 의한 예기치 못한 결과를 얻을 수 있다.
  • 깊은 복사(Deep copy)
    • 하위에 중첩되어 있는 객체(배열 등)까지 모두 복사하는 것을 말함
    • 즉, 원시 값처럼 완전히 새로운 메모리 공간을 차지하는 원시값처럼 복사본을 만든다.
    • 깊은 복사 방법으로는 JSON.stringify(), 재귀함수, lodash의 cloneDeep
  • 결론으로 객체의 참조 특성으로 인해 복사하고자 하는 객체에 중첩된 객체타입이 있는지를 확인하고 그에 맞는 복사 방법을 사용해야 한다.

그래서 리액트 상태를 변경할 때 불변성을 지켜주어야 되는 이유?

  • 리액트에서 불변성을 지켜주어야 되는 이유는 리액트가 상태 업데이트를 하는 원리 때문이다.
  • 리액트는 상태 값을 업데이트 할 때 얕은 비교상태 변경 여부를 체크하며, 수행한다.
  • 즉, 리액트는 객체의 속성 하나하나를 비교하는게 아니라 참조 값만 비교하여 상태 변화를 감지한다.
  • 이런 이유로 리엑트는 state(배열이나 객체)를 업데이트 할 때 setState()를 사용하여 배열이나 객체를 새로 생성해 새로운 참조 값을 만들어 상태를 업데이트 한다.
  • 그리고 리엑트에서 불변성을 지켜주면 다른 이점으로 사이드 이펙트를 방지할 수 있다.

정리하자면

리액트는 불변성을 지켜주면 효과적인 상태 업데이트(얕은 비교 수행: 계산 리소스를 줄여줌)사이드 이펙트를 방지하는 이점(원본 데이터를 건들게 될 경우, 원본 데이터를 참조하고 있는 다른 객체에서 예상치 못한 오류가 발생할 수 있으며, 프로그래밍의 복잡도가 올라간다.)을 가질 수 있게 된다.


불변성을 지키며 상태를 업데이트 하는 방법은?

  • spread operator, map, filter, slice, reduce 등등 새로운 배열을 반환하는 메소드들을 활용하거나

    • splice는 원본데이터를 변경하기 때문에 사용하면 안된다.
  • setState를 이용할 경우

    • 원시타입인 경우에는 값을 바로 넣어서 사용해도 되지만
    • 참조타입인 경우에는 새로운 객체나 배열을 생성한 후 값을 넣어주어야 한다.
    // 원시타입
    const [number, setNumber] = useState(0);
    setState(7);
    
    // 참조타입
    const [person, setPerson] = useState({ name: '', age: 33 });
    setState({ ...person, name: 'seahyun' });

정리

  • 불변성이란 메모리 영역의 값을 변경하지 않는 것이다.
  • 리액트는 불변성을 지켜줌으로써 효율적인 상태업데이트를 한다.
  • 리액트는 불변성을 지켜줌으로써 사이드 이펙트를 사전 방지하고 프로그래밍의 구조를 단순하게 유지한다.
  • 불변성을 가진 원시타입과 달리 참조타입의 경우에는 의도적으로 불변성을 지켜주어야 한다.
    • 방법으로는 새로운 주소 값을 가진 객체(배열)를 생성하여 상태를 업데이트 해주어야 한다. 또는 spread operator, map, filter, slice, reduce 메소드들을 이용하여 새로운 배열의 값을 반환하여 사용한다.

참고링크

자습서: React 시작하기 - React

리액트의 렌더링은 어떻게 일어나는가?

[Java Script] 원시타입과 참조타입 👀

리액트 불변성이란 무엇이고, 왜 지켜야 할까?

리액트에서 불변성이란