JS 얕은 복사와 깊은 복사
변수 복사
이제, 메모리에서 어떻게 저장되는지 확인해보았으니 변수를 복사했을 때 일어나는 현상도 이해하기 수월해질 것이다.
1
2
3
4
5
6
let a = 10;
let obj1 = { c: 10, d: `ddd` };
let b = a;
let obj2 = obj1;
위 처럼 a
를 b
에, obj1
을 obj2
에 복사를 했을때 다음과 같이 이루어진다.
주소 (변수명) | … | 1001 | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|---|
데이터 | a / @5001 | obj1 / @7001 ~ | b / @5001 | obj2 / @7001 ~ | |||
주소 (데이터) | … | 5001 | 5002 | 5003 | 5004 | 5005 | … |
데이터 | 10 | ddd | |||||
주소 (참조형) | … | 7001 | 7002 | 7003 | 7004 | 7005 | … |
데이터 | c / @5001 | d / @5002 |
여기 까지는 무리 없이 이해할 거라고 생각한다.
그런데, 여기서 b
와 obj2
의 값을 변경하려고 시도한다면 어떤 일이 생길까?
1
2
b = 15;
obj2.c = 20;
주소 (변수명) | … | 1001 | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|---|
데이터 | a / @5001 | obj1 / @7001 ~ | b / @5003 | obj2 / @7001 ~ | |||
주소 (데이터) | … | 5001 | 5002 | 5003 | 5004 | 5005 | … |
데이터 | 10 | ddd | 15 | 20 | |||
주소 (참조형) | … | 7001 | 7002 | 7003 | 7004 | 7005 | … |
데이터 | c / @5004 | d / @5002 |
b
의 경우, 5001 에서 5003 으로 값이 변경되었고 a 와는 무관하다.
우리가 눈여겨 봐야할 것은, obj1
과 obj2
의 c
변수가 가지고 있는 값이 5001 에서 5004 로 변경이 되었다.
분명히 obj2
의 c
값을 변경했는데, obj1
도 영향을 받았다.
이 상태에서, 아래 명령을 입력해보자.
1
2
b === a; // false 출력.
obj2 === obj1; //true 출력.
b
는 a
와 다른 값을 가져 false 를 출력한다. 그러나, obj2
과 obj1
는 동일한 객체로 true 를 출력하고 있다.
obj2
와 obj1
을 분리하고 싶어서 복사를 하고 값을 변경한 것인데, 분명 의도하지 않은 결과가 나왔다.
이것을 해결하기 위해서, 객체의 요소를 지정해서 접근하지 말고(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`};
이렇게 하면, obj2
와 obj1
은 데이터를 변경하더라도 서로 영향이 없어진다.
주소 (변수명) | … | 1001 | 1002 | 1003 | 1004 | 1005 | … |
---|---|---|---|---|---|---|---|
데이터 | a / @5001 | obj1 / @7001 ~ | b / @5003 | obj2 / @7003 ~ | |||
주소 (데이터) | … | 5001 | 5002 | 5003 | 5004 | 5005 | … |
데이터 | 10 | ddd | 15 | 20 | aaa | ||
주소 (참조형) | … | 7001 | 7002 | 7003 | 7004 | 7005 | … |
데이터 | c / @5001 | d / @5002 | c / @5004 | d / @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
객체는 또 결국 다른 공간에 정의되어 있을 것이기 때문에, 문제가 생긴다.
중간에 내가 머리가 복잡해서 다시 테이블로 정리했다.
주소 (변수명) | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 |
---|---|---|---|---|---|---|
데이터 | user /@ 7001 ~ | user3 /@ 7004 ~ | … | |||
주소 (데이터) | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | “kim” | “80” | … | |||
주소 (참조형) | 7001 | 7002 | 7003 | 7004 | 7005 | 7006 |
데이터 | name /@5001 | age /@5002 | url /@9001 ~ | name /@5001 | age /@5002 | url /@9001 ~ |
주소 (참조형) | 8001 | 8002 | 8003 | 8004 | 8005 | 8006 |
데이터 | “https:naver.com” | “https:google.com” | … | |||
주소 (참조형) | 9001 | 9002 | 9003 | 9004 | 9005 | 9006 |
데이터 | naver /@8001 | google /@8002 | … |
결국엔, url 객체를 다시 공유하는 문제가 생긴 것이다. 주소 9002 에서 객체끼리 간섭이 생긴다.
주소 (변수명) | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 |
---|---|---|---|---|---|---|
데이터 | user /@ 7001 ~ | user3 /@ 7004 ~ | … | |||
주소 (데이터) | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | “kim” | “80” | … | |||
주소 (참조형) | 7001 | 7002 | 7003 | 7004 | 7005 | 7006 |
데이터 | name /@5001 | age /@5002 | url /@9001 ~ | name /@5001 | age /@5002 | url /@9001 ~ |
주소 (참조형) | 8001 | 8002 | 8003 | 8004 | 8005 | 8006 |
데이터 | “https:naver.com” | “https:google.com” | “https:yahoo.com” | … | ||
주소 (참조형) | 9001 | 9002 | 9003 | 9004 | 9005 | 9006 |
데이터 | naver /@8001 | google /@8003 | … |
user3
의 url
도 별도의 공간을 가져야 문제가 생기지 않을 것이다. 결국, 정말정말 멀~~~~리 돌고 돌아서 이제야 깊은 복사가 필요하다고 할 수 있게 됐다!
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 을 복사하는 과정에서 객체가 다시 선언되지 않고 주소값 복사를 해버리는 것이다.
테이블을 통해 다시 확인하고 마무리하자.
주소 (변수명) | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 |
---|---|---|---|---|---|---|
데이터 | user /@ 7001 ~ | user3 /@ 7004 ~ | … | |||
주소 (데이터) | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | “kim” | “80” | … | |||
주소 (참조형) | 7001 | 7002 | 7003 | 7004 | 7005 | 7006 |
데이터 | name /@5001 | age /@5002 | url /@9001 ~ | name /@5001 | age /@5002 | url /@9003 ~ |
주소 (참조형) | 8001 | 8002 | 8003 | 8004 | 8005 | 8006 |
데이터 | “https:naver.com” | “https:google.com” | … | |||
주소 (참조형) | 9001 | 9002 | 9003 | 9004 | 9005 | 9006 |
데이터 | naver /@8001 | google /@8002 | naver /@8001 | google /@8002 | … |
따라서, 더 이상 객체간 아무런 간섭도 일어나지 않는다는 것이다.