불변성과 리액트

Dev.Jo·2023년 7월 13일
0

리액트

목록 보기
1/2
post-thumbnail

불변값

불변값은 변하지 않는 값이다. 값이 변하지 않는다는 말은 무슨 말일까? 예시를 보면서 이해해보자

let a = 1
a = 2

코드를 분석하면 다음과 같다.
1. 변수 a를 선언하고, 숫자 타입의 값 1을 할당한다.
2. 변수 a에 숫자 타입의 값 2를 재할당한다.

값을 변수에 할당(assign)하면 변수는 값을 기억한다. 이때 숫자 타입 값인 1과 2는 불변값이다. 변수에 값을 할당하기 위해서는 값이 가지는 크기만큼 메모리 공간의 크기를 결정해야한다.

1과 2값을 저장하기 위해서는 자바스크립트의 숫자 자료형의 크기 8바이트만큼의 메모리를 저장해야한다. (그림은 편의를 위해 메모리 주소를 임의로 작성했다.)

값을 재할당할때 메모리 동작은 다음과 같다.

  • 값 1은 메모리 공간에 8바이트 공간이 할당된다.
  • 값 2는 새롭게 메모리 공간에 8바이트 공간이 할당된다.

중요한 점은 값 1이 값 2로 변하는것이 아니라는 점이다. 값 2는 새로운 메모리 공간에 크기가 할당된다.

변하는 것은 변수다. 변수가 가리키는 대상이 바뀌는 것이지 불변값이 바뀌는 것이 아니다!

메모리에 할당된 불변값은 바뀌지 않는다.

값 1이 값 2로 바뀌는 것이 아니라 값 2는 새롭게 메모리 공간이 할당된다.

참조값(객체)

자바스크립트에서 불변값이란 메모리 영역에서 값이 바뀌지 않는 값이라는 것을 알았다. 그렇다면 객체는 어떻게 동작할까?

객체는 불변값이 아닌 참조값이다. 객체는 변경가능한 값이다.

let a = {}
a.b = 1
// a = { b : 1 }
  • 변수 a를 선언하고, 객체 리터럴을 생성하여 할당한다.
  • 변수 a는 객체를 참조한다.
  • 변수 a가 참조하고 있는 객체에 프러퍼티 b를 추가하고 값 1을 할당한다.

객체는 변경 가능한(mutable) 값이다.

메모리측면에서 그림과 함께 살펴보자

  1. 변수 a가 가리키는 메모리 주소에는 다른 메모리 주소에 접근할 수 있는 참조값이 있다.
  2. 그 참조값이 가리키는 메모리 주소에는 생성한 객체가 저장되어 있다.

변수 a는 객체가 저장된 메모리 주소를 직접 가리키는 것이 아니라, 메모리 주소인 참조값을 가진다.

만약 여기서 새로운 프로퍼티 c를 추가하면 객체의 값이 바뀐다. 객체는 변경 가능한 값이다. 중요한 점은 객체를 가리키는 참조값은 그대로라는 점이다.

let a = {}
a.b = 1
a.c = 2

불변객체

자바스크립트 객체는 변경 가능한 값이다. 그렇다면 불변 객체는 무엇일까? 객체가 불변하다는 말은 모순처럼 들린다.

사실 불변객체라는 말은 '객체가 불변하다'라는 뜻이 아니라 '객체를 불변값처럼 다루어야한다'라는 말과 같다.

불변 객체는 변하는 값인 객체를 불변값처럼 다루는 것이다.

"객체를 불변값처럼 다룬다" 라는 것이 중요하다.

객체를 불변값처럼 다루는 방법 - 복사하기

객체를 불변값처럼 다루려면 객체를 사용할때마다 서로 다른 참조값을 가지게하면된다.

객체 a에 프러퍼티 c를 추가해보자. 아래 두가지 방법을 비교해보자.

let a = { b: 1 }

// ❌ 
a.c = 1

// ✅ 
a = {
 ...a,
 c: 1
}
  • ❌ 방법은 변수 a의 참조값은 변하지 않는다.
  • ✅ 방법은 변수 a에 새로운 참조값을 가지는 객체를 할당한다. 마치 새로운 불변값을 할당하는것과 같다.

✅ 방법은 새로운 객체 리터럴을 생성하고, 생성된 객체에 기존의 변수 a의 값들을 복사한다. 참조값이 바뀐다

리액트 상태는 불변값처럼 다루자

