[React] 불변성

yongkini ·2023년 2월 21일
0

React

목록 보기
15/19
post-thumbnail

React의 불변성에 대하여

불변성이란?

: 어떤 값을 직접 변경하지 않고, 새로운 값을 만들어내는 것이다... 이게 무슨 뜻일까? 어쨌든, 리액트를 사용할 때는 이러한 불변성을 지켜줘야한다는건 모두가 대략적으로는 안다. 예를 들어,

const [someState, setSomeState] = useState("");
// 위와 같은 state를

const changeStateValue = (value) => {
	someState = value;
}

위에와 같은 함수를 써서 혹은 로직을 써서 직접 값을 변경하면 안되고, setSomeState를 써야한다는 걸 안다. 그리고 리액트는 이렇게 직접 값을 변경하면 안되고, 새로운 값을 만들어내는 불변성을 지켜야(따라야)한다.

JS Engine과 함께 이해해보기

: Javascript Engine은 세개로 구성돼있다고 할 수 있는데, code area, call stack, heap memory 이다. 이 때, 불변성의 원리랑 이 엔진의 구성요소랑 무슨 관련이 있을까?

참조타입 vs 원시타입

: 알다시피 자바스크립트의 데이터 타입은 두 종류로 분류 가능하다.

  • 원시 타입(Primitive Type) : String, Number, Symbol, Undefined, Null, Boolean
  • 참조 타입(Reference Type) : Object, Array

: 이 때, 자바스크립트 엔진이 각 타입의 데이터를 처리하는 방식을 통해 불변성 원리가 왜 리액트에 필요한지를 알아보자.

let stringTypeValue = "0715yk";
stringTypeValue = "yk0715";

위와 같은 코드가 있을 때 JS 엔진은 어떻게 처리를 할까?
처음엔 위와 같은 형태로 call stack의 변수값에 저장을 하게 된다. 그렇다면 두번째 줄을 실행하면? 즉, 변수에 값을 재할당하게 되면 어떻게 될까?

위와 같이 바뀌게 된다. 이 때, 봐야할건 우리가 흔히 얕은 복사 & 깊은 복사 혹은 얕은 비교 & 깊은 비교를 할 때의 얘기처럼 변수에 값을 재할당하니까 call stack에서는 해당 변수에 아예 다른 주소와 값을 저장했다. 그러면 이전에 있던 주소와 값은 어떻게 될까?. 이는 가비지 컬렉터의 판단에 의해 적절한 시점에서 메모리 상에서 삭제된다. 이런식으로 원시타입의 데이터는 콜스택에 저장되고, 재할당될 때, 즉, 값을 직접 수정하게 될 때 주소값 자체가 바뀌어서 저장된다. 이에 따라 이전에 변수에 있던 값(주소 값)과 재할당 이후에 변수에 있는 값(역시 주소 값)은 얕은 비교를 해도, 깊은 비교를 해도 다르다. 얕은 비교를 해도 다른건 주소값이 다르기 때문이고, 깊은 비교를 해도 다른건 0715yk 와 yk0715가 다르기 때문이다.

결론적으로 원시타입은 이런식으로 콜스택에서 저장되고, 변경된다고 생각하면 된다.

