JavaScript: JS 데이터 Ep.4 (전개연산자, 불변성, 얕은복사, 깊은 복사)

전개 연산자 (Spread)


마침표 3개 (…)가 전개 연산자의 기호이다.

전개연산자는 하나의 배열데이터를 쉼표로 구분된 각각의 아이템으로 전개해서 출력한다.

const fruits = ['Apple', 'Banana', 'Cherry']
console.log(fruits)

// 전개 연산자
console.log(...fruits)
// 'Apple', 'Banana', 'Cherry'
// 아래의 방법으로 출력하는 것과 동일한 모양으로 나온다.
// console.log('Apple', 'Banana', 'Cherry')

function toObject(a, b, c) {
	return {
		a: a,
		b: b,
		c: c
	}
}
console.log(toObject(...fruits))
// {a: "Apple", b: "Banana", c: "Cherry"}
  • 전개연산자를 사용하지 않았을 때
    아래와 같이 작성해야 동일한 형태로 출력이 된다. 하나씩 수동으로 작성해야 하기 때문에 번거롭다. 또, 갯수가 많아질 수록 비효율적이게 된다.
    console.log(toObject(fruits[0], fruits[1], fruits[2]))
    // {a: "Apple", b: "Banana", c: "Cherry"}
    
  • 매개변수에 전개연산자 사용하기 → 나머지 매개변수(rest parameter)
    Apple은 a로, Banana는 b로 들어가게 된다. 나머지 Cherry와 Orange는 c로 들어간다. 매개변수의 전개연산자는 나머지의 모든 인수들을 다 받아내는 역할을 한다. 이것을 나머지 매개변수(rest parameter)이라고 부른다.

    const fruits = ['Apple', 'Banana', 'Cherry', 'Orange']
      
    function toObject(a, b, ...c) {
      return {
        a: a,
        b: b,
        c: c
      }
    }
    console.log(toObject(...fruits))
    // {a: "Apple", b: "Banana", c: Array(2) 
    // 0: "Cherry" 1: "Orange"}
    
  • 축약형으로 만들기
    • 속성의 이름 = 변수의 이름 (데이터의 이름)
      속성의 이름과 변수의 이름이 같을 때, 하나만 남겨두어도 된다.
      const fruits = ['Apple', 'Banana', 'Cherry']
          
      function toObject(a, b, c) {
        return {
          a,
          b,
          c
        }
      }
      console.log(toObject(...fruits))
      // {a: "Apple", b: "Banana", c: "Cherry"}
      
    • 화살표 함수로 만들기
      객체데이터를 사용시에는 중괄호를 사용하는데, 중괄호는 화살표함수에서 함수의 범위를 나타내는 블럭의 의미로 해석이 된다. 중괄호가 블럭이 아닌 객체 데이터의 의미로 사용하기 위해서는 소괄호를 밖에 사용해주고 중괄호(객체 데이터 의미)를 사용해 주면 된다.
      const fruits = ['Apple', 'Banana', 'Cherry']
          
      const toObject = (a, b, c) => ({a, b, c})
          
      console.log(toObject(...fruits))
      // {a: "Apple", b: "Banana", c: "Cherry"}
      

불변성 (Immutability)


원시데이터

JS에서 사용할 수 있는 기본 데이터들을 의미한다.
String, Number, Boolean, undefined, null

참조형 데이터

Object, Array, Function

