게시글

강좌25 - JavaScript - 3년 전 등록 / 일 년 전 수정

객체의 복사

조회수:
0

안녕하세요, 이번 시간에는 객체의 복사에 대해서 알아보겠습니다.

일반 문자열, 숫자, 불린 같은 경우는

var string = 'hello';
var copy = string;
console.log(copy); // 'hello'

하면 바로 복사가 되는데요. 객체(배열, 일반 객체, 함수)도 마찬가지이긴 합니다만, 복사된 값을 조작할 때 차이가 있습니다. 다시 위의 예를 보면

var string = 'hello';
var copy = string;
console.log(copy); // 'hello'
copy = 'hi';
console.log(string); // 'hello'

이번엔 copy 값을 hi로 바꿔봤습니다. 기존 string 값은 변화가 없겠죠? copy에 값만 복사해 줬을 뿐 더이상 연관이 없으니까요. 객체의 경우를 볼까요?

var array = ['a', 'b', 'c'];
var ref = array;
ref[0] = 'd';
console.log(array); // ['d', 'b', 'c']

ref변수에 array 배열을 대입했는데요. ref의 첫 번째 항목을 변경했더니 array의 첫 번째 항목도 같이 변경되었습니다. 이상하죠? 이게 객체의 특징입니다. 문자열, 숫자, 불린을 제외한 객체는 다른 변수에 대입할 때 을 복사하는 게 아니라 참조(메모리의 주소)를 복사합니다.

변수는 모두 메모리에 저장됩니다. 그리고 대입을 하면 변수의 이름은 저장된 메모리의 주소를 가리키게 됩니다. 메모리 어딘가에 ['a', 'b', 'c']를 저장한 게 있다면 그것을 array와 ref변수 두 개가 가리키고 있는겁니다. 값은 하나인데 변수는 여러 개일 수 있는거죠. 이걸 참조라고 부릅니다. 따라서 ref변수가 바뀌면 array 값도 같이 바뀝니다. 이걸 방지하려면 메모리에 ['a', 'b', 'c'] 두 개를 만들어서 따로따로 운영되게 만들어주어야 합니다.

var array = ['a', 'b', 'c'];
var copy = Array.prototype.slice.call(array);
copy[0] = 'd';
console.log(array); // ['a', 'b', 'c']

좀 독특한 방법인데요. 지난 시간에 배운 call 함수를 사용했습니다. copy(복사)라고 하며, 이제는 copy변수가 바뀌어도 array 변수에 영향을 미치지 않습니다. slice함수는 배열을 자르는 함수인데 어떻게 복사가 되냐고요? array.slice(0)이라고 생각하면 됩니다. array를 자르는 데 0개만큼 자르니까 결국 그대로 반환하는거죠.

위의 경우는 Array.prototype.slice.call(array) 대신 array.slice(0)을 해도 되지만, arguments 같은 것(유사배열이라서 배열의 메소드를 사용할 수 없습니다)을 복사할 때를 생각하면 Array.prototype.slice.call로 통일하는 게 좋습니다.

copy에는 두 가지가 있습니다. shallow copy(얕은 복사)와 deep copy(깊은 복사)인데요. shallow copy는 가장 상위 객체만 새로 생성되고 내부 객체들은 참조 관계인 경우를 의미합니다. deep copy는 내부 객체까지 모두 새로 생성된 것을 의미합니다. 위에 copy 변수는 내부에 객체가 없기 때문에 shallow인지 deep인지 정하기 애매합니다.

var array = [{ name: 'a' }, { name: 'b' }, { name: 'c' }];
var shallow = Array.prototype.slice.call(array);
shallow[0].name = 'd';
shallow[1] = 'e';
console.log(array); // [{ name: 'd' }, { name: 'b' }, { name: 'c' }]

위의 예제에서는 shallow[0].name은 array에 적용되지만, shallow[1]은 적용되지 않습니다. 바로 이게 얕은 복사입니다. 가장 상위 객체에 직접 변경하는 것은 적용되지 않지만 내부 객체들을 참조로 이어져있기 때문에 적용되는 겁니다. 쉽게 외우시려면 가장 바깥 껍데기만 복사되는 복사가 얕은 복사라고 생각하시면 됩니다.

