자바스크립트가 제공하는 7가지 데이터 타입(숫자, 문자열, 불리안, null, undefined, 심벌, 객체 타입)은 크게 원시 타입(Primitive type)과 객체 타입(object/reference type)으로 구분된다.

 

원시타입과 객체 타입의 차이


  1. 원시값은 변경 불가능한 값이지만, 객체 타입(참조값)은 변경 가능한 값이다.
  2. 원시 값을 변수에 할당하면 확보된 메모리 공간에는 실제 값이 저장된다. 하지만 객체를 변수에 할당하면 확보된 메모리 공간에는 참조값이 저장된다.
  3. 원시값을 갖는 변수를 다른 변수에 할당하면 원본의 원시값이 복사되어 전달된다. 하지만 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조값이 복사되어 전달된다.

 

원시 타입(원시값)


원시 데이터 : String, Number, Boolean, undefined, null

원시값은 변경 불가능한 값으로 읽기 전용이다.

여기서 변경 불가능한 값이라는 것은 변수가 아니라 값에 대한 진술로 원시값이 변수에 저장될 때에는 메모리 공간이 확보되어 그 공간에 값이 저장되고 변수는 그 주소를 참조한다.

여기서 원시값을 할당한 변수에 새로운 원시 값을 재할당하면 변수(확보된 메모리 공간)에 저장된 원시 값을 변경하는 것이 아니라 새로운 메모리 공간을 확보하고 재할당한 원시값을 저장, 그리고 변수가 참조하던 메모리 공간의 주소가 바뀌는 방식이다.

이러한 특성을 불변성(immutability)이라고 한다.

즉, 원시값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 없다.

// 데이터 불변성(Immutability)
let a=1
let b=4
console.log(a,b, a===b) // 1 4 false
//a 와 b의 메모리 주소가 서로 다르며 값도 다름

b=a
console.log(a,b, a===b) // 1 1 true
//b에 a의 메모리 주소를 b에 할당하였으므로 주소와 값이 모두 동일함

a=7
console.log(a,b, a===b) // 7 1 false
//a에 재할당이 되어 메모리 주소의 값이 변하였으므로 a와 b의 주소와 값이 모두 다름

let c=1
console.log(b,c, b===c) // 1 1 true
// 앞전에 값 1이 저장되어 있던 메모리 공간의 주소가 c에 할당이 되면서 b와 c의 값과 주소가 모두 동일함.
// 즉, 새로운 변수에 기존에 한번 메모리에 저장된 원시값이 있다면 새로운 메모리 저장소를 생성하는 것이 아닌 그 주소를 할당 받는다.
// 이러한 특성이 원시값의 불변성이다.

 

객체 타입(참조값)


참조형 데이터 : Object, Array, Function

객체 타입은 원시값과 다르게 변경이 가능하다.

객체는 프로퍼티를 동적으로 추가,삭제 등 변경이 가능하며, 메모리 공간또한 사전에 정해 두지 않는다.

객체를 변수에 할당하면 실제 객체의 값은 별도의 메모리 공간에 저장되며 그 공간을 참조하는 주소를 변수는 값으로 갖는다.

즉, 참조값은 생성된 객체가 저장된 메모리 공간의 주소를 의미한다.

따라서 객체를 할당한 변수를 다른 변수에 할당하면 객체값이 전달되는 것이 아닌 메모리 공간의 주소를 값을 복사하여 전달한다.

