Day +13

비트·2023년 4월 27일
0

CodeStates

목록 보기
13/54
post-thumbnail

1. 원시 자료형과 참조 자료형

  • JavaScript에서 자료형(type)이란 값(value)의 종류.

  • 원시 자료형(primitive type)

    • 42, 'string', true, undefined, null
  • 참조 자료형(reference type)
    •  [0, 1, 2] // 배열
      {name: 'kimcoding', age: 45} // 객체
      function sum (x, y) { return x + y } // 함수



2. 원시 자료형과 참조 자료형의 특징

2-1. 원시 자료형의 특징

  1. 원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장
  2. 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달
  3. 원시 자료형은 변경 불가능한 값(immutable value)이다. 즉, 한 번 생성된 원시 자료형은 읽기 전용(read only) 값

2-2. 참조 자료형의 특징

  1. 참조 자료형을 변수에 할당하면 메모리 공간에 주소값이 저장
  2. 참조 값을 갖는 변수를 다른 변수에 할당하면 주소값이 복사되어 전달
  3. 참조 자료형은 변경이 가능한 값(mutable value)

⚡ 이처럼 원시 자료형과 참조 자료형의 특징은 서로 대비.

1. 값 자체를 저장 vs 주소값을 저장

  • 원시 자료형을 변수에 할당하면 값 자체가 할당된다.

    • let num = 20;
      
    • 변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보.

    • 그리고 20이라는 원시 값을 그 공간에 저장.

    • 이처럼 원시 자료형은 값 자체를 저장


  • 여러 개의 값을 다룰 수 있는 참조 자료형은 어떻게 값을 저장할까?

    • let arr = [0, 1, 2, 3];
      
    • 배열의 요소 각각이 하나의 값이기 때문에 하나의 공간에 배열 자체를 저장하는 것은 불가능

      • 여러 개의 값이 저장되어 있는 공간에 같은 변수명이 부여되어 있으므로, 원하는 데이터를 조회하기 어려울 것이다.
      • 또 배열의 요소나 객체의 프로퍼티는 추가 및 삭제가 수시로 일어나고, 정해진 개수가 없기 때문
    • JavaScript는 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장.

      • 힙(heap) : 참조 자료형을 저장하는 특별한 저장 공간
      • 참조한다(refer) : 변수에 해당하는 저장공간에는 주소값이 저장되어 있고, 그 주소값을 통해 참조 자료형에 접근.


2. 원시 값 자체를 복사 vs 주소값을 복사

  • 만약 어떤 변수에 저장되어 있는 원시 자료형을 다른 변수에 할당하면 어떻게 될까?

    • let num = 20;
      let copiedNum = num;
      
    • 원시 자료형은 값 자체가 복사된다.

      • 즉, 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가진다.

  • 참조 자료형은 이와는 달리 주소값을 복사합니다.

    • let arr = [0, 1, 2, 3];
      let copiedArr = arr;
      
    • 참조 자료형이 할당된 변수를 다른 변수에 할당하면, 이 두 변수는 같은 주소를 가리킨다.


  • 원시 자료형은 원본에 다른 값을 재할당해도 복사본에 영향을 미치지 않습니다.
  • 반면에 참조 자료형은 원본을 변경하면 복사본도 영향을 받는다.
  • 값 자체를 복사하는 원시 자료형과는 달리, 참조 자료형을 할당한 변수를 다른 변수에 할당할 경우 같은 주소를 참조하고 있기 때문


