Javascript study

[Javascript] 참조에 의한 객체 복사

카누가 좋아요 2023. 7. 21. 14:44

📌 참고 사이트

참조에 의한 객체 복사 (javascript.info)

 

참조에 의한 객체 복사

 

ko.javascript.info

 

 

 

📋 원시 타입의 복사와 객체의 복사

❓ 변수와 메모리 공간

변수를 선언하는 것은 값을 저장하기 위한 특정한 메모리 공간을 확보하는 것을 말한다.

이때 값이 저장되는 메모리 주소와 식별자가 매핑되게 되는데, 변수명이 식별자 역할을 한다.

변수 자체는 데이터를 담는 공간 역할을 한다.

변수 참조 시에는 변수의 메모리 주소가 아닌 해당 주소에 저장된 데이터를 참조한다.

 

변수(Variable)와 식별자(Identifier) (tistory.com)

 

변수(Variable)와 식별자(Identifier)

변수(Variable)와 식별자(Identifier) 들어가기 전에... 사람들은 변수(Variable)와 식별자(Identifier)를 혼용하는 경우가 많습니다. "내가 혼용했었나?"라고 의문이 들면서 이해가가지 않을 수도 있습니다.

okayoon.tistory.com

 

 

📍 원시 타입의 복사

원시 타입의 값을 복사할 경우 별개의 메모리 주소에 값이 저장된다.

 

let message = "Hello";
let phrase = message;
console.log(phrase);     // Hello

 

위와 같은 경우 message와 phrase가 가리키는 메모리 주소가 다르다. 

따라서 독립된 공간에 같은 값인 "Hello"가 각각 담기게 되고, message의 값을 변경해도 phrase에 영향을 미치지 못한다.

 

message = "Hi"

console.log(message);     // Hi
console.log(phrase);     // Hello

 

 

📍 객체의 복사

객체를 복사할 경우 원시 타입에서와 다르게 변수의 주소에 객체가 그대로 저장되는 것이 아닌 객체가 저장되어 있는 메모리 주소인 객체에 대한 참조 값이 저장되게 된다.

즉, 변수 메모리 공간에 객체가 저장되어 있는 메모리 공간 주소가 저장되게 되는 것이다.

 

let user = {
  name: "John"
};

let admin = user;

console.log(admin);     // {name: 'John'}

 

위와 같은 경우 객체 {name: "John"}은 메모리 내 어딘가에 저장되고, 변수 user에는 객체를 참조할 수 있는 값 (객체가 저장되어 있는 메모리 주소)가 저장되게 된다.

따라서 user와 admin에는 똑같이 객체가 저장되어 있는 메모리 주소 값이 들어가게 되고, user에서 객체를 수정할 경우 admin에서도 수정되게 된다. admin에서 수정할 때도 마찬가지다.

 

user.name = "Bob";
console.log(user);     // {name: 'Bob'}
console.log(admin);     // {name: 'Bob'}

admin.name = "Jane";
console.log(user);     // {name: 'Jane'}
console.log(admin);     // {name: 'Jane'}

 

 

 

📋 참조에 의한 비교

➡️ 피연산자 2개 모두 객체일 경우 동등 연산자 ==일치 연산자 ===동일하게 동작한다.

➡️ 위와 같은 연산자를 이용해 비교 시 두 객체가 동일한 객체일 경우 참을 반환한다. 

즉, 같은 객체를 참조할 경우(객체의 메모리 주소가 같을 경우) 참을 반환한다.

 

let a = {};
let b = a;     // 참조에 의한 복사

// a와 b는 같은 객체를 참조한다.
console.log(a == b);     // true
console.log(a === b);     // true

 

 

let a = {};
let b = {};

// a와 b는 겉으로 보기에는 같지만 사실 독립된 객체로써 다른 메모리 주소에 저장되어 있다.
console.log(a == b);     // false
console.log(a === b);     // false

 

➡️ obj1 > obj2 와 같은 대소 비교나 obj == 5와 같은 원시값과의 비교에서는 객체가 원시형으로 변환된다.

 

 

 

📋 객체 복사, 병합과 Object.assign

📍 for문으로 객체의 프로퍼티를 복사

기존에 있던 객체와 똑같으면서 독립적인(서로 다른 메모리 주소에 저장되는) 객체를 만들고 싶다면 새로운 객체를 생성하고 기존 객체의 프로퍼티들을 모두 순회그 프로퍼티들을 새 객체에 복사시키면 된다.

 

let user = {
  name: "John",
  age: 30
};

let clone = {};     // 새로운 빈 객체

