불변값은 변하지 않는 값이다. 값이 변하지 않는다는 말은 무슨 말일까? 예시를 보면서 이해해보자
let a = 1
a = 2
코드를 분석하면 다음과 같다.
1. 변수 a를 선언하고, 숫자 타입의 값 1을 할당한다.
2. 변수 a에 숫자 타입의 값 2를 재할당한다.
값을 변수에 할당(assign)하면 변수는 값을 기억한다. 이때 숫자 타입 값인 1과 2는 불변값이다. 변수에 값을 할당하기 위해서는 값이 가지는 크기만큼 메모리 공간의 크기를 결정해야한다.
1과 2값을 저장하기 위해서는 자바스크립트의 숫자 자료형의 크기 8바이트
만큼의 메모리를 저장해야한다. (그림은 편의를 위해 메모리 주소를 임의로 작성했다.)
값을 재할당할때 메모리 동작은 다음과 같다.
중요한 점은 값 1이 값 2로 변하는것이 아니라는 점이다. 값 2는 새로운 메모리 공간에 크기가 할당된다.
변하는 것은 변수다. 변수가 가리키는 대상이 바뀌는 것이지 불변값이 바뀌는 것이 아니다!
메모리에 할당된 불변값은 바뀌지 않는다.
값 1이 값 2로 바뀌는 것이 아니라 값 2는 새롭게 메모리 공간이 할당된다.
자바스크립트에서 불변값이란 메모리 영역에서 값이 바뀌지 않는 값이라는 것을 알았다. 그렇다면 객체는 어떻게 동작할까?
객체는 불변값이 아닌 참조값이다. 객체는 변경가능한 값이다.
let a = {}
a.b = 1
// a = { b : 1 }
객체는 변경 가능한(mutable) 값이다.
메모리측면에서 그림과 함께 살펴보자
참조값
이 있다.변수 a는 객체가 저장된 메모리 주소를 직접 가리키는 것이 아니라, 메모리 주소인 참조값을 가진다.
만약 여기서 새로운 프로퍼티 c
를 추가하면 객체의 값이 바뀐다
. 객체는 변경 가능한 값이다. 중요한 점은 객체를 가리키는 참조값은 그대로라는 점이다.
let a = {}
a.b = 1
a.c = 2
자바스크립트 객체는 변경 가능한 값이다. 그렇다면 불변 객체는 무엇일까? 객체가 불변하다는 말은 모순처럼 들린다.
사실 불변객체라는 말은 '객체가 불변하다'라는 뜻이 아니라 '객체를 불변값처럼 다루어야한다'라는 말과 같다.
불변 객체는 변하는 값인 객체를 불변값처럼 다루는 것이다.
"객체를 불변값처럼 다룬다" 라는 것이 중요하다.
객체를 불변값처럼 다루려면 객체를 사용할때마다 서로 다른 참조값을 가지게하면된다.
객체 a에 프러퍼티 c를 추가해보자. 아래 두가지 방법을 비교해보자.
let a = { b: 1 }
// ❌
a.c = 1
// ✅
a = {
...a,
c: 1
}
✅ 방법은 새로운 객체 리터럴을 생성하고, 생성된 객체에 기존의 변수 a의 값들을 복사한다. 참조값이 바뀐다
리액트는 상태를 불변값으로 다루어야한다. 즉 아래와 같은 코드는 작성하면 제대로 동작하지 않는다.
❌
const [person, setPerson] = useState({
name: 'jo'
})
person.job = "developer"
setPerson(person)
올바르게 동작하려면 객체를 불변값처럼 다루어야한다. 객체를 복사하여 서로 다른 참조값을 가지게 한다.
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)로 이루어진 상태를 다루는 것은 꽤나 까다롭다.
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'] = '중'
})