3. 변경 불가능한 값 vs 변경이 가능한 값

  • ⚡ 한 번 생성된 원시 값은 변경할 수 없다.

    • let num = 20;
      
    • 여기서 변수 num에 할당된 값을 숫자 20 대신, 다른 값으로 변경하고 싶으면 변수에 재할당을 하면 가능.

      • num = 30;
      • 변수에 할당된 값이 20에서 30으로 변경되기 때문에 원시 자료형인 숫자 타입의 값이 변경된 것처럼 보인다.

  • num이라는 변수가 참조하던 공간에 들어 있던 2030으로 변경될 것 같지만, 메모리 내부에서는 이처럼 동작하지 않는다.
  • 메모리 내부에서는 30이라는 원시 값을 저장하기 위한 새로운 공간을 확보한 뒤, 그 공간에 num이라는 이름을 붙이고 30을 저장.
  • 이처럼 변수에 다른 값을 재할당해도 원시 값 자체가 변경된 것이 아니라 새로운 원시 값을 생성하고, 변수가 다른 메모리 공간을 참조.
  • 따라서 원시 자료형은 어떤 상황에서도 불변하는 읽기 전용 데이터.
  • 이는 원시 자료형이 높은 신뢰성을 가질 수 있는 요인.
  • 남아 있는 값 20은 어떻게 될까?
    • 가비지 콜렉터(garbage collector) : JavaScript 엔진은 이처럼 사용하지 않는 값을 자동으로 메모리에서 삭제.
    • 그러나 가비지 콜렉터가 어느 시점에 진행되는지는 예측할 수 없다.

  • ⚡참조 자료형은 변경 가능한 값입니다.
    • 앞서 설명한 것처럼 변수는 참조 자료형이 있는 저장공간(heap)의 주소값을 저장하고 있다.
      • 원시 자료형의 경우 값의 크기가 거의 일정하기 때문에 새로운 공간을 확보하여 값을 복사하는 방법이 유용.
      • 하지만, 크기가 일정하지 않은 참조 자료형의 경우 매번 값을 복사한다면 그만큼 효율성은 떨어질 수밖에 없다.
      • 이런 이유로 참조 자료형은 변경이 가능하도록 설계되어 있다.
    • arr[3] = '3';
      arr.push(4);
      arr.shift();
      console.log(arr); // [1, 2, '3', 4]   

  • 변수 str에 문자열 'code'을 할당한 후 다시 변수 str에 문자열 'states'을 할당해보자.
    • let str = 'code';
      str = 'states';
    • 숫자 타입과 마찬 가지로, 변수 str에 다른 문자열을 재할당하면 새로운 공간을 확보하고 그 공간의 이름이 str이 된다.
    • 그 후에 문자열 ‘states’를 저장.
    • 그리고 마찬가지로 가비지 콜렉터에 의해 메모리에서 삭제된다.
    • 문자열은 원시 자료형이지만 배열처럼 인덱스로 문자열의 각 문자에 접근이 가능.
    • console.log(str[0]) // 's'
      console.log(str[2]) // 'a'
    • 하지만 배열과는 달리 인덱스에 직접 다른 문자를 할당하여 값을 변경할 수 없다.
    • ⚡ 문자열도 원시 자료형이기 때문에 값을 변경할 수 없기 때문



3. 얕은 복사와 깊은 복사

  • 원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어난다.
    • 값 자체가 복사된다는 것은 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다는 것을 의미
    • let num = 5;
      let copiedNum = num;
      console.log(num); // 5
      console.log(copiedNum); // 5
      console.log(num === copiedNum); // true
      ...
      copiedNum = 6;
      console.log(num); // 5
      console.log(copiedNum); // 6
      console.log(num === copiedNum); // false  

  • 반면, 참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사된다.
    • let arr = [0, 1, 2, 3];
      let copiedArr = arr;
      ...
      console.log(arr); // [0, 1, 2, 3]
      console.log(copiedArr); // [0, 1, 2, 3]
      console.log(arr === copiedArr) // true
    • 따라서 둘 중 하나를 변경하면 해당 변수가 참조하고 있는 주소에 있는 값이 변경되기 때문에 다른 하나에도 영향을 미치게 된다.
      • 예를 들어 배열을 할당한 변수 arr를 변수 copiedArr에 할당한 후,
      • copiedArrpush() 메서드를 사용하여 배열의 요소를 추가하면,
      • 원본 배열인 arr에도 동일하게 요소가 추가
      • arr이 참조하고 있던 주소가 copiedArr로 복사되어, 두 변수가 같은 주소를 참조하고 있기 때문
    • copiedArr.push(4);
      ...
      console.log(arr); // [0, 1, 2, 3, 4]
      console.log(copiedArr); // [0, 1, 2, 3, 4]
      console.log(arr === copiedArr) // true

⚡ 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 수 없다.


3-1. 배열 복사하기

1. slice()

