[JavaScript] 원시 자료형과 참조 자료형

KIM DA MI·2023년 3월 2일
0

JavaScript

목록 보기
7/16
post-thumbnail

JavaScript에서 자료형(type)이란 값(value)의 종류이다.
각각의 자료형은 고유한 속성과 메서드를 가지고 있다.
이 자료형은 크게 두 가지로 구분할 수 있는데, 바로 원시 자료형(primitive type)참조 자료형(reference type)이다.


1. 원시 자료형과 참조 자료형의 종류와 특징


원시 타입 데이터 (primitive type data)

  • number(숫자), string(문자), boolean(참과 거짓), undefined, null
    42         // number
    'string'   // string
    true       // boolean
    undefined
    null

원시 자료형의 특징

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

참조 타입 데이터 (reference type data)

  • array(배열), object(객체), function(함수)
    [0, 1, 2] // 배열
    {name: 'kimcoding', age: 45} // 객체
    function sum (x, y) { return x + y } // 함수

참조 자료형의 특징

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

[표] 원시 자료형과 참조 자료형의 특징

  • 원시 자료형과 참조 자료형의 특징은 서로 대비된다.

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

원시 자료형

  • 원시 자료형을 변수에 할당하면 값 자체가 할당된다.
    num이라는 변수를 선언하고 숫자 20을 할당해보자.
    let num = 20;
  • 변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보한다.
    그리고 20이라는 원시 값을 그 공간에 저장한다.
    • 이처럼 원시 자료형은 값 자체를 저장한다.

참조 자료형

  • JavaScript는 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장한다.
    arr이라는 변수를 선언하고 배열 [0, 1, 2, 3]을 할당해보자.
    let arr = [0, 1, 2, 3];
  • 이때 참조 자료형을 저장하는 특별한 저장 공간을 힙(heap)이라고 부르기도 한다.
    따라서 변수 arr에 해당하는 저장공간에는 주소값이 저장되어 있고,
    그 주소값을 통해 참조 자료형에 접근할 수 있다. 이를 참조한다(refer)고 한다.

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

원시 자료형

  • 만약 어떤 변수에 저장되어 있는 원시 자료형을 다른 변수에 할당하면 어떻게 될까?
    let num = 20;
    let copiedNum = num;
  • 원시 자료형은 값 자체가 복사된다.
    즉, 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가진다.

참조 자료형

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

원본을 변경했을 경우

// 원시 자료형이 참조된 변수를 다른 변수에 할당하기
let num = 20;
let copiedNum = num;

// 참조 자료형이 할당된 변수를 다른 변수에 할당하기
let arr = [0, 1, 2, 3];
let copiedArr = arr;

// 두 변수가 같은지 확인하기 - 1
console.log(num === copiedNum); // true
console.log(arr === copiedArr); // true

// 원본을 변경하기
num = 30;
arr.push(4);

// 두 변수가 같은지 확인하기 - 2
console.log(num === copiedNum); // false
console.log(arr === copiedArr); // true

// 이 두 변수의 현재 상태는?
console.log(copiedNum); // 20
console.log(copiedArr); // [0, 1, 2, 3, 4]
  • 원시 자료형은 원본(num)에 다른 값을 재할당해도 복사본(copiedNum)에 영향을 미치지 않았지만,
  • 참조 자료형은 원본(arr)을 변경하면 복사본(copiedArr)도 영향을 받는다.
    ➡ 값 자체를 복사하는 원시 자료형과는 달리, 참조 자료형을 할당한 변수를 다른 변수에 할당할 경우 같은 주소를 참조하고 있기 때문이다.

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