리액트는 상태를 불변값으로 다루어야한다. 즉 아래와 같은 코드는 작성하면 제대로 동작하지 않는다.

const [person, setPerson] = useState({
	name: 'jo'
})

person.job = "developer"
setPerson(person)
  • 변수 person이 참조하는 객체를 직접 변경했다. 참조값은 변하지 않는다.
  • 렌더링을 트리거하는 setPerson 함수로 변경한 객체로 상태를 업데이트한다.
  • 객체의 값이 바뀌었음에도 렌더링이 트리거 되지 않는다!
  • 왜냐하면 객체의 참조값은 그대로이기 때문이다

올바르게 동작하려면 객체를 불변값처럼 다루어야한다. 객체를 복사하여 서로 다른 참조값을 가지게 한다.

const [person, setPerson] = useState({
	name: 'jo'
})

let newPerson = {
  ...person,
  job: 'developer'
}

setPerson(newPerson)

리액트는 얕은 비교를 사용한다.

리액트는 상태가 변하는것을 감지할때 얕은 비교를 통해 상태를 비교한다. 얕은 비교는 객체의 참조값만 비교하는 것을 말한다.

1 === 2 // false
"c" === "c" // true

let a = {}
let b = {}
a === b // false 다른 참조값을 가지기 때문!

깊은 비교는 참조값을 비교하는 것이 아니라 값을 직접 비교한다.

let a = {
	c: 1,
  	d: {
      e: 1
    }
}
let b = {
	c: 1,
  	d:{
      e: 1
    }
}
a.c === b.c
a.d.e === b.d.e

a와 b를 깊은 비교하면 같다.

왜 리액트는 얕은 비교를 사용하여 상태를 비교할까?

얕은 비교가 동작 비용이 저렴하기 때문이다. 오로지 참조값만 비교하면 되기 때문에 성능이 뛰어나다. 깊은 비교는 객체의 값을 모두 탐색해야하기 때문에 성능이 떨어진다.

이 때까지의 내용을 정리하면,

  • 리액트는 성능을 위해 얕은 비교로 상태를 비교한다.
  • 새로운 참조값을 가지는 상태가 업데이트 되면 리렌더링을 트리거한다.

상태를 제대로 업데이트하려면 이전 상태와는 새로운 참조값을 가지는 상태로 업데이트해야한다.

깊은 복사로 nested 객체 (중첩된 객체) 상태 다루기

아래와 같은 중첩된 객체(nested)로 이루어진 상태를 다루는 것은 꽤나 까다롭다.

const [person, setPerson] = useState({
	name: 'jo',
  	develop: {
      skill: {
          frontend: { },
		}
    }
})

만약 frontend가 가리키는 객체의 값을 변경하고 얕은 복사를 사용하여 상태로 저장하려고 한다면 코드가 지저분해진다.

setPerson({
 	...person,
  	develop: {
     	 ...person.develop,
      	skill: {
         	...person.develop.skill,
          	frontend: {
             	...person.develop.skill.frontend,
              'javascript': '중'
            }
        }
    }
})

이럴 때에는 깊은 복사를 사용하면 편하다. 깊은 복사는 객체의 값을 복사하여 새로운 참조값을 가지는 객체를 만들어준다.

const newPerson = deepCopy(person)
newPerson.develop.skill.frontend['javascript'] = '중'

setPerson(newPerson)

useImmer와 같이 불변성을 도와주는 라이브러리를 사용하면 좀 더 편하게 사용할 수 있다. immer 라이브러리도 내부적으로는 깊은 복사를 사용한다.

import { useImmer } from 'use-immer';

const [person, updatePerson] = useImmer({
    name: 'jo',
  	develop: {
      skill: {
          frontend: { },
		}
    }
  });

updatePerson(draft=>{
 draft.develop.skill.frontend['javascript'] = '중'
})

정리

  1. 불변값은 메모리의 값이 변하지 않는 값이다.
  2. 객체는 변경가능한 값이다. 불변 객체는 객체를 마치 불변값처럼 다루는 것이다.
  3. 객체를 사용할때는 객체를 직접 변경하는 것이 아니라 새로운 참조값의 객체를 생성하여 사용한다.
  4. 리액트의 상태는 불변값을 기대한다. 그렇기 때문에 객체타입의 상태를 업데이트하려면 새로운 참조값을 가져야한다.
profile
소프트웨어 엔지니어, 프론트엔드 개발자

0개의 댓글