//객체 타입
let x = {k: 1}
let y = {k: 1}
console.log(x,y, x===y) // {k: 1} {k: 1} false 값은 같지만 메모리 주소(참조값)가 다름.
x.k=7
y=x
console.log(x,y, x===y) // {k: 7} {k: 7} true 로 y는 x의 같은 메모리 주소(참조값)을 복사하여 전달받았기 때문에 값과 메모리 주소가 같음.
x.k=2
console.log(x,y, x===y) // {k: 2} {k: 2} true 로 같은 메모리 공간을 서로 참조하고 있으므로 객체의 프로퍼티가 갱신 되었을 때 공유되어 true값이 나옴
let z= y
console.log(x,y, x===y) // {k: 2} {k: 2} {k: 2} true 로 같은 메모리 공간에 대한 주소를 x,y가 가지고 있으며 이 값을 z가 전달 받았으므로 모두 같은 메모리 주소를 참조하고 있음.
x.k=9
console.log(x,y, x===y) // {k: 9} {k: 9} {k: 9} true 같은 메모리 공간을 참조하는 x,y,z에서 값을 변경하였을 때 그 영향을 다른 변수도 받게되어 모두 갱신됨.

이러한 특성으로 인해 객체를 복사(참조값이 아닌 실제 데이터)하기 위해서는 얕은 복사와 깊은 복사를 하여야한다.

즉, 원시타입은 재할당을 통해서만 변수에 저장한 값을 변경할 수 있지만 객체타입은 재할당없이 객체를 직접 동적으로 추가, 삭제 갱신 등이 가능하다는 것이 제일 큰 차이점이다.

 

얕은 복사와 깊은 복사


  • 얕은 복사(Shallow copy)

얕은 복사는 객체를 프로퍼티 값으로 갖는 객체의 경우에 한 단계 까지만 복사하는 것을 말한다.

즉 첫 객체는 새로운 메모리 주소를 가지지만 내부에 있는 중첩된 객체는 얕은 복사를 한 경우에 같은 메모리 주소를 가진다는 의미가 된다.

  • 얕은 복사 방법
    1. Object.assign()
      • const user = {
            name : 'Lee',
            age : 25,
            email : 'dankthedust@gmail.com'
        }
        
        //const copyUser = user; 같은 메모리 공간의 주소가 복사되어 같은 메모리 공간에 저장된 값을 공유하게 되어 서로 영향을 주게 됨.
        //console.log(copyUser === user); true
        let copyUser = Object.assign({}, user) // 새로운 빈 객체를 생성하고 병합하여 copyUser에게 할당
        console.log(copyUser === user); //false
    2. 전개연산자(spread)
      • const user = {
            name : 'Lee',
            age : 25,
            email : 'dankthedust@gmail.com'
        }
        
        //const copyUser = user; 같은 메모리 공간의 주소가 복사되어 같은 메모리 공간에 저장된 값을 공유하게 되어 서로 영향을 주게 됨.
        //console.log(copyUser === user); true
        let copyUser = {...user};
        console.log(copyUser === user); //false

얕은 복사의 경우 하위 중첩되어 있는 객체까지 복사하지 않으므로 공유에 의한 예기치 못한 결과를 얻을 수 있다.

const user = {
    name : ['Lee'],
    age : 25,
    email : 'dankthedust@gmail.com'
}

//1. Object.assign()을 이용한 얕은 복사
let copyUser = Object.assign({}, user)

//2. 전개 연산자(spread)를 이용한 얕은 복사
let copyUser = {...user};
console.log(copyUser === user);

user.name.push('Kim');
console.log(user.name === copyUser.name) // true 인 이유는 user내의 name또한 배열로 참조형 데이터 이다.
// user의 name을 복사 된것이 아니기 때문에 user내의 name은 동일한 주소를 공유하고 있다.

이러한 이유로 깊은 복사가 필요하다.

 

  • 깊은 복사(Deep copy)

깊은 복사는 하위에 중첩되어 있는 객체(배열 등)까지 모두 복사하는 것을 말하며,