배열을 깊은 복사하는 방법은 아래의 객체 깊은 복사에서 함께 다룹니다.

일반 객체를 복사하는 것에도 shallow copy와 deep copy가 있습니다. shallow는 그냥 대입만 해주면 되니까 deep copy하는 방법을를 살펴보겠습니다. 상속이 없는 일반 객체만 해당됩니다.

function copyObj(obj) {
  var copy = {};
  if (Array.isArray(obj)) {
    copy = obj.slice().map((v) => {
      return copyObj(v);
    });
  } else if (typeof obj === 'object' && obj !== null) {
    for (var attr in obj) {
      if (obj.hasOwnProperty(attr)) {
        copy[attr] = copyObj(obj[attr]);
      }
    }
  } else {
    copy = obj;
  }
  return copy;
}
var obj = { a: 1, b: 2, c: [{ d: null, e: 'f' }] };
var obj2 = copyObj(obj);
obj2.a = 3;
obj2.c[0].d = true;
console.log(obj.a); // 1
console.log(obj.c[0].d); // null

copyObj이란 함수를 새로 만들었습니다. 결과를 보면 obj과 obj2가 따로 놀죠. 여기서 for~inhasOwnProperty를 처음 보실 겁니다. for ~ in 은 obj안의 키를 순서대로 반복합니다. (주의할 점은 키가 숫자면 순서대로 반복되지 않는다는 겁니다. 숫자로된 키가 없을 때 사용하세요) 문제는 prototype에 있는 상속된 객체의 속성도 반복되기 때문에 obj.hasOwnProperty(keyName)메소드로 상속되지 않은 자기의 속성만 반복되도록 제한하는 겁니다. hasOwnProperty 부분을 빼고 복사하면, 왜 그 부분이 필요한지 이유를 알 수 있을 겁니다.

함수는 복사할 때 bind를 하면 됩니다. this를 기존 함수와 같게 하면 똑같게 함수가 복사됩니다.

var func = function () {
  alert('hi');
};
func2 = func.bind(this);
func2(); // 'hi'

이상으로 객체의 복사편을 마치겠습니다. 다음 시간에는 디자인 패턴에 대해서 알아보겠습니다!

투표로 게시글에 관해 피드백을 해주시면 많은 도움이 됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright © 2016- 무단 전재 및 재배포 금지

댓글

7개의 댓글이 있습니다.
15일 전
예전부터 봐왔는데 알고리즘(그래프)이나 이런 딥한 부분에서 오류나 설명이 부족한 부분들이 있네요. 글 쓸때 정확히 알아보고 올려주세요. 님이 올리는 정보 때문에 남들이 피해를 볼 수도 있으니까요. array.slice(0) 가 가장 빠른 딥카피 방법인데, 무슨 문제가 있는지 내용이 명확히 있으면 좋겠습니다.
15일 전
알고리즘은 고려대 수업 그대로 옮긴겁니다. 오타가 있을 순 있어도 틀린 건 거의 없습니다. 또한 slice는 쉘로카피입니다.
8시간 전
그래프 쪽에 보면 몇가지 경우의 수만 추가해서 실험해봐도 바로 틀리고 오류가 있으니까 그걸로 문제를 풀어보던 직접 한번 돌려보던 확인해 보세요. 그리고 글에 'Array.prototype.slice.call(array) 대신 array.slice(0)을 해도 되지만, arguments 같은 것(유사배열이라서 배열의 메소드를 사용할 수 없습니다)' 라고 되어있는데 아니고 배열 메소드 사용할 수 있던데요.
8시간 전
그리고 소름돋는게.. 제가 쉘로카피 딥카피를 잘못 이해했는데 그게 본인글 때문인건 아시죠? var shallow = array; 는 쉘로카피고 var deep = Array.prototype.slice.call(array); 가 딥카피라면서요. 참조 / 쉘로카피 / 딥카피에 대해 이해를 잘못 하시고 글을 쓰셨었나봐요.
7시간 전
aruguments는 배열 메서드 못 씁니다. 그래프도 그렇고 직접 틀린 점을 보여주세요. 말로만 틀렸다고 하지 마시고요. 그리고 제 글에 대한 책임은 아무도 지지 않습니다.
6시간 전
본인 글에 'slice로 복사한건 유사배열이라서 배열의 메소드를 사용할 수 없다' 라고 되어 있는데 유사 배열 아닙니다. Array.isArray 한번 해보세요. 그리고 님이 올린 다익스트라쪽 코드 참조해서 그래프 문제 풀다보면 틀린 경우의 수가 나옵니다. 대학 수업 그대로라서 틀린게 없다는 논리는 좀 아닌것 같고요... 책임 지라는건 아니지만 이정도 큰 블로그를 운영하시고 책도 쓰시는 분이시면 글 쓸때 책임감은 가지고 최소한 직접 해보고 올리셔야죠.
5시간 전
이건 제가 3년 전에 쓴 글인데 제가 왜 책임을 져야하죠? 그리고 제가 언제 slice로 복사한건 유사배열이라고 그랬죠? arguments가 유사 배열이라고 그랬죠. 글을 다시 읽으시고요. 다익스트라는 틀린 예제가 있으면 알려주시면 고칠테니까 틀린 예제를 주세요. 말로만 틀렸다고 하지 마시고요. 저는 지금까지 명확하게 안 되는 예시를 들어주시면 다 수정해드렸습니다.
한 달 전
var func = function () {
alert('hi');
};
func2 = func.bind(this);
func2(); // 'hi'

