객체의 복사, 불변성의 문제

Maliethy·2021년 10월 25일
0

javascript error

목록 보기
2/5
post-custom-banner

1. issue

실무에서 다음 a와 같은 구조의 배열을 복사해서 안의 프로퍼티 값을 변경하니 a의 불변성이 지켜지지 않는 문제가 발생했다.

const a = [
{ first: 1, second: 2, third: 3 },
{ first: 4, second: 5, third: 6 },
{ first: 7, second: 8, third: 9 },
];

1) 전개구문(spread syntax)

배열 a를 전개 구문으로 복사한 b를 map을 이용해 안의 객체 first의 값을 바꿔보자.
그러면 a의 값 또한 변하면서 불변성이 지켜지지 않는다.

let b = [...a];
b.map((obj) => {
  obj.first = 10;
});



// console.log("a", a);
// console.log("b", b);

// a [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]
// b [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]

전개구문은 다음과 같은 특성이 있기 때문이다.

Spread 문법은 배열을 복사할 때 1 레벨 깊이에서 효과적으로 동작합니다. 그러므로, 다음 예제와 같이 다차원 배열을 복사하는것에는 적합하지 않을 수 있습니다.

const arr = [1, 2, 3];
const arr2 = [...arr]; // arr.slice() 와 유사
arr2.push(4);
// console.log("arr", arr);
// console.log("arr2", arr2);
// arr [ 1, 2, 3 ]
// arr2 [ 1, 2, 3, 4 ]
const arr3 = [[1], [2], [3]];
const arr4 = [...arr3];
arr4.shift().shift();
// console.log("arr3", arr3);
// console.log("arr4", arr4);
// arr3 [ [], [ 2 ], [ 3 ] ]
// arr4 [ [ 2 ], [ 3 ] ]

2) Array.prototype.slice()

let c = a.slice();
c.map((obj) => {
  obj.first = 10;
});

// console.log("a", a);
// console.log("c", c);

// a [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]
// c [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]

Array.prototype.slice()로 객체를 참조하는 경우도 마찬가지 결과가 나온다. slice()는 다음과 같은 특성이 있기 때문이다.

slice()는 객체 참조를 새 배열로 복사합니다. 원본 배열과 새 배열은 모두 동일한 객체를 참조합니다. 참조 된 객체가 변경되면 변경 내용은 새 배열과 원래 배열 모두에서 볼 수 있습니다.

3) Array.prototype.slice.call()

비슷한 방법으로 call()은 이미 할당되어있는 다른 객체의 함수/메소드(아래 예시에서는 slice)를 호출하는 해당 객체(아래 예시에서는 a)에 재할당할 때 사용한다. 위의 경우와 마찬가지 결과가 나온다.

let d = Array.prototype.slice.call(a);
d.map((obj) => {
  obj.first = 10;
});


// console.log("a", a);
// console.log("d", d);

// a [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]
// d [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]

4) Array.prototype.concat()

concat() 역시 깊은 복사가 되지 않는다.

let e = [].concat(a);
e.map((obj) => {
  obj.first = 10;
});

console.log("a", a);
console.log("e", e);


// a [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]
// e [
//   { first: 10, second: 2, third: 3 },
//   { first: 10, second: 5, third: 6 },
//   { first: 10, second: 8, third: 9 }
// ]

2. solution

위와 같은 상황이 발생하는 이유는 우선 객체는 복사할 때 일반 문자열, 숫자, 불린 같은 경우와 다르게 다른 변수에 대입할 때 값을 복사하는 게 아니라 메모리의 주소를 복사하기 때문이다. 이를 다른 말로 참조라고 한다.
shallow copy는 가장 상위 객체만 새로 생성되고 내부 객체들은 참조 관계인 경우를 의미하고, deep copy는 내부 객체까지 모두 새로 생성된 것을 의미한다.
위의 1), 2), 3), 4) 모두 얕은 복사를 했기때문에 a 배열 안 객체의 불변성이 지켜지지 못한 것이다.

객체를 깊은 복사하기 위해서는 다음과 같이 hasOwnProperty로 부모가 아닌 본인이 가진 프로퍼티일 때 copyObj(obj[attr])와 같이 재귀적으로 일반 문자열, 숫자, 불린 값이 될 때까지 값을 불러온 뒤 최종적으로 copy = obj를 통해 복사한다.



function copyObj(obj) {
  let copy = {};
  if (Array.isArray(obj)) {
    copy = obj.slice().map((v) => {
      return copyObj(v);
    });
  } else if (typeof obj === "object" && obj !== null) {
    for (let attr in obj) {
      if (obj.hasOwnProperty(attr)) {
        console.log("attr", attr);
        console.log("obj[attr]", obj[attr]);
        // attr a
        // obj[attr] 1

        // attr b
        // obj[attr] 2

        // attr c
        // obj[attr] [ { d: null, e: 'f' } ]

        // attr d
        // obj[attr] null

        // attr e
        // obj[attr] f

        copy[attr] = copyObj(obj[attr]);
      }
    }
  } else {
    console.log("obj", obj);
    // obj 1
    // obj 2
    // obj null
    // obj f
    copy = obj;
  }
  return copy;
}

const obj = { a: 1, b: 2, c: [{ d: null, e: "f" }] };
const obj2 = copyObj(obj);
obj2.a = 3;
obj2.c[0].d = true;
console.log(obj.a); // 1
console.log(obj.c[0].d); // null

출처:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
https://www.zerocho.com/category/JavaScript/post/5750d384b73ae5152792188d

profile
바꿀 수 있는 것에 주목하자
post-custom-banner

0개의 댓글