Post

JS 얕은 복사와 깊은 복사

변수 복사

이제, 메모리에서 어떻게 저장되는지 확인해보았으니 변수를 복사했을 때 일어나는 현상도 이해하기 수월해질 것이다.

1
2
3
4
5
6
let a = 10;

let obj1 = { c: 10, d: `ddd` };

let b = a;
let obj2 = obj1;

위 처럼 ab 에, obj1obj2 에 복사를 했을때 다음과 같이 이루어진다.

주소 (변수명)10011002100310041005
데이터 a / @5001obj1 / @7001 ~b / @5001obj2 / @7001 ~  
주소 (데이터)50015002500350045005
데이터 10ddd    
주소 (참조형)70017002700370047005
데이터 c / @5001d / @5002    

여기 까지는 무리 없이 이해할 거라고 생각한다.

그런데, 여기서 bobj2 의 값을 변경하려고 시도한다면 어떤 일이 생길까?

1
2
b = 15;
obj2.c = 20;
주소 (변수명)10011002100310041005
데이터 a / @5001obj1 / @7001 ~b / @5003obj2 / @7001 ~  
주소 (데이터)50015002500350045005
데이터 10ddd1520  
주소 (참조형)70017002700370047005
데이터 c / @5004d / @5002    

b 의 경우, 5001 에서 5003 으로 값이 변경되었고 a 와는 무관하다.

우리가 눈여겨 봐야할 것은, obj1obj2c 변수가 가지고 있는 값이 5001 에서 5004 로 변경이 되었다.

분명히 obj2c 값을 변경했는데, obj1 도 영향을 받았다.

이 상태에서, 아래 명령을 입력해보자.

1
2
b === a; // false 출력.
obj2 === obj1; //true 출력.

ba 와 다른 값을 가져 false 를 출력한다. 그러나, obj2obj1 는 동일한 객체로 true 를 출력하고 있다.

obj2obj1 을 분리하고 싶어서 복사를 하고 값을 변경한 것인데, 분명 의도하지 않은 결과가 나왔다.

이것을 해결하기 위해서, 객체의 요소를 지정해서 접근하지 말고(obj2.c 처럼 하지 말고) 복사한 객체를 다시 정의해보자.

1
2
3
4
5
6
7
8
let a = 10;
let obj2 = { c: 10, d = `ddd`};

let b = a;
b = 15;

let obj2 = obj1;
obj2 = { c: 20, d = `aaa`};

이렇게 하면, obj2obj1 은 데이터를 변경하더라도 서로 영향이 없어진다.

주소 (변수명)10011002100310041005
데이터 a / @5001obj1 / @7001 ~b / @5003obj2 / @7003 ~  
주소 (데이터)50015002500350045005
데이터 10ddd1520aaa 
주소 (참조형)70017002700370047005
데이터 c / @5001d / @5002c / @5004d / @5005  

실제 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let user = {
  name: "kim",
  age: "80"
};

const changeName = function (user, newName) {
  let newUser = user; // 복사

  newUser["name"] = newName;

  return newUser;
};

let user2 = changeName(user, "park");

console.log(user["name"], user2["name"]); // park park

분명, user 는 건드리지 않고 user2 를 수정했는데 user 의 이름도 바뀌었다!

그래서 아래와 같이 변경해야 문제가 생기지 않는다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user = {
  name: "kim",
  age: "80"
};

const changeName = function (user, newName) {
  return {
    user: newName
  };
};

let user2 = changeName(user, "park");

console.log(user["name"], user2["name"]); // park park

가변성을 갖기 때문에 생기는 문제점을 해결했는데, 그럼 객체의 모든 요소를 일일이 하드코딩 해야하냐?? => 그렇지 않다. iterator 로 간단히 해결!

1. 얕은 복사

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const copyObject = (obj) => {
  let newObj = {};

  for (variable in obj) {
    // for ... in 문은 Object 의 property 이름(key 값)을 불러오는 것이다. 배열에서는 사용 불가능함.
    newObj[variable] = obj[variable];
    // 다시말해 variable 은 key 값임.
  }

  return newObj;
};

const user3 = copyObject(user);

console.log(user3);

이렇게 하면, 끝…인가..? 하지만 이름이 괜히 얕은 복사겠느냐..

또, 예시를 들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let user = {
  name: "kim",
  age: "80",
  url: {
    naver: "https:naver.com",
    google: "https:google.com"
  }
};

const copyObject = (obj) => {
  let newObj = {};

  for (variable in obj) {
    // for ... in 문은 Object 의 property 이름(key 값)을 불러오는 것이다. 배열에서는 사용 불가능함.
    newObj[variable] = obj[variable];
    // 다시말해 variable 은 key 값임.
  }

  return newObj;
};

const user3 = copyObject(user);

user3[`url`][`google`] = "https:yahoo.com";