위 예제에서 func2 = func와 func2 = func.bind(this)가 무엇이 다른가요? this를 내부적으로 사용할 경우에만 둘의 차이가 있는 건가요?
한 달 전
func2 = func는 복사가 아니라 참조입니다. 즉 func의 속성으로 func.a = 'hi'를 넣으면 func2.a도 'hi'가 됩니다. (함수도 객체이기 때문이죠)
일 년 전
객체 deep copy 예제를 돌려보고 있는데요
var obj = {a: 1, b: 2} 이렇게 copy를 하면
obj2는 { a: {}, b: {} } 이렇게 나옵니다.

var obj = {a: 'aaa', b: 'bbb'} 이렇게 copy를 하면
Maximum call stack size exceeded at copyObj 에러가 나와요!

copy[attr] = copyObj(obj[attr]); 이부분을
copy[attr] = obj[attr] 로 고치니까 잘 되네요

저 부분은 왜 에러가 발생하는 거죠??
일 년 전
아 감사합니다. 코드에 에러가 있네요. 수정했습니다. copyObj를 다시 하는 것은 재귀를 사용해서 중첩된 객체까지 복사하기 위함입니다.
일 년 전
var obj2 = JSON.parse(JSON.stringify(obj));
이 코드도 깊은 복제로 검색하다가 발견했는데요 위의 예제와 차이점이 있을까요?
일 년 전
성능상 매우 안 좋습니다! 그리고 눈에 보이지 않는 prototype같은 게 날아가버릴 수 있어요.
일 년 전
아하 감사합니다!
일 년 전
얕은복사와 깊은복사는 이해했는데요..

복사와 복제는 같은 말인가요???
일 년 전
비슷한 뜻일거라 생각합니다
일 년 전
잘봤습니다. 한가지 질문이 있는데 혹시 for in문 안의 obj키가 숫자면은 어떻게 해야하나요..? 키를 다 string타입으로 바꿔서 해야하나요?
일 년 전
일단 쉬운 방법은 없어보입니다. 단순히 문자열 '3', '2', '1' 이렇게 바꿔도 순서는 항상 1, 2, 3으로 나옵니다. 사실 객체의 키가 1,2,3이라면 처음부터 배열로 하는 게 맞습니다.
2년 전
for in 문이 순서 보장되지 않는걸로 알고있는데 "for ~ in 은 obj안의 키를 순서대로 반복합니다." 이부분에서 순서대로 반복합니다 라는 내용이 오해의 소지가 있는듯 합니다~!
2년 전
for in 문은 속성의 키가 숫자가 아닌 이상은 순서를 보장하긴 합니다. 이 부분 살짝 수정해보겠습니다.