즉, 원시 값처럼 완전히 새로운 메모리 공간을 차지하는 원시값처럼 복사본을 만든다.

  • 깊은 복사 방법
    1. JSON.stringify()
      • 객체를 문자열로 변환하여 복사하면 문자열은 원시값이므로 깊은 복사처럼 활용하는 것이 가능하다.
      • const user = {
            name : ['Lee'],
            age : 25,
            email : ['dankthedust@gmail.com']
        .
        .
        .
        
        var copyUser = JSON.parse(JSON.stringify(user));
        user.email.push('newEmail@gmail.com');
        console.log(user.email === copyUser.email); // false
        하지만 단점으로 느리다는 점과 JSON.stringify() 메소드는 함수를 만났을 때 undefined로 처리한다는 점이다.
    2. 재귀함수를 이용한 복사
      • 순수 javascript로 깊은 복사를 하기 위해선 재귀함수를 이용하는 방법이 있다.
      • 이는 첫 객체부터 내부 중첩된 객체까지 재귀 함수를 통해서 반복문을 통해서 재귀를 호출하여 복사된 값을 반환한다.
        const user = {
            name : ['Lee'],
            age : 25,
            email : ['dankthedust@gmail.com']
        .
        .
        .
        
        function deepCopy(obj) {
            if (obj === null || typeof obj !== "object") {
                return obj;
            }
        
            let copy = {};
            for (let key in obj) {
                copy[key] = deepCopy(obj[key]);
            }
            return copy;
        }
        
        var copyUser = deepCopy(user);
        user.email.push('newEmail@gmail.com');
        console.log(user.email === copyUser.email);
        console.log(user)
        console.log(copyUser)
      • 다만 let copy = 부분으로 인하여 배열 또는 객체로 통일되어 값이 반환된다는 단점이 있다.
      • 현재 예제의 경우 객체 user내에 배열이 존재하는데, let copy = []로 재귀함수를 작성할 경우
        • //user
          {name: Array(2), age: 25, email: Array(2)}
          age: 25
          email: (2) ['dankthedust@gmail.com', 'newEmail@gmail.com']
          name: (2) ['Lee', 'Kim']
          [[Prototype]]: Object
          
          //copyUser
          [name: Array(2), age: 25, email: Array(1)] // < 배열로 반환
          age: 25
          email: ['dankthedust@gmail.com'] // < 배열로 반환
          name: (2) ['Lee', 'Kim'] // 배열로 반환
          length: 0
          [[Prototype]]: Array(0)
      • let copy = {} 인 경우,
        • //user
          {name: Array(2), age: 25, email: Array(2)}
          age: 25
          email: (2) ['dankthedust@gmail.com', 'newEmail@gmail.com']
          name: (2) ['Lee', 'Kim']
          [[Prototype]]: Object
          
          //copyUser
          {name: {…}, age: 25, email: {…}}
          age: 25
          email: {0: 'dankthedust@gmail.com'} // < 객체로 반환
          name: {0: 'Lee', 1: 'Kim'} // < 객체로 반환
          [[Prototype]]: Object
      • 위와 같이 재귀함수이기 때문에 발생하는 문제가 있다.
    3. lodash의 cloneDeep을 이용한 복사
      • 모듈을 이용한 가장 간편한 깊은 복사가 가능하다.
      • lodash의 공식 문서에 따르면 cloneDeep 또한 2.와 같이 재귀를 통해서 깊은 복사를 한다.
      • 다른 점은 더욱 편리하고 원본의 형식을 해치지 않는 다는 장점이 있다.
      • import _ from 'lodash'
        
        const user = {
            name : ['Lee'],
            age : 25,
            email : ['dankthedust@gmail.com']
        .
        .
        .
        
        let copyUser = _.cloneDeep(user);
        console.log(copyUser === user); //false
        user.email.push('newEmail@gmail.com');
        console.log(user.email === copyUser.email); //false

 

이러한 객체의 참조 특성으로 인해 복사하고자 하는 객체에 중첩된 객체타입이 있는지를 확인하고 그에 맞는 복사 방법을 사용해야한다.

'ocean floor > js floor' 카테고리의 다른 글

JSON과 Storage(local, session)  (0) 2021.10.09
Import와 Export  (0) 2021.10.09
assign() 과 객체의 참조값, 참조에 의한 전달  (0) 2021.10.07
UI와 API  (0) 2021.03.14
JavaScript 연동과 라이브러리(Library)  (0) 2021.03.14