: 그럼 이제 참조 타입으로 넘어와보자. 참조 타입은 원시타입과 다르게 저장되는데, 일단 참조타입은 주소값은 원시타입처럼 콜스택에 저장되지만, 실제 값은 힙 메모리(Heap Memory)에 저장된다. 다른분의 블로그에서 그걸 잘 표현한 이미지를 가져와봤다.
(참조 : https://narup.tistory.com/268)

위와 같이 콜스택에 주소와 값이 있지만, 그 값에는 메모리 힙의 주소값이 들어가있고(빨강색 표시가 참조 타입이다), 메모리 힙에는 주소값을 키처럼 쓰고, 그 값으로 실제 값이 들어있다. 결론적으로 한번더 말하면, 실제 값은 힙 메모리에 들어가 있다는 것이다(참조 타입의 경우).

그러면 이러한 배경을 가지고 다시 앞서 한 것과 같이 직접 수정을 가하는 케이스 시뮬레이션을 돌려보자.

const array = [1,2,3];
array.push(77);
console.log(array); // [1,2,3,77]

먼저, 맨위의 코드를 실행했을 경우 콜스택과 메모리 힙에는 다음과 같은 형태로 데이터가 저장될 것이다. 그런 다음에 push 문을 실행하면 ?

콜스택의 주소와 값과 메모리 힙의 주소값은 동일하지만, 값만 바뀐 것을 알 수 있습니다. 이처럼 참조 타입은 원시 타입과 다르게 직접 수정을 가하면 주소 자체가 변동되는 것이 아니라 즉, 변수가 다른 주소값을 참조하게 되는식으로 바뀌는게 아니라 참조하는 주소값은 같고, 해당 주소에 대응되는 메모리 힙 내의 값에 변화가 있는 방식으로 수정이 이뤄집니다.

최종 결론

: 앞서 JS Engine과 원시타입 그리고 참조타입 등을 저장할 때의 방식을 알아봤는데, 이를 통해 처음 주제였던 불변성에 대해서 정리해보면,

  • 불변성이란 값을 직접 수정하지 않고, 새로 만드는 것이다.
  • 리액트에서는 불변성이 지켜져야한다. 즉, 리액트에서는 값을(state) 직접 수정하면 안되고, 새로 만들어야한다.
  • 원시타입을 할당한 변수는 JS 엔진의 로직에 의해 재할당을 하면 그 주소값이(콜스택 내의) 바뀐다. 그러나, 참조타입을 할당한 변수는 재할당을 하면 주소값이 바뀌지 않고, 메모리 힙에 있는 값만 바뀐다.
  • 이 때, React는 setState를 트리거했을 때, 이전 state값과 현재 변경하려는 state값을 얕은 비교를 통해 비교해서 그 비교가 false가 되면 리렌더링을 트리거한다. 이렇게 봤을 때 특정 변수를 바탕으로 setState를 일으키는 로직이 있다면, 원시타입일 때랑 참조타입일 때를 잘 구분해서 처리해야한다.
  • 리액트를 쓸 때 불변성을 지켜야하는 이유는 리액트 자체가 얕은비교를 쓰기 때문인데 그 얕은 비교를 쓰는 이유는 이걸 깊은 비교를 통해하면 리소스 낭비가 생기기 때문이다(얕은 비교가 더 쉽고 심플하기에). 좀 더 정리해보면, 얕은 비교를 하기 위해 불변성을 지켜야하고, 불변성을 지킨다는 의미는 결과적으로 메모리 영역(힙)에서 값을 변경할 수 없게하는, 즉, 콜 스택의 주소값을 변경해주는 형태로 값을 변경해야함을 의미한다.
  • 그래서 보통 object, array 등의 깊은 복사 스킬인 spread syntax, slice(), Object.assign 혹은 loadash._clonDeep 등의 방법론을 우리가 알고 있고, 써먹는거다라고 할 수 있다.

예시

  • 원시 타입 변수를 통해 setState
import './App.css'
import {useState} from 'react'

let myIdValue = '0715yk'

function App() {
  const [myId, setMyId] = useState(myIdValue)

  const onClick = () => {
    myIdValue = 'yk0715'
    setMyId(myIdValue)
  }

  return (
    <div>
      <p>{myId}님 환영합니다!</p>
      <button onClick={onClick}>click 해서 이름 바꾸기</button>
    </div>
  )
}

export default App

위에를 보면 0715yk -> yk0715로 정상적으로 리렌더링 됐다. 하지만, 참조타입을 직접 수정하는 방식으로 하면 어떻게 될까.

  • 참조 타입 변수를 통해 setState
import './App.css'
import {useState} from 'react'

let myIdValue = {id: '0715yk'}

function App() {
  const [myId, setMyId] = useState(myIdValue)

  const onClick = () => {
    myIdValue.id = 'yk0715'
    setMyId(myIdValue)
  }

  return (
    <div>
      <p>{myId.id}님 환영합니다!</p>
      <button onClick={onClick}>click 해서 이름 바꾸기</button>
    </div>
  )
}

export default App

위의 경우는 참조 타입인 객체 내에 id라는 키에 값을 넣어놨고, 그 값을 직접 수정해서 해당 객체를 바탕으로 데이터 리렌더링을 노려봤다(?). 그 결과 리렌더링이 발생하지 않는 것을 볼 수 있었다. 그 이유는 메모리 힙에서는 id 값이 'yk0715'로 정상적으로 변경됐겠지만 콜스택의 주소값은 그대로이기 때문이다.

이런 예시와 같은 일이 발생할 수 있기 때문에 리액트를 사용할 때는 불변성(어떤 값을 직접 수정하면 안되고, 새로운 값으로 만들어야한다는 것)을 지켜줘야 한다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글