// 빈 객체에 user 프로퍼티 전부를 복사해 넣는다.
for (let key in user) {
  clone[key] = user[key];
}

// 이제 clone 객체는 완전히 독립적인 복제 객체가 되었다.
clone.name = "Pete";     
console.log(user.name);     // John (clone의 데이터를 변경해도 기존 객체에는 영향이 없다.)

 

📍 Object.assign

Object.assign을 이용하여 독립적인 복제 객체를 만드는 방법도 있다.

 

Object.assign(dest, [src1, src2, src3]);

 

➡️ Object.assign()에 첫 번째 인수로 들어가는 dest는 목표로 하는 객체이다.

➡️ 이어지는 인수 src1, ... , srcN복사하고자 하는 객체이다. 자신이 원하는 만큼 사용할 수 있다.

➡️ 객체 src1, ... , srcN의 프로퍼티dest에 복사된다.  dest 객체에 동일한 이름을 가진 프로퍼티가 있는 경우 기존 값이 덮어씌워진다.

 

let user = {name: "John"};

let permissions1 = {canView: true};
let permissions2 = {canEdit: true};

// user 객체에 {name: "Pete"} 객체의 프로퍼티와 바로 위의 두 객체의 프로퍼티들을 복사한다.
// {name: "Pete"}의 경우 key가 겹치므로 기존 값인 John에서 Pete로 값이 덮어씌워진다.
Object.assign(user, {name: "Pete"}, permissions1, permissions2);     

console.log(user);
// {name: 'Pete', canView: true, canEdit: true}

 

➡️ Object.assign을 통해 복사된 객체를 변수에 할당하는 것도 가능하다.

 

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);
console.log(clone);
// {name: 'John', age: 30}

 

 

📍 중첩 객체 복사

지금까지는 모든 프로퍼티가 원시값인 경우만 봤었는데, 프로퍼티의 값으로 객체가 있는 중첩 객체의 경우에는 독립적인 복제 객체를 어떻게 만들어야 할지 알아보자.

 

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

console.log(user.sizes.height);     // 182

 

Obejct.assign을 이용하여 객체를 복사하면 프로퍼티가 원시값일 때와 다르게 독립적인 객체가 되지 않는다.

clone.sizes = user.sizes와 같은 식으로 복제를 진행하게 되고, 객체가 복사된다. 객체는 참조에 의한 복사가 이루어지기 때문에 clone.sizes은 user.sizes와 같은 객체를 참조하게 된다.

따라서 user.sizes나 clone.sizes 중 하나에서 프로퍼티를 변경하였을 때 나머지 하나에서도 똑같이 변경이 된다.

(물론 name과 같은 원시형의 값을 가지는 프로퍼티의 경우에는 독립적이다.)

 

let clone = Object.assign({}, user);

console.log(user.sizes === clone.sizes);     // true (둘이 같은 객체이다.)

user.sizes.width++;     // user.sizes 객체에서 width 1 증가
console.log(clone.sizes.width);     // 51
// user와 clone은 sizes를 공유하므로 하나를 변경하면 다른 하나에서도 변경이 됨.

 

복사를 하되, 독립적인 객체를 만들기 위해서는 user[key]의 값을 검사하면서, 그 값이 객체인 경우 객체의 구조도 복사해주는 반복문을 사용하여 '깊은 복사'(deep copy)를 실행해야 한다.

(앞에서 계속 봐왔던 참조에 의한 복사를 하여 같은 주소를 가지게 되는 복사를 얕은 복사(shallow copy)라 한다.)

 

프로퍼티로 원시형만 존재하는 경우 Object.assign을 통해 깊은 복사(독립적인 메모리 주소를 가지는 객체를 만들기)를 해 주었는데 이는 딱 1만큼의 깊이까지만 가능하다. 즉, 객체 안에 객체가 있는 경우 Object.assign을 사용할 때 안에 있는 객체가 독립적인 객체가 되는 것이 불가능하다. (for문을 통해 프로퍼티를 일일이 복사할 때도 마찬가지)

[JavaScript] 얕은 복사(shallow copy) vs 깊은 복사(deep copy) - 하나몬 (hanamon.kr)

 

[JavaScript] 얕은 복사(shallow copy) vs 깊은 복사(deep copy) - 하나몬

💡 얕은 복사(shallow copy) vs 깊은 복사(deep copy) ❗️얕은 복사(shallow copy)란? const obj1 = { a: 1, b: 2}; const obj2 = obj1; console.log( obj1 === obj2 ); // true 위의 예시처럼 객체를 직접 대입하는 경우 참조에 의

