iTerm2, Vim, Zsh의 Syntax highlight를 비롯한 플러그인 및 테마 설정하기 (for Mac OS)
Javascript #6 객체의 얕은 복사와 깊은 복사에 대해 알아봅니다.
2019-01-23
Explanation
오늘은 자바스크립트 객체의 복사에 관하여 알아 보려 합니다. 자바스크립트의 객체 복사에는 얕은 복사와 깊은 복사로 나뉘는데요. 먼저 얕은 복사를 하는 방법?? 에 대해 알아봅니다.
얕은 복사와 깊은 복사의 차이는 복사하고자 하는 객체의 속성의 값으로 객체가 있을때, 그 내부 객체의 값에 대하여 참조 값을 복사하느냐 또는 그 값을 복사하느냐의 차이를 가지고 있는데요.
이렇게 적고 보니 더 헷갈릴 수 있을 거 같네요..
간단하게 예로 들어서 첫번째로는 직접 함수를 만들어서 얕은 복사를 해봅니다.
1-1. 함수를 만들어서 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function shallowCopy(value) { const result = {}; for(const key in value) { result[key] = value[key]; } return result; } const testData = {a: 1, b: 2}; const copyData = shallowCopy(testData); console.log(copyData); // {a: 1, b: 2} |
다음은 ES6에 추가된 Object.assign() 메서드를 사용하는 방법입니다.
Object.assign() 메서드는 객체를 병합하거나 복사할때 사용하는데요.
1-2. Object.assign() 메서드 사용
1 2 3 4 5 |
const testData = {a: 1, b: 2}; const copyData = Object.assign({}, testData); console.log(copyData); // {a: 1, b: 2} |
그리고 마지막으로 전개 연산자(펼침 연산자)를 사용하는 방법입니다. 역시 ES6 스펙에 정의되었습니다.
1-3. 전개 연산자 사용
1 2 3 4 5 |
const testData = {a: 1, b: 2}; const copyData = { ...testData }; console.log(copyData); // {a: 1, b: 2} |
대략 이러한 방법들이 있는거 같아요.
그런데 아까 이야기 했듯이 복사하고자 하는 객체가 객체를 담고 있으면 참조 값을 복사하기 때문에 ‘복사 대상 객체’의 값이 바뀌면 ‘복사한 객체’의 값이 바뀌어요. 간단히 예를 들면 아래와 같아요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function shallowCopy(value) { const result = {}; for(const key in value) { result[key] = value[key]; } return result; } const testData = {a: {aa: 1}, b: 2}; const copyData = shallowCopy(testData); testData.a.aa = 2; console.log(copyData); // {a: {aa: 2}, b: 2} |
1 2 3 4 5 6 |
const testData = {a: {aa: 1}, b: 2}; const copyData2 = Object.assign({}, testData); testData.a.aa = 2; console.log(copyData2); // {a: {aa: 2}, b: 2} |
1 2 3 4 5 6 |
const testData = {a: {aa: 1}, b: 2}; const copyData = { ...testData }; testData.a.aa = 2; console.log(copyData); // {a: {aa: 2}, b: 2} |
앞서 보았듯이 깊은 복사는 객체 안의 객체의 참조 값도 완전히 원시 데이터 값으로 복사하는 방법이겠죠?
우선 첫번째로 가장 간단하게 사용할 수 있는 방법은 JSON 객체를 사용하거나, JSON 객체와 eval() 메서드를 사용하는 방법이에요.
2-1. JSON 객체 사용 ( + eval() 메서드 사용)
1 2 3 4 5 6 |
const testData = {a: {aa: 1}, b: 2}; const copyData = JSON.parse(JSON.stringify(testData)); testData.a.aa = 2; console.log(copyData); // {a: {aa: 1}, b: 2} |
1 2 3 4 5 6 |
const testData = {a: {aa: 1}, b: 2}; const copyData = eval('('+JSON.stringify(testData)+')'); testData.a.aa = 2; console.log(copyData); // {a: {aa: 1}, b: 2} |
JSON.stringify() 메서드로 객체의 데이터를 JSON 형태의 문자열로(원시 데이터)로 만들어서 복사한 후 JSON.parse() 또는 eval() 메서드를 사용해서 다시 객체로 만드는 방법입니다.
비숫해 보이는 이 둘을 함께 적은 건 ‘뭐가 더 빠를까?’ 하는 생각이 들어서… 간단하게 실험을 해봤는데요.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function test() { const aa = {}; for(let i=0; i<10000; i++) { aa['test'+i] = i*100; } const bb = JSON.stringify(aa); console.time(); const cc = JSON.parse(bb); console.timeEnd(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function test2() { const aa = {}; for(let i=0; i<10000; i++) { aa['test'+i] = i*100; } const bb = JSON.stringify(aa); console.time(); const cc = eval('('+bb+')'); console.timeEnd(); } |
저는 크롬 v71.0.3578 과 Node v8.11.2 에서 두개의 함수를 만들어서 실행해 봤는데요. 크롬에서는 JSON.parse() 가 약 4s, 그리고 eval()는 약 8s로 JSON.parse()가 훨씬 빠르게 나왔어요. 그리고 노드에서는 JSON.parse()의 경우 2.2s, 3.6s, 3.6s, 3.6s, 2.1s … 정도의 속도가 그리고 eval()은 6.6s, 5.7s, 3.3s, 0.2s, 0.2s…로 처음엔 느리고 함수를 실행할 수록 점점 더 빨라졌어요.
왜지…? 하지만 점점 시간이 너무 늦어지고 있는 관계로…
여하튼 급하게 마무리를 지어보자면, 데이터나 브라우저에 따라서 조금씩 차이는 있겠지만 eval() 보단 JSON.parse() 쓰는게 좋을거 같아요. (MDN에서도 eval() 함수 사용의 위험에 대해 언급하는 것도 보이고요.)
약간 뜬금없이 JSON.parse() 와 eval() 을 비교했는데요. 다시 본론으로 돌아가면 JSON 객체를 사용하는 법은 간편하지만 우선 성능적으로 좋지 못하고, 그리고 복사하는 객체의 속성 중 값이 함수이거나 undefined 인 경우 또 완전한 복사가 되지 않는다는 문제가 있답니다. 예를 들자면 아래와 같습니다.
1 2 3 4 |
const testData = {a: (a, b) => a + b, c: undefined}; const copyData = JSON.parse(JSON.stringify(testData)); console.log(copyData); // {} |
다음으로 깊은 복사를 할 수 있는 함수를 만들어서 사용하는 방법입니다. 얕은 복사에서 사용한 함수와 거의 비슷하지만 객체의 속성의 값이 객체일때는 재귀를 사용하는 정도가 추가 됩니다.
2-2. 함수를 만들어서 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function deepCopy(value) { const result = {}; for(const key in value) { if(typeof value[key] === 'object') { result[key] = deepCopy(value[key]); } else { result[key] = value[key]; } } return result; } const testData = {a: {aa: 1}, b: 2}; const copyData = deepCopy(testData); testData.a.aa = 2; console.log(copyData); // {a: 1, b: 2} |
이렇게 함수를 만들어 사용하면 객체 속성의 값이 함수이거나 undefined여도 복사가 가능합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function deepCopy(value) { const result = {}; for(const key in value) { if(typeof value[key] === 'object') { result[key] = deepCopy(value[key]); } else { result[key] = value[key]; } } return result; } const testData = {a: (a, b) => a+b, b: undefined}; const copyData = deepCopy(testData); console.log(copyData); // {a: (a, b) => a+b, b: undefined} |
그리고 간단한 성능테스트
1 2 3 4 5 6 7 8 9 10 11 |
function test() { const aa = {}; for(let i=0; i<10000; i++) { aa['test'+i] = i*100; } console.time(); const bb = JSON.parse(JSON.stringify(aa)); console.timeEnd(); } |
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 |
function test2() { const aa = {}; for(let i=0; i<10000; i++) { aa['test'+i] = i*100; } const deepCopy = value => { const result = {}; for(const key in value) { if(typeof value[key] === 'object') { result[key] = deepCopy(value[key]); } else { result[key] = value[key]; } } return result; }; console.time(); const bb = deepCopy(aa); console.timeEnd(); } |
역시 동일하게 크롬 v71.0.3578 과 Node v8.11.2 에서 두개의 함수를 만들어서 실행해 보았는데요. 크롬에서 JSON 객체를 사용할 경우 7.7s, 7.1s, 9.6s, 6s, 7.3s, 6.5s… deepCopy 함수를 만들어 사용할 경우 4.1s, 4.2s, 2.6s, 2.5s … 그리고 동일하게 노드에서는 JSON 객체는 4.5s, 4.5s, 4.6s, 4.4s, 4s … deepCopy 함수는 2.9s, 3.4s, 3.8s, 2.5s, 2.1s …
원래 처음 생각은 Lodash의 _.cloneDeep의 코드를 한번 훑어보는 것까지 적으려고 했으나.. 시간이 너무 늦어서 Lodash의 관한 부분은 다음에 따로 한번에 포스팅해야 겠어요.
1. https://stackoverflow.com/questions/1843343/json-parse-vs-eval
2. https://mygumi.tistory.com/322
3. https://hyunseob.github.io/2016/02/08/copy-object-in-javascript/
4. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
5. http://hochulshin.com/javascript-best-deepcopy/