본문 바로가기

내직업은 IT종사자/javascript

[javascript] 변수 참조타입 메모리 접근 방식, 참조타입 복사가 필요한 이유 (shallow copy, deep copy)

반응형

 

 목차

 

들어가기전에!  자바스크립트 엔진은 가상머신으로 돌아가는데 버츄얼 머신 메모리 모델에는 힙과 콜스택 영역으로 나눠져 있습니다.  (힙: 참조타입, 콜스택: 원시타입)

 

 

 

1. 원시값, 참조값 이란? 그리고 복사가 필요한 이유


1.1 원시값이란?

원시값은 기본자료형을 의미합니다. 예: Number, String, Boolean, Null, undefined

변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장됩니다. 

 

1.2 원시값은 어떻게 작동하는가?

자바스크립트에서 원시타입을 선언(declare)하면 이는 stack에 저장됩니다.  

 

* stack이란 LIFO(last in first out: 나중에들어온 순서대로 나간다) 구조를 가진 자료구조 입니다. 저장된 원시타입은 식별자 를 통해서 접근할 수 있고 원시 데이터와 함께 스택에 저장됩니다. 

 

[아래 사진]
자바스크립트 원시타입은 변수에 할당될때, 메모리의 고정 크기로 원시값을 저장하고 해당 저장된 값을 변수가 직접적으로 가르키는 형태를 띕니다.
또한 값이 절때 변하지 않는 불변성을 갖고 있기 때문에 재할당 시 메모리에 재할당 값이 저장되고 변수가 가리키는 메모리가 달라집니다.  여기서 이제 아무대도 쓰이지 않는 메모리는 가비지 컬렉터가 정리하게 됩니다. 

let a = 100;
let b = a;
a = 50;

console.log(a); // 50;
console.log(b); // 100;

let a = 100은 오른쪽 그림과 같이 처음에 메모리영역 1에 저장됩니다.  그다음 let b = a 는 메모리 영역2에 저장됩니다.

a = 50은 위에 설명과 같이 원시타입 불변성 으로 새로운값을 재할당 하면 새로운 메모리영역인 메모리영역3에 저장됩니다.  그렇기 때문에 b 변수의 값은 영향을 받지 않습니다.  그리고 메모리영역 1은 사용이 불필요해 졋으므로 가비지 컬렉터가 정리합니다.

 

1.3 참조값이란??

자바스크립트에서 원시타입을 제외한 나머지는 참조타입(객체(Object)라 할수 있습니다.

예를들어 array, function,object등등 ..

원시타입과 가장 큰 차이점은 변수의 크기가 동적으로 변한다는 것 입니다.

이러한 특징때문에 Object의 데이터 자체는 별도의 메모리공간(heap)에 저장되며, 변수에 할당시 데이터에 대한 주소(heap메모리의 주소값)이 저장되므로 자바스크립트 엔진이 변수가 가지고 있는 메모리 주소를 이용해서 변수의 값에 접근을 합니다. 

const object1 = {
	name: 'test',
    age: 1
}

const object2 = object1;

object1.age = 50;

console.log(object2.age); // 50;

원시값이라면 object1에 대한 업데이트는 object2에 영향이 없을 텐데 

위와 같은 경우는 힙 영역에 새로운 영역이 추가되지 않습니다.

콜스택에는 따로 쌓여있지만 같은 heap 메모리 주소를 바라보고 있기 때문입니다.

 

https://hwani.dev/static/ba9d9540aee98fbf8c8aec22c4d021e1/e31cc/e42af7e2.png

 

1.4 참조타입 변수 사용시 복사가 필요한 이유

참조 타입의 변수는 실제 데이터가 저장된 주소를 참조하기에 참조(reference)타입이라고 불리는 것입니다.

그렇기에 참조타입은 서로에게 영향을 줄 수 있으므로 변수의 복사나 수정시 참조 여부를 잘 고려해야합니다.

그렇기 때문에 복사가 필요할 수 있는 것입니다.

 

 

 

2. 얕은 복사 정의 및 방법  (Shallow Copy)


얕은 복사란? 객체를 복사 할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말합니다. 
객체 안에 객체가 있을 경우 한개의 객체라도 기존 변수의 객체를 참고하고 있다면 이를 얕은 복사라고 합니다. 
얕은 복사 후 해당 변수를 재사용하여 수정한다면 원본 값이 동시에 변하므로 주의가 필요합니다.

 

그럼 얕은 복사를 하는 방법에 대해서 알아봅시다~

 

2.1 일반적인 복사

let original = {name: 'joy'}
let copyOriginal = original;

copyOriginal.name = 'whoareu';

console.log(original.name); // whoareu;
console.log(original === copyOriginal);

 

2.2 Object.assign()

object.assign()을 이용하면 객체 자체는 깊은 복사가 수행되지만, 2차원 이상의 객체는 얕은 복사가 수행됩니다. 

아래 코드와 같이 객체 origin, copy = {} 는 콜스택 내부에 서로 다른 위치에 저장되어있지만  객체의 내부에 위치한 오브젝트는 같은 heap 주소를 참조하고 있기 때문입니다. 

 

/* Object.assign(생성할 객체, 복사할 객체) */
let origin = {
    a: 1,
    b: { c: 2 }
};

//복사진행
let copy = Object.assign({}, origin);

//1차원은 깊은 복사가 수행됨
copy.a = 3;
console.log(origin.a); //a

//2차원 이상은 얕은 복사가 수행됨
copy.b.c = 3
console.log(origin.b.c); //3

console.log(origin === copy) // false
console.log(origin.b.c === copy.b.c) // true

 

2.3 전개구문 {...}

전개구문도 Object.assign()과 마찬가지로 let origin = {} 객체 자체 1단계는 깊은 복사 이지만 객체 내부의 객체는 위와 같이 같은 힙 주소를 참조하고 있기 때문에 얕은 복사가 진행됩니다.

let origin = {
    a: 1,
    b: { c: 2 }
};

let copy = {...origin}

//깊은 복사 결과
copy.a = 4;
console.log(origin.a); // 1;

//얕은 복사 결과 
copy.b.c = 3
console.log(origin.b.c); //3

console.log(origin === copy) // false
console.log(origin.b.c === copy.b.c) // true

 

 

 

 

 

3. 깊은 복사 (Deep copy)


3.1 재귀 함수를 통한 깊은 복사

const origin = {
  name : 'test',
  age : 30,
  address : {
    city : "jeju"
  }
}

const deepCopy = (obj) => {
  if(typeof obj !== "object" || obj === null){
    return obj;
  }
  
  const deepCopyObj = {};
  
  for(let key in obj){
    deepCopyObj[key] = deepCopy(obj[key]);
  }
  
  return deepCopyObj;
}

const copy  = deepCopy(origin);
copy.address.city = 'seoul';


console.log(origin === copy);  //false
console.log(origin.address.city === copy.address.city); //false

 

3.2 JSON.parse()와 JSON.stringify()를 이용한 깊은 복사

let origin = {
    a: 1,
    b: { c: 2 }
};

let copy = JSON.parse(JSON.stringify(origin));
copy.b.c = 3

console.log(origin.b.c); // 2
console.log(origin.b.c === copy.b.c) //false

 

 

 

 

 

참고: https://velog.io/@nomadhash/Java-Script-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC, https://hwani.dev/js-primitive-reference-types/

 

 

잘못된 정보에 대한 피드백은 언제나 환영입니다  (´▽`ʃƪ)♡

반응형