본문 바로가기
Frontend/Javascript

[JavaScript] 불변성(Immutability)을 유지하는 방법

by hyeok1235 2023. 6. 25.

객체 타입(Object Type)의 데이터는 변경될 수 있어서 예상치 못한 오류가 발생할 수 있습니다. 다음과 같은 코드를 보면, y의 속성 값을 변경했는데 x의 속성 값도 같이 변경되는 것을 확인할 수 있습니다. 

let x = {
    size: 21
};

let y = x;
y.size = 25;

console.log(x.size); // 25

 

만약 코드가 더욱 복잡해진다면, 코드의 결과를 예측하기가 어려워집니다. 이러한 문제점을 해결하기 위해서 크게 2가지의 방법이 존재합니다. 객체 타입의 데이터를 복사할 때 깊은 복사를 하는 방법과, 객체 자체를 불변하게 만드는 것입니다. 

 

① 깊은 복사하기

얕은 복사는 해당 객체만 복사하여 새로운 객체가 같은 메모리 주소를 참조하고 있게 합니다. 반대로 깊은 복사는 해당 객체의 실제 값 전부를 복사하여 새로운 주소를 참조하게 합니다. 자바스크립트에서 깊은 복사는 Object.assign() 메소드와 spread 연산자를 통해서 할 수 있습니다.

 

1) Object.assign() 메소드

Object.assign() 메소드의 구문은 다음과 같습니다.

Object.assign(target, source)

target 인수는 새롭게 만들어지는 객체이며, source 인수는 복사의 대상이 되는 객체입니다. 

let x_copy1 = {
    size: 21
};

let y_copy1 = Object.assign({}, x_copy1);
y_copy1.size = 25;

console.log(x_copy1.size); // 21

→ y_copy1의 속성 값을 변경해도 x_copy1의 속성 값은 변경되지 않습니다.

 

2) spread 연산자

spread 연산자는 복사하고자 하는 객체 앞에 점 세개, ...을 붙이면 됩니다. 

let x_copy2 = {
    size: 21,
    text: {
        word: 'apple',
    }
};

let y_copy2 = {...x_copy2};
y_copy2.size = 25;
y_copy2.text.word = 'banana';

console.log(x_copy2.size); // 21
console.log(x_copy2.text.word); // 'banana'

사용 방법은 비교적 단순하지만, 깊이가 2단계 이상이라면 깊은 복사가 되지 않습니다. 따라서 2단계 이상의 객체까지 깊은 복사를 하기 위해서라면 spread 연산자를 객체 내부의 객체들까지 사용해주어야 합니다.

 

② 객체를 불변하게 만들기

객체를 복사할 때 깊은 복사를 해서 새로운 객체를 만들 수 있지만, 아예 객체 자체를 불변하게 만드는 방법이 있습니다.

총 3가지 메소드 Object.preventExtensions(), Object.seal(), Object.freeze()를 통해서 객체의 변경을 막을 수 있습니다. 각 메소드들은 불변성을 어느정도까지 유지하는지에 따라 차이가 있습니다. 먼저 결론적으로 말하면

preventExtensions < seal < freeze

순서로 불변성이 강하게 유지됩니다. 그러면 각 메소드들에 대해서 자세하게 설명드리도록 하겠습니다.

 

1) Object.preventExtensions() 메소드

Object.preventExtensions() 메소드는 이름으로 유추할 수 있듯이, 객체의 확장을 막는 메소드입니다. 새로운 속성을 추가하려고 시도한다면, TypeError가 발생하거나 조용히 실패하게 됩니다. 새로운 속성을 추가하지는 못하지만, 기존의 속성 값을 변경하거나 delete 연산자를 통해서 속성을 삭제할 수는 있습니다.

const coordinate1 = {
    x: 10,
    y: 20
};

Object.preventExtensions(coordinate1);

Object.defineProperty(coordinate1, "z", {
  value: 30,
}); // TypeError occurs
console.log(coordinate1); // {x: 10, y: 20}

Object.defineProperty(coordinate1, 'x', {
    value: 15,
});
console.log(coordinate1); // {x: 15, y: 20}

delete coordinate1.y;
console.log(coordinate1); // {x: 15}

 

2) Object.seal() 메소드

Object.seal() 메소드는 속성의 추가와 속성의 삭제를 둘 다 막는 메소드입니다. 단, 기존의 속성 값을 변경하는 것은 가능합니다.

const coordinate2 = {
    x: 40,
    y: 50
};

Object.seal(coordinate2);

Object.defineProperty(coordinate2, "z", {
  value: 60,
}); // TypeError(silent fail) occurs
console.log(coordinate2); // {x: 40, y: 50}

Object.defineProperty(coordinate2, 'x', {
    value: 45,
});
console.log(coordinate2); // {x: 45, y: 50}

delete coordinate2.y; // TypeError(silent fail) occurs
console.log(coordinate2); // {x: 45, y: 50}

 

3) Object.freeze() 메소드

Object.freeze()를 메소드는 속성의 추가, 속성의 삭제, 속성 값 변경을 모두 막는 강력한 메소드입니다. 

const coordinate3 = {
    x: 70,
    y: 80
};

Object.freeze(coordinate3);

Object.defineProperty(coordinate3, "z", {
  value: 90,
}); // TypeError(silent fail) occurs
console.log(coordinate3); // {x: 70, y: 80}

Object.defineProperty(coordinate3, 'x', {
    value: 75,
}); // TypeError(silent fail) occurs
console.log(coordinate3); // {x: 70, y: 80}

delete coordinate3.y; // TypeError(silent fail) occurs
console.log(coordinate3); // {x: 70, y: 80}

주의해야 할 점은, Object.freeze() 메소드를 사용했다더라도 2단계 이상의 객체에는 적용이 되지 않는다는 것입니다. 

 

 

 

* Object.isSealed() 메소드는 객체에 Object.seal() 메소드가 적용되어 있는지 확인할 때 사용할 수 있습니다. 만약 Object.freeze() 메소드가 적용되어 있다면, freeze의 효과가 seal을 포함하고 있기 때문에 true를 반환하게 됩니다. 

Object.isSealed(coordinate1); // false
Object.isSealed(coordinate2); // true
Object.isSealed(coordinate3); // true

 

** Object.isFrozen() 메소드는 객체에 Object.freeze() 메소드가 적용되어 있는지 확인할 때 사용할 수 있습니다. 

Object.isFrozen(coordinate1); // false
Object.isFrozen(coordinate2); // false
Object.isFrozen(coordinate3); // true

 

 

 

 

 

728x90
반응형