자바스크립트에서 중요한 개념으로 객체 불변성에 대해서 알아 보자.
객체는 참조(reference) 형태로 값을 주고 받는다. 하나의 객체가 생성되고 그 값을 다른 객체들이 참조 하고 있다면 의도 하지 않은 값의 변형으로 문제가 발생하는 경우가 있을 수 있다. 보통 이런 경우 레퍼런스를 참조한 다른 객체에서 객체를 변경 하기 때문이다. 이와 같은 문제를 방지 하기 위해서는 불변의(immutable) 객체를 만들거나 객체 복사를 통해 새로운 객체를 생성 한 후 변경하는 것이 안전하겠다.
Immutable value (primitives)
Boolean / String / Number / null / undefined
위 값들은 원시 타입(primitive type)으로 불변의 성격을 가지고 있다.
var x = 1
var y = 'a'x = 2
y = 'b'console.log (x, y) // 2, 'b'
재 할당은 가능하다. 메모리 상에는 1, 'a'
/ 2, ‘b’
모두 존재 하며 x, y
가 재 할당된 값을 가리키도록 변경된 것이다.
const x = 1
x = 2console.log (x) // 오류 ...
y = 3
y = 4 라고하자console.log (y) // 4
ES6 에서는 const
는 재선언, 재할당이 불가능 하다.
const user = { name: 'Jack' }
user.name = 'John'console.log(user) // { name: 'John' }
하지만 객체 내부까지 깊은 곳의 재할당 까지 제어 하지는 않는다.
var foo = 'immutable'
var boo = foo.replace('imm', 'm')console.log(foo) // immutable
console.log(boo) // mutable
변수 foo
의 값은 변하지 않는 원시타입 이기 때문에 replace()
메서드는 새로운 문자를 생성해서 반환 하였다고 볼 수 있다.
Mutable value
원시 타입의 값을 제외하고 나머지는 모두 객체 타입으로 새로운 값을 만들지 않고도 직접 변경이 가능 하다.
var arr = [1]
var arrCopy = arrarrCopy.push(2)console.log(arr, arrCopy) // [1, 2], [1, 2]
배열은 객체이며 불변성을 가지고 있지 않다. 그래서 push()
메서드는 arrCopy
가 참조 하고 있는 arr
의 값도 직접적으로 영향을 준다.
var obj = { name: 'Jack' }
var objCopy = objobjCopy.name = 'John'console.log(obj, objCopy) // { name: 'John' }, { name: 'John' }
이러한 결과는obj
와 objCopy
가 참조하는 곳이 같기 때문이다.
대응 방법
Object.assign : 객체 참조가 아닌 객체 복사(cloning)
Object.freeze : 객체의 불변화를 통한 객체 변경 방지
Object.assign
Object.assign()
메서드는 타깃 객체의 속성만을 대상으로 객체로 복사 한다. (IE를 지원하지 않는다.)
// syntax
// Object.assign(target, ...obj)const o1 = { a: 1 }
const o2 = { b: 2 }const obj = Object.assign({}, o1, o2)console.log(obj) // { a: 1, b: 2 }
console.log(o1) // { a: 1 }// 타깃 객체가 복사 됨const o3 = { a: 1 }
const o4 = { b: 2 }
const o5 = { c: 3 }const obj1 = Object.assign(o3, o4, o5)console.log(obj1) // { a: 1, b: 2, c: 3 }
console.log(o3) // { a: 1, b: 2, c: 3 }// 타깃 객체 자체가 변경됨
하지만 Object.assign()
메서드는 객체 내부의 객체가 있다면 완전한 복사를 하지 못한다는 것을 알아야 한다.
const user = {
name: 'Jack',
body: {
weight: 100
}
}
const userCopy = Object.assign({}, user)
console.log(user === userCopy)
// false - user와 userCopy의 참조는 다르다.console.log(user.body === userCopy.body)
// true - 객체 내부 객체의 참조는 같다.
user.body.weight = 200
console.log(user.body.weight); // 200
console.log(userCopy.body.weight); // 200
Object.freeze
Object.freeze()
메서드를 사용하면 불변성의 객체를 만들 수 있다.
let user = { name: 'Jack' }Object.freeze(user)user.name = 'John' // 재할당 할 수 없다.console.log(user) // error...
user
가 불변 객체가 되어 재할당이 불가 하게 되었다. 하지만 Object.assign
와 마찬가지로 객체 내부의 객체에는 대응 하지 못한다.
const user = {
name: 'Jack',
body: {
weight: 100
}
}Object.freeze(user)user.body.weight = 200 // 재할당 가능하다.console.log(user) // { user: 'Jack', body: { weight: 200 } }
객체 내부의 객체까지 불변하게 만들려면 deep freeze 해야 한다.
function deepFreeze(obj) {
const props = Object.getOwnPropertyNames(obj) props.forEach((name) => {
const prop = obj[name] if (typeof prop === 'object'&& prop! == null) {
deepFreeze(prop)
}
}) return Object.freeze(obj)
}const user = {
name: 'Jack',
body: {
weight: 100
}
}deepFreeze(user)user.name = 'Jack' // 재할당 불가
user.body.weight = 200 // 재할당 불가console.log(user) // 오류
하지만 Object.assign
과 Object.freeze
을 사용하여 완전한 불변 객체를 만드는 방법은 추가 비용이 들기도 하고 성능상 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋다.
오픈소스
- Immutable.js
- lodash