hanamon.kr

 

깊은 복사를 할 수 있는 방법으로 다음과 같은 것들이 있다.

다음 객체를 깊은 복사 해보겠다.

 

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

 

➡️ JSON.parse(JSON.stringify(obj))

 

let deepCopy = JSON.parse(JSON.stringify(user));

user.sizes.height = 190;

console.log(deepCopy);
// {name: 'John', sizes: {height: 182, width: 50}}
// user에서 키를 변경했어도 deepCopy의 키에는 적용되지 않았다. (깊은 복사 성공!)

 

→ JSON.stringify 메서드는 JS 값이나 객체를 JSON 문자열로 변환한다.

→ JSON.parse 메서드는 JSON 문자열의 구문을 분석하고 그 결과에서 JS 값이나 객체를 생성한다.

→ but 위 방법은 프로퍼티의 값으로 new Date(...), new Set(...)과 같은 경우 복사 후에도 각각 Prototype이 Date 객체, Set 객체여야 하는데 각각 문자열로 된 날짜(string type), {} (Prototype이 Object, 즉 일반 객체에서의 중괄호)로 변환된다는 허점이 있다. 이것은 아래 structuredClone() 함수를 통해 해결할 수 있다.

 

 

➡️ 깊은 복사 표준 알고리즘 Structured cloning algorithm

Structured cloning 알고리즘은 원본 객체의 모든 프로퍼티를 거치고 각 프로퍼티들을 새로운 객체로 복제하는데, 만약 해당 프로퍼티의 값이 객체라면 모든 프로퍼티에서의 모든 객체로 된 값들이 새로운 객체로 복제될 때까지 재귀적으로 동작한다.

JS에서는 깊은 복사 기능을 가지고 있는 structuredClone() 내장 함수를 제공한다.

structuredClone()은 성능이 뛰어날 뿐만 아니라 모든 주요 브라우저에서 지원되고, JSON.parse(JSON.stringify(obj)) 에서의 버그를 해결할 수 있다는 장점이 있다.

but 함수를 포함하는 객체나 DOM 노드 등 복사 불가능한 경우도 있다.

 

let deepCopy = structuredClone(user);

user.sizes.height = 190;

console.log(deepCopy);
// {name: 'John', sizes: {height: 182, width: 50}}

 

 

➡️ JS 라이브러리 lodash의 메서드인 _.cloneDeep(obj)

위 메서드를 이용하기 위해서는 다음과 같은 방법으로 라이브러리를 설치해야 한다

 

// 브라우저에서
<script src="lodash.js"></script>

// npm 이용하여 설치하기
$ npm i -g npm
$ npm i --save lodash

// node.js에서 
// Load the full build.
var _ = require('lodash');
// Load the core build.
var _ = require('lodash/core');
// Load the FP build for immutable auto-curried iteratee-first data-last methods.
var fp = require('lodash/fp');
 
// Load method categories.
var array = require('lodash/array');
var object = require('lodash/fp/object');
 
// Cherry-pick methods for smaller browserify/rollup/webpack bundles.
var at = require('lodash/at');
var curryN = require('lodash/fp/curryN');

 

위 함수는 _.cloneDeep(value)와 같은 형식으로 value 부분에 복사하고 싶은 객체를 넣어 사용하고, 변수에 반환값을 할당해주면 그 변수는 복제되었지만 독립적인 객체를 참조하게 된다.

다른 예시를 통해 사용해 보자.

 

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

 

(번역) StructuredClone API를 사용하여 객체를 깊은 복사하는 법 (soobing.github.io)

 

(번역) StructuredClone API를 사용하여 객체를 깊은 복사하는 법

원문: https://blog.openreplay.com/deep-copying-objects-with-the-structuredclone-api/ Deep Copying Objects with the StructuredClone API 개요: 자바스크립트에서 객체를 복사하는 것은 간단하지 않으며, 이는 잘 알려진 문제입

soobing.github.io

 

 

 

📌 읽어보면 좋을 게시물

[javascript] 원시타입(primitive type) VS 참조타입(reference type)(feat. stack과 heap 영역) (velog.io)

 

[javascript] 원시타입(primitive type) VS 참조타입(reference type)(feat. stack과 heap 영역)

자바스크립트에는 원시 타입과 참조 타입 2가지 자료형이 있다. 타입을 알아보기 전에 메모리 구조에 대해 짧게 얘기하고 넘어가야 이해가 더 쉽다. JS 메모리 주소 공간 프로그램이 실행될 때

velog.io