원시값과 참조값, 얕은 복사와 깊은 복사

kim unknown·2022년 9월 8일
1

JavaScript

목록 보기
22/22
post-thumbnail

1. 원시값과 참조값

자바스크립트에서의 데이터 타입은 원시값과 참조값으로 구분한다.

  • 원시 타입(Primitive Type): String, Number, Boolean, Null, Undefined, Symbol
  • 객체 타입(Reference Type): Array, Object, Function

원시값은 읽기 전용으로 변경 불가능한 값이다. 원시 값을 할당한 변수는 원시 값 자체를 값으로 갖는다.

여기서 변경 불가능한 값은 변수에 재할당이 불가능하단 의미가 아니라, 메모리에 올라간 값이 변경되지 않는다는 의미이다. 원시값이 변수에 저장될 때에는 메모리 공간이 확보되어 그 공간에 값이 저장되고 변수는 그 주소를 참조한다. 재할당도 마찬가지로 새로운 메모리 공간을 확보하여 값을 저장하고 변수가 참조하던 기존 주소를 새로 할당한 값의 주소로 변경하는 것이다. 즉, 값이 직접 변경되는 것이 아닌 참조하는 메모리 공간의 주소가 변경되는 것이다.

참조값은 변경 가능한 값이다. 참조 값을 변수에 할당하면 변수에는 참조 값이 저장된다. 실제 객체의 값은 별도의 메모리 공간에 저장되며 변수는 그 공간을 참조하는 주소를 값으로 갖는다. 즉, 참조값은 생성된 객체가 저장된 메모리 공간의 주소를 의미한다. 참조값은 재할당 없이 직접적으로 객체에 접근하여 프로퍼티를 동적으로 추가, 갱신, 삭제가 가능하다. 이것이 변경 가능한 값의 의미이다.

원시 타입과 참조 타입은 값을 복사할 때에도 큰 차이가 있다. 원시 타입은 값이 복사되어 전달되며, 이것을 값에 의한 전달이라고 한다. 반면, 참조 타입은 실제 값이 아닌 참조값이 전달되며, 이것을 참조에 의한 전달이라고 한다.

요약

원시값은 변경 불가능한 값으로 원시값이 변수에 저장될 때에는 메모리 공간이 확보되어 그 공간에 값이 저장되고 변수는 그 주소를 참조한다. 재할당을 하게되면 값이 직접 변경되는 것이 아닌 참조하는 메모리 공간의 주소가 변경된다.

참조값은 변경 가능한 값으로 참조 값을 변수에 할당하면 실제 객체의 값은 별도의 메모리 공간에 저장되며 변수는 그 공간을 참조하는 주소를 값으로 갖는다. 참조값은 직접적으로 객체에 접근하여 추가, 갱신, 삭제가 가능하다.


2. 얕은 복사와 깊은 복사

원시 타입의 값은 새로운 메모리 공간에 독립적인 값을 저장하기 때문에 깊은 복사가 되고 참조 타입값은 얕은 복사가 된다.

즉, 얕은 복사는 '메모리 주소 값'을 복사한 것이다. 반대로 깊은 복사는 새로운 메모리 공간을 확보해 완전히 복사하는 것이다.

2-1. 얕은 복사

얕은 복사(Shallow Copy)란 객체를 복사할 때 원본 값과 복사된 값이 같은 참조(=메모리 주소)를 가리키고 있는 것을 말한다. 객체 안에 객체가 있을 경우 한 개의 객체라도 원본 객체를 참조하고 있다면 이를 얕은 복사라고 한다.

아래와 같이 일반적인 복사의 경우 참조에 의한 할당이 이루어지므로 둘은 같은 주소를 참조하게 된다. 즉, 똑같은 데이터가 하나 더 생성되는 것이 아니라, 해당 데이터의 메모리 주소를 복사함으로써 한 데이터를 같이 공유하는 것이다. 이와 같은 경우를 얕은 복사라고 한다.

얕은 복사의 경우 서로 같은 참조를 가리키기 때문에 복사 후에 객체를 수정하면 기존 객체에도 영향을 끼친다.

const obj1 = { a: "a", b: "a", c: "c"};
const obj2 = obj1;

// 원본 객체(obj1)와 복사 객체(obj2)가 서로 같은 참조를 가리킴
console.log( obj1 === obj2 );  // true

obj2.a = "apple";  // 복사한 객체 수정

// 원본 객체에도 영향을 미침 둘 다 값이 바뀜
console.log(obj1);  // { a: "apple", b: "a", c: "c"}
console.log(obj2);  // { a: "apple", b: "a", c: "c"}

Object.assign(), spread 연산자{...obj}는 객체 자체는 깊은 복사가 수행되지만, 2차원 이상의 객체는 얕은 복사가 수행된다. 아래와 같이 객체 자체는 서로 다른 주소를 참조하고 있는 깊은 복사가 이루어지지만 내부의 객체는 같은 주소를 참조한다.

let origin = {
    a: "a",
    b: { c: "c" }
};

// Object.assign()을 활용한 복사
let copyAssign = Object.assign({}, origin);
// 전개 구문을 활용한 복사
let copySpread = {...origin}

// 복사한 객체 수정
copyAssign.b.c = "cat"
copySpread.b.c = "cat"

// 객체 자체는 깊은 복사가 되어 서로 다른 주소를 참조함
console.log(origin === copyAssign) // false
console.log(origin === copySpread) // false
// 하지만 2차원 이상의 객체는 얕은 복사가 수행되어 같은 주소를 참조함
console.log(origin.b.c === copyAssign.b.c) // true
console.log(origin.b.c === copySpread.b.c) // true

2-2. 깊은 복사

깊은 복사(Deep Copy)는 복사된 객체가 다른 주소를 참조하며 내부의 값만 복사된다. 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어졌다면 이를 깊은 복사라고 한다.

JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어진다. 객체를 json 문자열로 변환 후, JSON.parse()를 이용해 다시 원래 객체(자바스크립트 객체)로 만들어준다.

const obj1 = {
  a: "a",
  b: {
    c: "c",
  },
};

const obj2 = JSON.parse(JSON.stringify(obj1));

obj2.b.c = "cat";

console.log(obj1); // { a: "a", b: { c: "c" } }
console.log(obj1.b.c === obj2.b.c); // false

커스텀 재귀 함수를 구현하면 깊은 복사를 할 수 있다.

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

function isCopyObj(origin) {
    let res = {};

    for (let key in origin) {
      if (typeof origin[key] === 'object') {
          res[key] = isCopyObj(obj[key]);
      } else {
          res[key] = origin[key];
      }
    }

    return res;
}

let copy = isCopyObj(origin);

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

요약

얕은 복사(Shallow Copy)란 객체를 복사할 때 원본 값과 복사된 값이 같은 참조(=메모리 주소)를 가리키고 있는 것을 말한다. 객체 안에 객체가 있을 경우 한 개의 객체라도 원본 객체를 참조하고 있다면 이를 얕은 복사라고 한다.

깊은 복사(Deep Copy)는 복사된 객체가 다른 주소를 참조하며 내부의 값만 복사된다. 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어졌다면 이를 깊은 복사라고 한다.


참고 자료
📝 [JavaScript] - 원시 값과 참조 값
📝 [JS] 참조 타입의 얕은 복사와 깊은 복사(Shallow Copy & Deep copy)

0개의 댓글