console.log(user); // google: 'https:yahoo.com'

user3 의 url 객체를 바꿨는데, 또 user 의 url 객체의 값도 같이 바뀌는 문제가 발생했다. 객체의 요소로 정의된 객체(다시 말해, 객체 내부의 객체)는 다시 주소값을 공유해버리는 문제가 생기는 것이다.

url 객체는 또 결국 다른 공간에 정의되어 있을 것이기 때문에, 문제가 생긴다.

중간에 내가 머리가 복잡해서 다시 테이블로 정리했다.

주소 (변수명)100110021003100410051006
데이터user /@ 7001 ~user3 /@ 7004 ~   
주소 (데이터)500150025003500450055006
데이터“kim”“80”   
주소 (참조형)700170027003700470057006
데이터name /@5001age /@5002url /@9001 ~name /@5001age /@5002url /@9001 ~
주소 (참조형)800180028003800480058006
데이터“https:naver.com”“https:google.com”   
주소 (참조형)900190029003900490059006
데이터naver /@8001google /@8002   

결국엔, url 객체를 다시 공유하는 문제가 생긴 것이다. 주소 9002 에서 객체끼리 간섭이 생긴다.

주소 (변수명)100110021003100410051006
데이터user /@ 7001 ~user3 /@ 7004 ~   
주소 (데이터)500150025003500450055006
데이터“kim”“80”   
주소 (참조형)700170027003700470057006
데이터name /@5001age /@5002url /@9001 ~name /@5001age /@5002url /@9001 ~
주소 (참조형)800180028003800480058006
데이터“https:naver.com”“https:google.com”“https:yahoo.com”  
주소 (참조형)900190029003900490059006
데이터naver /@8001google /@8003   

user3url 도 별도의 공간을 가져야 문제가 생기지 않을 것이다. 결국, 정말정말 멀~~~~리 돌고 돌아서 이제야 깊은 복사가 필요하다고 할 수 있게 됐다!

2. 깊은 복사

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let user = {
  name: "kim",
  age: "80",
  url: {
    naver: "https:naver.com",
    google: "https:google.com"
  }
};

const deepCopyObject = (target) => {
  let result;

  if (typeof target === "object" && target !== null) {
    // 복사하려는 대상이 빈 객체가 아니라면,
    for (variable in target) {
      // 해당 객체의 key 값을 복사하는데, 재귀 호출로 깊은 복사를 가능케 했다.
      result[variable] = deepCopyObject(target[variable]);
    }
  } else {
    result = target;
  }

  return result;
};

const user3 = deepCopyObject(user);

마지막으로, 간단히 위 깊은 복사 재귀 함수의 동작을 이해해보자.

user 가 들어오게 되면, 빈 객체가 아닌지 확인을 먼저 한다. 다음, 조건을 만족하므로 재귀호출이 시작된다.

for…in 문은 임의의 순서로 진행되는 것으로 알고 있다. 편의상 user 객체에서 정의한 순서대로 살펴보자.

첫 번째로 result["name"] = deepCopyObject(user["name"]) 이 이루어 질 것이다.

결국, user["name"] 은 객체가 아니기 때문에 result["name"] = "kim" 이 된다.

두 번째로 result["age"] = deepCopyObject(user["age"]) 가 진행된다. 이 역시도 객체가 아니기 때문에 곧바로 result["age"] = "80" 이 된다.

이제는 result["url"] = deepCopyObject(user["url"]) 이 진행된다.

그렇다면, 재귀호출이 이루어진 함수 내에서 url 이 위 두 과정 처럼 복사가 진행될 것이다.

result["naver"] = url["naver"], result["google"] = url["google"]
–> 이 result 는 재귀 호출 바깥의 result 가 아니다.

그렇게 호출이 끝나고 나면, result["url"] 까지 깊은 복사가 이루어지게 된다.

이렇게 재귀호출을 통해 깊은 복사까지 구현해보았다.

재귀호출을 사용하지 않으면, 위의 url 을 복사하는 과정에서 객체가 다시 선언되지 않고 주소값 복사를 해버리는 것이다.

테이블을 통해 다시 확인하고 마무리하자.

주소 (변수명)100110021003100410051006
데이터user /@ 7001 ~user3 /@ 7004 ~   
주소 (데이터)500150025003500450055006
데이터“kim”“80”   
주소 (참조형)700170027003700470057006
데이터name /@5001age /@5002url /@9001 ~name /@5001age /@5002url /@9003 ~
주소 (참조형)800180028003800480058006
데이터“https:naver.com”“https:google.com”   
주소 (참조형)900190029003900490059006
데이터naver /@8001google /@8002naver /@8001google /@8002 

따라서, 더 이상 객체간 아무런 간섭도 일어나지 않는다는 것이다.

This post is licensed under CC BY 4.0 by the author.