원시 데이터불변성

  • 생김새가 달라서 false가 아닌, 메모리의 주소가 다르기 때문에 일치하지 않는다.
    // main.js
        
    let a = 1
    let b = 4
    console.log(a, b, a === b) // 1, 4, false
        
    // ----------------------------------------
    // |1: 1    |2:  4   |3:    |4:   
    // ----------------------------------------
    
  • b 는 기존에 2번 메모리의 주소를 가지고 있었다. 그러나 b = a로 인해 b는 더이상 2번 메모리 주소가 아닌 1번 메모리 주소를 가지고 있게 되었다. 그렇기에 b와 a가 가지고 있는 주소가 동일해 지면서 true가 나온다.
    2번 메모리의 값은 4로 변하지 않는다. 변하는 것은 오직 b가 가지고 있는 메모리 주소값!
    // main.js
        
    let a = 1
    let b = 4
        
    b = a
    console.log(a, b, a === b) // 1, 1, true
        
    // ----------------------------------------
    // |1: 1    |2:  4   |3:    |4:   
    // ----------------------------------------
    
  • a = 7이라는 새로운 데이터가 생성된다. 숫자 데이터 7은 메모리 3번에 들어가면서 a는 1번 메모리 주소가 아닌 3번 메모리 주소를 갖게 된다.
    // main.js
        
    let a = 1
    let b = 4
        
    a = 7
    console.log(a, b, a === b) // 7, 1, false
        
    // ----------------------------------------
    // |1: 1    |2:  4   |3: 7   |4:   
    // ----------------------------------------
    
  • 변수 c에는 숫자 데이터 1을 할당했다. c에 할당된 숫자 데이터 1이 4번째 메모리에 들어가는 것이 아니라, c는 기존 숫자 데이터 1이 있는 1번 메모리 주소를 갖게 된다.
    // main.js
        
    let a = 1
    let b = 4
        
    let c = 1
    console.log(b, c, b === c) // 1, 1, true
        
    // ----------------------------------------
    // |1: 1    |2:  4   |3: 7   |4:   
    // ----------------------------------------
    
  • 생긴것이 다르면 다른 데이터!
    새로운 원시데이터를 사용했을 때, 원시 데이터가 기존에 메모리 주소에 들어있다면 해당 원시데이터를 새로운 메모리에 새롭게 만드는 것이 아니다. 기존에 존재하는 메모리 주소만 바라보도록 만들어 준다.
    즉, 원시데이터들은 새롭게 만들어 지는 것이 아닌 한번 만들어 지면 항상 불변한다는 개념이다. (기존에 데이터들은 변하지 않는다!)
    간단히 말하면, 원시형 데이터들은 생긴것이 다르면 다른 데이터라고 이해하면 된다. 생긴 것이 같으면 같은 데이터!

참조형 데이터가변성

참조형 데이터는 원시형 데이터와 다르게 새로운 값을 만들때 마다 새로운 메모리 주소에 할당되는 구조를 가지고 있다.
따라서, 참조형 데이터는 불변성이 없다. 즉, 가변성이다!

  • a와 b는 생김새는 같아도 서로 다른 메모리 주소를 바라보고 있기 때문에 false가 출력된다. a는 1번 메모리, b는 2번 메모리 주소를 갖고 있다.
    let a = { k: 1 }
    let b = { k: 1 }
    console.log(a, b, a === b)
    // { k: 1 } { k: 1 } false
        
    // ---------------------------------------------------
    // |1:  { k: 1 } |2: { k: 1 } |3: {     } |4: {     }
    // ---------------------------------------------------
        
    
  • a가 가지고 있는 메모리 주소의 데이터 k의 값을 7로 변경했다.
    b는 2번 메모리 주소를 가지고 있었지만, b = a로 인해 b는 a가 가지고 있는 메모리 주소 (1번 메모리)를 갖게 된다.
    동일한 메모리 주소를 가지고 있기 때문에 true값이 출력된다.
    let a = { k: 1 }
    let b = { k: 1 }
        
    a.k = 7
    b = a
    console.log(a, b, a === b)
    // { k: 7 } { k: 7 } true
        
    // ---------------------------------------------------
    // |1:  { k: 7 } |2: { k: 1 } |3: {     } |4: {     }
    // ---------------------------------------------------
        
    
  • a.k = 2를 통해 a의 메모리 주소의 값 (1번 메모리)이 2로 변경된다.
    b는 a와 동일한 주소를 가지고 있기 때문에 b도 1번 메모리 주소를 가지고 있다.
    a와 b의 값을 출력하면 k:2가 출력되면서 동일한 메모리 주소를 갖기에 true가 반환된다.
    let a = { k: 1 }
    let b = { k: 1 }
        
    a.k = 2
    console.log(a, b, a === b)
    // { k: 2 } { k: 2 } true
        
    // ---------------------------------------------------
    // |1:  { k: 2 } |2: { k: 1 } |3: {     } |4: {     }
    // ---------------------------------------------------
      ****
    

    주의할 점은, a의 k 속성값만을 바꾼 것인데 의도하지 않게 b의 k값도 변경이 된다. b가 a와 동일한 주소를 가지고 있기 때문이다.

  • a는 1번 메모리 주소, b도 1번 메모리 주소를 가지고 있다.
    c도 b를 할당 했기 때문에 c도 1번 메모리 주소를 갖게 된다.
    a, b, c 모두 동일한 메모리 주소를 갖게 되었다.
    let a = { k: 1 }
    let b = { k: 1 }
        
    let c = b
    console.log(a, b, c, a === c)
    // { k: 2 } { k: 2 } { k: 2 } true
        
    // ---------------------------------------------------
    // |1:  { k: 2 } |2: { k: 1 } |3: {     } |4: {     }
    // ---------------------------------------------------
      ****
    
  • 수정한 것은 a변수의 k속성이지만 a, b, c 모두가 동일한 1번 메모리 주소 (a의 메모리 주소)를 가지고 있기 때문에 동일한 값이 출력된다.
    let a = { k: 1 }
    let b = { k: 1 }
        
    a.k = 9
    console.log(a, b, c, a === c)
    // { k: 9 } { k: 9 } { k: 9 } true
        
    // ---------------------------------------------------
    // |1:  { k: 9 } |2: { k: 1 } |3: {     } |4: {     }
    // ---------------------------------------------------
    
  • a = b (참조형 데이터에서 할당연산자 사용)
    a가 가지고 있는 데이터가 복사되는 개념이 아닌 메모리의 주소만 같은 곳을 바라보도록 만들어 주는 것이기 때문에 a를 수정하면 b도 수정되고, b를 수정하면 a도 수정되는 현상이 발생한다.
    위의 방식은 의도한 것이 아니라면 일반적인 경우에는 복사를 사용하여 a 와 b를 메모리 상에서 분리해줘야 생각한 방식으로 사용이 가능하다.

