다른 언어와 마찬가지로, 자바스크립트에도 불변성(Immutability)라는 것이 존재한다.
불변성은 함수형 프로그래밍을 만드는 기초적이면서도 핵심적인 원리이며, 불변성을 통해 의도하지 않았던 side effect를 줄일 수 있다.
이름에 대한 불변함
// 기존 코드
var a = 1;
console.log(a); // 1
위와 같은 코드가 있다고 가정해보자. 실제로 일을 할 때는 여러 사람이 하나의 파일에서 작업을 할 가능성이 굉장히 많다.
// 누군가의 손을 거친 코드
var a = 1;
... // 무수히 많은 코드
a = 2;
console.log(a); // 2
중간에 누군가 a에 2를 할당함으로써 원래 a라는 변수를 선언한 사람의 의도와는 다른 값이 출력되고 있다.
이를 방지하기 위해 JS에서는 값을 바꿀 수 없는 상수 변수를 선언할 수 있도록 const 라는 예약어를 제공하고 있다.
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.
const를 통해 선언한 값을 바꾸려고 하면 위와 같은 에러 메세지가 발생한다!
Primitive VS Reference
var를 const로 바꿈으로써 불변성을 지킬 수 있으면 좋겠지만, 안타깝게도 불변성을 지닌 자료형에서만 위와 같은 방식을 사용할 수 있다.
그렇다면 JS에서 불변성을 지닌 자료형에는 어떤 것이 있을까?
Primitive Type(Immutable)
- Number
- String
- Boolean
- Null
- Undefined
- Symbol
원시 타입(Primitive Type)은 메모리에 값이 할당되고 나면 수정이 불가능하다.
var, let을 통해 선언한 변수의 값을 바꾸더라도 실제 메모리에 올라간 값이 수정되는 것이 아니라, 해당 값을 가진 다른 메모리 주소를 참조하게 된다.
또한, 값을 참조하기 때문에 비교 연산을 하게 되면 true 결과 값이 나타난다.
var a = 1; // 메모리에 1이라는 새로운 값을 생성하고 참조
var b = 1; // 메모리에 존재하는 1을 참조
console.log(a === b); // true
var c = a; // a가 참조하고 있는 값인 1을 참조
console.log(a === c); // true
c = 2; // 메모리에 2라는 값을 생성하고 참조
console.log(a); // 1
JS에서 원시 타입의 값 할당은 다른 언어와 똑같이 이루어지기 때문에 여기까지는 큰 문제 없이 이해가 가능할 것이다.
Reference Type(Muttable)
실제로 JS 코드를 작성하다보면 원시 타입보다는 Object(객체)를 더 많이 사용하게 되는데, 객체는 가변성을 지니고 있어 원시 타입처럼 사용하면 엄청난 side effect가 발생한다.
원시 타입과는 다르게 객체는 같은 값을 할당하더라도 메모리의 같은 부분을 참조하는 것이 아니라 새롭게 생성한다.
// 같은 값을 가졌음에도 서로 다른 객체
var o1 = { name : 'lee' };
var o2 = { name : 'lee' };
console.log(o1 === o2); // false
또한, 원시 타입을 const로 선언했을 때와는 다르게, 객체를 const로 선언했을 때는 프로퍼티의 수정이 가능하다.
// const로 선언했지만 property 수정이 가능한 Object
const o1 = { name : 'lee' };
o1.name = 'kim';
console.log(o1); // { name : 'kim' }
기존에 생성된 변수를 할당하는 과정에서도 불변 객체와 다른 결과가 나타난다.
var o1 = { name : 'lee' };
var o2 = o1;
o2.name = 'kim';
// o2의 name 값만 바꾸려고 했지만, o1의 name 값까지 바뀜
console.log(o1, o2, o1 === o2); // { name: 'kim' } { name: 'kim' } true
원시 타입에서는 기존 변수를 참조하고 있을 때 새로운 값을 할당하면 기존 변수에는 영향이 미치지 않았지만, 가변 타입인 객체에서는 원하지 않았던 결과가 발생했다.
o2의 name 값을 바꾸고 싶었지만, o1을 선언하며 생성된 메모리에 대한 참조가 끊어지지 않아 o1의 값까지 바뀌어버린 것이다.
그렇다면 o2의 name만 바꿀 수 있는 방법은 없는걸까?
객체의 복사
Object.assign(target, source)
Object.assign 메소드는 target 객체에 source의 프로퍼티를 덮어씌운 객체를 반환한다.
var o1 = { name : 'lee' };
var o2 = Object.assign({}, o1);
o2.name = 'kim';
// 이제 o2의 name만 바뀐다!
console.log(o1, o2, o1 === o2); // { name: 'lee' } { name: 'kim' } false
원시 타입을 프로퍼티로 가질 경우에는 이렇게 복사가 가능하지만, 프로퍼티로 객체를 가지는 경우(nested Object)에는 말이 달라진다.
var o1 = { name : 'lee', score : [1, 2] };
var o2 = Object.assign({}, o1);
o2.name = 'kim'
o2.score.push(3);
// ??!?!?!?
console.log(o1); // { name: 'lee', score: [ 1, 2, 3 ] }
분명 assign 메소드로 객체를 복사했는데 왜 이렇게 되는걸까?
객체의 복사는 이루어졌지만, score 프로퍼티는 값을 복사한 것이 아니라 메모리에서 참조하는 부분만 복사했기 때문이다.
o1의 score에 영향이 안 가게 하려면 Object.assign 메소드와 같이 o2의 score에 새로운 배열을 생성해서 할당해줘야 한다.
var o1 = { name : 'lee', score : [1, 2] };
var o2 = Object.assign({}, o1);
o2.name = 'kim'
o2.score = o2.score.concat();
o2.score.push(3);
console.log(o1.score, o2.score); // [ 1, 2 ] [ 1, 2, 3 ]
concat 메소드 이외에도 배열을 복제해서 반환하는 메소드를 사용할 수 있다.
이런 중첩 객체가 무수히 많다면 일일히 작업을 해줄수는 없다. 아직 바닐라 JS에서는 이를 위한 메소드를 제공하지 않고 있기 때문에 라이브러리를 사용하는 수 밖에 없다.
https://code.tutsplus.com/articles/the-best-way-to-deep-copy-an-object-in-javascript--cms-39655
불변과 가변 API
Array.push처럼 기존 객체를 변경한다고 해서 반드시 지양해야 하는 것은 아니다.
concat 메소드와는 다르게 복제가 이루어지지 않기 때문에 성능면에서는 더 뛰어나기 때문이다.
개발자라면 항상 듣는 마법의 그 말을 잘 생각하면서 사용하자.
Object.freeze
고맙게도 JS에는 가변 객체를 수정할 수 없도록 꽁꽁 얼리는 메소드가 존재한다.
var o1 = {name : 'lee'};
Object.freeze(o1);
o1.name = 'kim';
console.log(o1); // { name : 'lee' }
원래라면 name 값이 kim으로 바뀌어야 했지만, freeze 덕분에 값이 변하지 않았다.
unfreeze 같은 메소드는 제공하지 않으며, 엄격 모드를 적용하면 에러도 발생하니 마음놓고 사용할 수 있다.
하지만 freeze 메소드 역시 중첩 객체에는 효과가 없다..
var o1 = {name : 'kim', score:[1,2]};
Object.freeze(o1);
o1.name = "hi";
o1.score.push(3);
// .........
console.log(o1); // { name: 'kim', score: [ 1, 2, 3 ] }
Deep Frozen을 원한다면 아래 글을 참고하자.
https://medium.com/@nikhil_gupta/how-to-deepfreeze-a-nested-object-array-800671147d53
How to DeepFreeze a nested Object/Array
So I believe you’ve read my previous article — ‘Differences between Object.freeze( ) & Object.seal( ) in Javascript’ and now you’re…
medium.com
const vs freeze
마지막으로 const와 freeze의 차이에 대해 알아보자.
const는 변수의 재할당을 막는 것이고, freeze는 객체의 프로퍼티 변경만을 막는 것이다.
const o1 = {name:'lee'};
const o2 = {name:'kim'};
o1 = o2; // Assignment to constant variable.
Object.freeze(o1);
o1.name = 'kim'; // freeze로 인해 name 수정 불가
따라서 var이나 let으로 선언하여 freeze를 적용한 객체에 새로운 객체를 할당해버리면 freeze가 풀리게 된다.
var o1 = {name:'lee'};
Object.freeze(o1);
o1 = {name : "kim"}
console.log(o1); // { name : 'kim' }
o1.name = "seo";
console.log(o1); // { name : 'seo' }
따라서, 불변한 객체를 만들고 싶다면 const와 freeze를 모두 사용하자!
참고
https://www.inflearn.com/course/javascript-immutability
[무료] 생활코딩 - JavaScript Immutability - 인프런 | 강의
생활코딩에서 제공하는 자바스크립트 관련 강의로, 자바스크립트에서 데이터를 불변하게 다루는 방법에 대한 수업입니다., - 강의 소개 | 인프런...
www.inflearn.com
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
Object.assign() - JavaScript | MDN
Object.assign() 메서드는 출처 객체들의 모든 열거 가능한 자체 속성을 복사해 대상 객체에 붙여넣습니다. 그 후 대상 객체를 반환합니다.
developer.mozilla.org