원시 타입과 참조 타입

김세현·2022년 3월 14일
1

JavaScript

목록 보기
5/5

자바스크립트에는 Number, String, null, undefined, symbol, boolean, 객체 총 7가지 데이터 타입이 존재한다.
이 7가지는 다시 두 가지의 타입으로 분류할 수 있다.

  • 원시타입 : Number, String, null, undefined, symbol, boolean
  • 참조타입 : 원시 타입을 제외한 나머지 (객체, 배열, 함수...)

두 가지 타입의 간단한 비교


원시 타입과 참조 타입을 아래와 같이 세 가지 관점에서 비교할 수 있다.

  • 변수에 값을 할당할 때
    원시 값을 변수에 할당하면 확보된 메모리 공간(변수)에는 '실제 값'이 저장된다.
    참조타입은 변수에 할당하면 확보된 메모리 공간(변수)에는 '참조 값'(주소 값)이 저장된다.

  • 변수의 값을 변경할 때
    원시 타입의 값은 변경 불가능한 값이다.(=Immutable)
    참조 타입의 값은 변경 가능한 값이다.(=Mutable)

  • 변수를 다른 변수에 할당했을 때
    원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 원시 값이 '복사'되어 전달된다.
    이를 값에 의한 전달(pass by value)라고 한다.
    객체를 가리키는 변수를 다른 변수에 할당하면 '원본의 참조 값'이 복사되어 전달된다.
    이를 참조에 의한 전달(reference by value)이라 한다.

원시 값을 가지는 원본 변수를 다른 변수에 할당하고 나서, 원본 변수 값을 변경하는 코드

객체 타입을 갖는 변수를 다른 변수에 할당하고 나서, 원본 변수의 값(객체의 프로퍼티)을 변경하는 코드


원시 타입에 대하여...


원시 타입의 '값'은 변경 불가능한 값(Immutable Value)이다.

값을 변경할 수 없다?... 변수의 값은 변경할 수 있지 않았나?

우선, 여기서 말하는 변수와 값은 구분해서 생각해야 한다.
'값'을 저장하기 위해 메모리 공간을 확보하고 이 메모리 공간들을 식별하기 위해 이름을 붙인 이름이 '변수'이다.
즉 변경 불가능하다는 것은 변수가 아니라 값 자체를 말하는 것이다.
또한 값 변경이 불가능하다고 이것이 상수를 말하는 것은 아니다.
상수는 재할당이 금지된 '변수'를 말한다.

원시 값은 변경 불가능한 값, 즉 읽기 전용 값이다.
이러한 원시 값의 특성은 데이터의 신뢰성을 보장한다.

즉 우리가 자바스크립트에서 변수에 값을 할당하고, 다시 변수에 다른 값을 재할당할 때 값 자체를 변경하는 것이 아니라, 새로운 메모리 공간을 확보하고 재할당할 값을 저장한 후, 변수는 재할당한 값을 가리키는 것이다.
이때 변수가 참조하던 메모리 공간의 주소가 바뀌는 것이다.

(var 키워드는 변수의 선언과 초기화(undefined)가 동시에 진행된다.
메모리 공간의 변화를 여러 단계로 나타내기 위해 var 키워드를 사용했다.)

이처럼 변수 값을 변경하기 위해 원시 값을 재할당하면 새로운 메모리 공간을 확보하고 새로운 값을 저장한 후에 변수가 참조하던 메모리 공간의 주소를 변경한다.


객체 타입에 대하여...


객체(참조) 타입의 값, 즉 객체는 변경 가능한 값이다.(Mutable Value)

위에서 살펴본 원시 값을 할당한 변수는 원시 값이 저장된 메모리 공간을 기억하고 있으므로, 변수를 통해 메모리 공간에 접근하면 원시 값에 접근할 수 있다.
즉 원시 값을 할당한 변수는 원시 값 자체가 값이다.

하지만 객체 타입을 할당한 변수를 통해 메모리 공간에 접근하면 주소 값에 접근할 수 있다.
주소 값은 생성된 객체가 저장된 메모리 공간의 주소 자체를 말한다.

그림처럼 객체를 할당한 변수를 참조하면 메모리에 저장되어 있는 참조 값을 통해 진짜 객체에 접근한다.
원시 타입과 객체 타입의 이러한 차이 때문에 일반적으로 원시 값을 할당한 변수를 "변수는 X값을 갖는다" 또는 "변수의 값은 X이다"라고 표현하는 반면,
객체를 할당한 변수는 "변수는 객체를 참조하고 있다" 또는 "변수는 객체를 가리키고 있다"라고 표현한다.
즉 위의 그림에서 "obj 변수는 객체 {name : 'kim'}을 가리키고(참조하고) 있다"라고 표현할 수 있다.

원시 값은 변경 불가능한 값이므로 원시 값을 갖는 변수의 값을 변경하려면 무조건 변수에 값을 재할당 해야한다.
하지만 객체는 변경 가능한 값이다. 따라서 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있다.
즉, 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고 프로퍼티 값을 갱신할 수도 있으며 프로퍼티를 삭제할 수도 있다.