원시 자료형

  • 한 번 생성된 원시 값은 변경할 수 없다.
    num이라는 변수를 선언하고 숫자 20을 할당해보자.
    let num = 20;
  • 여기서 변수 num에 할당된 값을 숫자 20 대신 다른 값을 변수에 재할당하면 어떻게 될까?
    let num = 30;

    • 변수에 할당된 값이 20에서 30으로 변경되기 때문에 원시 자료형인 숫자 타입의 값이 변경된 것처럼 보인다. 그렇다면 원시 자료형이 변경 불가능한 값이라는 것은 무슨 뜻인 걸까?

  • 값을 재할당했을 때, 메모리에서 어떤 일이 일어났는지 확인해보자.

    • num이라는 변수가 참조하던 공간에 들어 있던 2030으로 변경될 것 같지만,
      메모리 내부에서는 이처럼 동작하지 않는다.
    • 메모리 내부에서는 30이라는 원시 값을 저장하기 위한 새로운 공간을 확보한 뒤,
      그 공간에 num이라는 이름을 붙이고 30을 저장한다.
  • 따라서 원시 자료형은 어떤 상황에서도 불변하는 읽기 전용 데이터이며, 이는 원시 자료형이 높은 신뢰성을 가질 수 있는 요인이기도 하다.

  • 남아 있는 값 20은 JavaScript 엔진이 이처럼 사용하지 않는 값을 자동으로 메모리에서 삭제한다.
    이런 기능을 가비지 콜렉터(garbage collector)라고 한다.
    그러나 가비지 콜렉터가 어느 시점이 진행되는지는 예측할 수 없다.

  • 여기에서 이전에 생겼었던 궁금증이 해소되었다!
    문자열도 배열처럼 인덱스로 문자열의 각 문자에 접근이 가능하여
    문자열의 특정 인덱스의 값을 변경하려고 한 적이 있었는데 값이 변경되지 않았기 때문이다.
    • 문자열도 원시 자료형이기 때문에 값을 변경할 수 없었던 것이었다..🤦‍♀️

참조 자료형

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

    arr[3] = '3';
    arr.push(4);
    arr.shift();
    
    console.log(arr); // [1, 2, '3', 4]
    • 위 코드가 실행되면, 변수가 참조하고 있는 주소에 저장되어 있는 값을 변경한다.



2. 얕은 복사와 깊은 복사


얕은 복사

배열 복사하기

배열을 복사하는 방법은 크게 두 가지 방법이 있다.
배열 내장 메서드인 slice()를 사용하는 방법과 ES6에서 도입된 spread문법을 사용하는 방법이다.

slice()

  • 배열 내장 메서드인 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]

spread syntax

  • spread syntax는 ES6에서 새롭게 추가된 문법으로, spread라는 단어의 뜻처럼 배열을 펼칠 수 있다.

  • 펼치는 방법은 배열이 할당된 변수명 앞에 ... 을 붙여주면 된다.
    배열을 펼치면 배열의 각 요소를 확인할 수 있다.

    let arr = [0, 1, 2, 3];
    
    console.log(...arr); // 0 1 2 3
  • spread syntax로 배열을 복사하기 위해서 배열을 생성하는 방법을 이해해야 한다.
    만약 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당한다면,
    두 변수는 참조 자료형이기 때문에 각각 다른 주소를 참조한다.

    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]



객체 복사하기

Object.assign()

  • 객체를 복사하기 위해서는 Object.assign()을 사용한다.

    let obj = { firstName: "dami", lastName: "kim" };
    let copiedObj = Object.assign({}, obj);
    
    console.log(copiedObj) // { firstName: "dami", lastName: "kim" }
    console.log(obj === copiedObj) // false


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를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없다.

  • 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있다.

  • 유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 usersslice() 메서드를 사용하여 복사해보자.

    let users = [
        {
            name: "kimcoding",
            age: 26,
            job: "student"
        },
        {
            name: "parkhacker",
            age: 29,
            job: "web designer"
        },
    ];
    
    let copiedUsers = users.slice();

  • userscopiedUsers를 동치연산자(===)로 확인해 보면 false가 반환된다.
    위에서 살펴본 바와 같이 각각 다른 주소를 참조하고 있기 때문이다.

    console.log(users === copiedUsers); // false

  • 그러나 userscopiedUsers의 0번째 요소를 각각 비교하면 true가 반환된다.
    users[0]copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문이다.

    console.log(users[0] === copiedUsers[0]); // true

  • 이처럼 slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사한다. 이것을 얕은 복사(shallow copy)라고 한다.



깊은 복사

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

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로 바뀌게 된다.
    따라서 이 방법 또한 완전한 깊은 복사 방법이라고 보기 어렵다.

    const arr = [1, 2, [3, function(){ console.log('hello world')}]];
    const copiedArr = JSON.parse(JSON.stringify(arr));
    
    console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
    console.log(copiedArr); // [1, 2, [3, null]]
    console.log(arr === copiedArr) // false
    console.log(arr[2] === copiedArr[2]) // false

외부 라이브러리 사용

  • 완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 된다.
    lodashramda는 각각 방법으로 깊은 복사를 구현해두었다.

  • lodashcloneDeep을 사용한 깊은 복사의 예시

    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

0개의 댓글