얕은 복사(Shallow copy)와 깊은 복사(Deep copy)


복사를 사용하기 전

  • copyUser과 user는 같은 메모리 주소를 가지고 있다.
    const user = {
      name: 'Soha',
      age: 45,
      email: ['soha@gmail.com']
    }
    const copyUser = user
    console.log(copyUser === user) // true
        
    // |1:     |2:     |3:      |4:     
    
  • user.age의 값은 22로 변경했는데 copyUser의 age값도 22로 변경됐다. 둘이 동일한 메모리 주소를 바라보고 있기 때문이다.
    const user = {
      name: 'Soha',
      age: 45,
      email: ['soha@gmail.com']
    }
    const copyUser = user
        
    user.age = 22
    console.log('user', user) 
    // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
    console.log('copyUser', copyUser)
    // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
    

복사 사용하기

  • 메소드 assign을 사용한 복사 → 얕은 복사
    Object에 assign 메소드를 실행해서 첫번째 인수로 빈 객체 데이터, 두번째 인수로 복사한 user변수를 넣는다.
    첫번째 인수는 대상객체, 두번째 인수부터는 출처객체. 출처객체를 대상객체에 담아 반환한다.
    const user = {
      name: 'Soha',
      age: 45,
      email: ['soha@gmail.com']
    }
    const copyUser = Object.assign({}, user)
    console.log(copyUser === user) // false
        
    user.age = 22
    console.log('user', user)
    // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
    console.log('copyUser', copyUser)
    // { name: 'Soha', age: 45, email: ['soha@gmail.com']}
        
    console.log('------')
    console.log('------')
    

    중괄호를 사용한 객체 리터럴도 데이터이기 때문에 새로운 메모리 주소에 할당된다. 2번 메모리에 user가 가지고 있는 속성과 값이 들어가게 된다.

    따라서 user은 1번 메모리 주소, copyUser은 2번 메모리 주소를 갖고 있다. user.age를 22로 변경했을 때, copyUser의 age는 변경되지 않는 것을 볼 수 있다.

  • 전개 연사자를 사용한 복사 → 얕은 복사
    const copyUser = {...user}를 통해 copyUser가 새로운 메모리 주소에 할당되었다.
    하나의 빈 객체 데이터 내부에 user라는 객체 데이터가 가지고 있는 속성과 값들을 전개해서 집어넣게 된다.
    const user = {
      name: 'Soha',
      age: 45,
      email: ['soha@gmail.com']
    }
    const copyUser = {...user}
    console.log(copyUser === user) // false
        
    user.age = 22
    console.log('user', user)
    // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
    console.log('copyUser', copyUser)
    // { name: 'Soha', age: 45, email: ['soha@gmail.com']}
        
    console.log('------')
    console.log('------')
    
  • 얕은 복사(Shallow copy)

    user 부분에 emails라는 배열 데이터 부분에 push 메소드를 사용했다. push 메소드는 배열 데이터의 가장 뒷부분에 새로운 아이템으로 push 부분의 인수를 밀어 넣는다.

    const user = {
      name: 'Soha',
      age: 45,
      email: ['soha@gmail.com']
    }
    const copyUser = {...user}
    console.log(copyUser === user) // false
      
    user.age = 22
    console.log('user', user)
    // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
    console.log('copyUser', copyUser)
    // { name: 'Soha', age: 45, email: ['soha@gmail.com']}
      
    user.emails.push('noha@naver.com')
    
    • user.emails 와 copyUser.emails가 동일하다는 값이 나온다.
      복사를 통해 user와 copyUser는 달라졌는데 왜 일까?
      console.log(user.emails === copyUser.emails) // true
      // console.log('user', user)
      // console.log('copyUser', copyUser)
      

      user.emails는 배열데이터로 배열 데이터는 참조형 데이터이다.
      우리는 user.emails 배열데이터를 따로 복사처리를 한 적이 없다. 우리는 user라는 객체 데이터 하나를 복사처리 한 것이지 user. emails 배열 데이터를 복사한 것은 아니라는 것! 그래서 true값이 나오는 것이다.

      console.log(user.emails === copyUser.emails) // true
          
      console.log('user', user)
      // { name: 'Soha', age: 22, email: 
      // Array(2) 0: 'soha@gmail.com' 1: 'noha@naver.com'}
          
      console.log('copyUser', copyUser)
      // { name: 'Soha', age: 45, email: 
      // Array(2) 0: 'soha@gmail.com' 1: 'noha@naver.com'}
      

      copyUser의 emails에 추가한 적은 없지만, 따로 복사를 하지 않았기 때문에 같은 메모리 주소를 바라보게 되면서 copyUser의 emails와 user의 emails가 동일하게 변경되었다.

    • 얕은 복사
      겉 표면만 복사가 되었고 내부의 깊은 부분(email 배열 데이터)은 복사가 되지 않았다. 이것을 얕은 복사라고 한다.
  • 깊은 복사(Deep copy)

    JS만으로는 깊은 복사를 구현하기 어렵기 때문에 Lodash를 사용하여 깊은 복사를 할 것이다.

    • _ (언더바) 기호가 하나의 객체 데이터이다. (lodash 패키지 기능)
    • lodash 기능 뒤에 lodash 메소드인 cloneDeep() 메소드를 작성한다. 인수로 user라는 객체를 넣어주면서 깊은 복사가 된다.
      import _ from 'lodash'
          
      const user = {
      	name: 'Soha',
      	age: 45,
      	email: ['soha@gmail.com']
      }
      const copyUser = _.cloneDeep(user)
      console.log(copyUser === user) // false
          
      user.age = 22
      console.log('user', user)
      // { name: 'Soha', age: 22, email: ['soha@gmail.com']}
      console.log('copyUser', copyUser)
      // { name: 'Soha', age: 45, email: ['soha@gmail.com']}
      
    • user부분의 emails만 새로운 값이 push된다.
        user.emails.push('noha@naver.com')
        console.log(user.emails === copyUser.emails) // false
            
        console.log('user', user)
        // { name: 'Soha', age: 22, email: 
        // Array(2) 0: 'soha@gmail.com' 1: 'noha@naver.com'}
            
        console.log('copyUser', copyUser)
        // { name: 'Soha', age: 45, email: 
        // Array(1) 0: 'soha@gmail.com'}
      
  • 총 정리

    • 얕은 복사
      대표적인 참조형 데이터 (객체, 배열)를 복사할때는 내부에 또 다른 참조형 데이터가 없다는 전재하에 사용하기.
    • 깊은 복사
      내부에 또 다른 참조형 데이터가 있을 때 사용하기.

Comments