slice() 메서드는 어떤 배열의 begin 부터 end 까지(end 미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환.

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

console.log(animals.slice(2));//["camel", "duck", "elephant"]

console.log(animals.slice(2, 4));//["camel", "duck"]

console.log(animals.slice(2, 2));//[ ]

console.log(animals.slice(1, 5));//["bison", "camel", "duck", "elephant"]

console.log(animals.slice(-2));//["duck", "elephant"]

console.log(animals.slice(2, -1));//["camel", "duck"]

console.log(animals.slice());//["ant", "bison", "camel", "duck", "elephant"]

mdn 자세한 예제 참조

  • 배열 내장 메서드인 slice()를 사용하면 원본 배열을 복사할 수 있다.
    • let arr = [0, 1, 2, 3];
      let copiedArr = arr.slice();
      console.log(copiedArr); // [0, 1, 2, 3]
      console.log(arr === copiedArr); // false
    • 새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다르다.
    • 주소가 다르기 때문에 복사한 배열에 요소를 추가해도 원본 배열에는 추가되지 않는다.
    • copiedArr.push(4);
      console.log(copiedArr); // [0, 1, 2, 3, 4]
      console.log(arr); // [0, 1, 2, 3]


2. spread syntax

  • spread syntax는 ES6에서 새롭게 추가된 문법.
  • spread라는 단어의 뜻처럼 배열을 펼칠 수 있다.
  • 펼치는 방법은 배열이 할당된 변수명 앞에 ...을 붙여주면 된다.
  • 배열을 펼치면 배열의 각 요소를 확인할 수 있다.
let arr = [0, 1, 2, 3];

console.log(...arr); // 0 1 2 3
  • 만약 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당한다면, 두 변수는 같은 주소를 참조할까?
    • 참조 자료형이기 때문에 각각 다른 주소를 참조
    • let num = [1, 2, 3];
      let int = [1, 2, 3];
      ...
      console.log(num === int) // false

  • 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 어떻게 될까?
    • 원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조하게 된다.
    • 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작.
let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false

copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]

3-2. 객체 복사하기

1. Object.assign()

Object.assign() 메서드는 출처 객체들의 모든 열거 가능한 자체 속성을 복사해 대상 객체에 붙여넣습니다. 그 후 대상 객체를 반환

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);//{ a: 1, b: 4, c: 5 }

console.log(returnedTarget === target);//true

mdn 자세한 예제 참조



2. spread syntax

  • spread syntax는 배열뿐만 아니라 객체를 복사할 때도 사용할 수 있다.
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};

console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false

  • ⚡ 예외
    • 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우에는,
      - slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없다.
    • console.log(users === copiedUsers); // false
    • userscopiedUsers를 동치연산자 === 로 확인해 보면 false가 반환.
    • 각각 다른 주소를 참조하고 있기 때문

  • 그러나 userscopiedUsers의 0번째 요소를 각각 비교하면 true가 반환.
  • users[0]copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문.
    • console.log(users[0] === copiedUsers[0]); // true
  • slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사한다.
    • 이것을 얕은 복사(shallow copy)라고 한다.

3-3. 깊은 복사

  • 깊은 복사(deep copy)
    • 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것
    • JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없다.
    • 단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있다.

1. JSON.stringify()와 JSON.parse()

  • 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고,

  • 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환.

    • JSON.stringify()
      • 참조 자료형을 문자열 형태로 변환하여 반환
    • JSON.parse()
      • 문자열의 형태를 객체로 변환하여 반환

  • const arr = [1, 2, [3, 4]];
    const copiedArr = JSON.parse(JSON.stringify(arr));
    ...
    console.log(arr); // [1, 2, [3, 4]]
    console.log(copiedArr); // [1, 2, [3, 4]]
    console.log(arr === copiedArr) // false
    console.log(arr[2] === copiedArr[2]) // false
  • 예외
    • 중첩된 참조 자료형 중에 함수가 포함되어 있을 경우 위 방법을 사용하면 함수가 null로 바뀌게 된다.


2. 외부 라이브러리 사용

  • 완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 된다.
  • 다음은 lodash의 cloneDeep을 사용한 깊은 복사의 예시
const lodash = require('lodash');

const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);

console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
profile
Drop the Bit!

0개의 댓글