obj.age = 30;      //age 프로퍼티 추가
obj.name = 'park'; //프로퍼티 값 갱신
delete obj['age']  //프로퍼티 삭제

객체는 변경 가능한 값이므로 메모리에 저장된 객체를 수정할 수 있다. 이때 객체를 할당한 변수에 재할당을 하지 않았으므로 객체를 할당한 변수의 주소 값(참조 값)은 변경되지 않는다.

사실, 객체 타입은 이러한 특징 때문에 부작용?이 발생할 수 있다.

여러 개의 식별자(변수 이름이라고 이해하자)가 하나의 객체를 공유할 수 있기 때문!!

객체 타입을 가리키는 원본 변수(obj)을 다른 변수(copy)에 할당하면 원본의 참조 값이 복사되어 전달된다. (copy는 obj가 가리키는 객체를 가리킨다. = 즉 두 개의 식별자(변수)가 하나의 객체를 가리키고 있다.)

위 그림처럼 원본 obj 변수를 다른 변수 copy에 할당하면 원본의 주소 값을 복사해서 copy에 저장한다.
이때 원본 obj와 copy가 저장된 메모리 주소는 다르지만 동일한 주소 값을 갖는다.
즉, 원본 obj와 copy는 동일한 객체를 가리킨다. 이것은 두 개의 변수가 하나의 객체를 '공유'한다는 것을 의미하기도 한다.
따라서 원본 obj 또는 copy 중 한쪽에서 객체의 프로퍼티에 대한 추가, 삭제, 변경을 하게 되면 나머지 한쪽도 영향을 받게 된다.

이러한 객체 타입의 특징을 잘 모르고 사용한다면 예상치 못한 에러? 결과? 를 만날 수 있다!


참고


얕은 복사 vs 깊은 복사

  • 얕은 복사 (Shallow Copy)
    얕은 복사는 참조(주소)값의 복사를 의미한다.
    위의 사진에서 11~13행에 해당하는 코드이다.
    이를 참조 할당이라고도 하는데, 이렇게 얕은 복사를 하게 되면, 새로운 객체가 새로 생성되는 것이 아니라, 원본과 사본이 하나의 객체를 공유한다고 보면 된다. (해당 데이터의 참조 값(주소 값)을 복사)

  • 깊은 복사 (Deep Copy)
    깊은 복사는 값 자체의 복사를 의미한다.
    기존의 원시 값이 저장된 변수를 다른 변수에 할당 했을 때를 생각하면 된다.
    아래의 코드에서 변수 a를 새로운 변수 b에 할당한 뒤에 b의 값을 변경하여도 a의 값은 변경되지 않는다.
    두 값을 비교하면 false가 출력되며 서로의 값은 별개의 값이다.
    즉 독립적인 메모리 공간에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.

let a = 5;
let b = a;

b = 3;
console.log(a===b) //false
console.log(a) // 5
console.log(b) // 3

객체 타입의 값을 복사하여 사용할 경우 기존 객체의 원본 데이터가 의도치 않게 변경될 수 있으므로 주의해서 사용해야 한다.
객체의 깊은 복사를 하는 방법 중 몇 가지에 대해 알아보자.

깊은 복사 첫 번째 방법 : Object.assign

Object.assign(생성할 객체, 복사할 객체)

메서드의 첫 번째 인수로 빈 객체를 넣어주고, 두 번째 인수로 복사할 객체를 넣어주면 된다.

10행을 보면, 새롭게 생성한 copyObj와 원본 obj는 다르다. (false)
하지만 11행을 보면 중첩된 객체의 경우에는 참조 값이 복사되어 서로 같은 데이터(c라는 프로퍼티를 가진 b객체)를 공유한다. (true)
따라서, 중첩된 객체의 프로퍼티 값을 변경하면 원본 객체의 중첩된 객체도 변경된다.

Object.assign을 활용한 복사는 완벽한 깊은 복사가 아니다.


깊은 복사 두 번째 방법 : 전개 연산자 (Spread Operator)

Object.assign을 통해 깊은 복사를 했던 것처럼 같은 결과가 나온다.
새롭게 생성된 객체와 원본 객체는 다른 주소 값(참조 값)을 가지지만, 중첩된 배열의 참조 값은 복사되어 서로 하나의 객체를 공유하고 있다. (b 객체)
따라서 아래의 10행처럼 새롭게 생성된 객체에 있는 프로퍼티 값을 변경했지만, 원본의 프로퍼티도 변경된다.(11행)

결론 :

중첩된 객체까지 모두 복사하는 깊은 복사를 위해서는 lodash 모듈의 cloneDeep()을 써주자.


===를 통한 객체 타입 비교


객체 리터럴을 통해 객체를 생성할 때마다 새로운 객체가 생성된다. (다른 주소 값을 가지는)
따라서 9행은 겉보기에 같아 보이지만, 실제로는 다른 주소 값을 가지므로 false가 출력된다.
그리고 객체의 프로퍼티 값으로 원시 값(문자열)이 저장되어 있다.
원시값의 비교는 true를 나타낸다.


출처


모던 자바스크립트

profile
under the hood

0개의 댓글