[JavaScript] 자료형 심화

2023-09-18

원시 자료형과 참조 자료형

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

JavaScript에서는 6개의 자료형(number, string, boolean, undefined, null, symbol)을 원시 자료형으로 구분합니다. 원시 자료형이 아닌 모든 자료형은 참조 자료형입니다.

원시 자료형의 특징


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

변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보합니다. 그리고 20이라는 원시 값을 그 공간에 저장합니다. 이를 그림으로 나타내면 아래와 같습니다.

230922-145034

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

let num = 20;
let copiedNum = num:

원시 자료형은 값 자체가 복사됩니다. 즉, 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가집니다.

230922-145102

참초 자료형


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

JavaScript는 참조 자료형을 어떻게 저장할까요? JavaScript는 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간의을 참조할 수 있는 주소값을 변수에 저장합니다. 이때 참조 자료형을 저장하는 특별한 저장 공간을 **힙(heap)**이라고 부르기도 합니다.

참조 자료형이 할당된 변수를 다른 변수에 할당하면, 이 두 변수는 같은 주소를 가리킵니다.

230922-145127

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


1. 변경 불가능한 값

let num = 20;

230922-145151

여기서 변수 num에 할당된 값을 숫자 20 대신, 다른 값으로 변경할 수 있을까요? 다른 값을 변수에 재할당하면 됩니다.

num = 30;

변수에 할당된 값이 20에서 30으로 변경되기 때문에 원시 자료형인 숫자 타입의 값이 변경된 것처럼 보입니다. 그렇다면 원시 자료형이 변경 불가능한 값이라는 것은 무슨 뜻인 걸까요? 값을 재할당했을 때, 메모리에서 어떤 일이 일어났는지 확인해 보겠습니다.

num이라는 변수가 참조하던 공간에 들어 있던 2030으로 변경될 것 같지만, 메모리 내부에서는 이처럼 동작하지 않습니다.

230922-145211

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

230922-145227

2. 변경 가능한 값

참조 자료형은 변경 가능한 값입니다. 앞서 설명한 것처럼 변수는 참조 자료형이 있는 저장공간(heap)의 주소값을 저장하고 있습니다.

크기가 일정하지 않은 참조 자료형의 경우 매번 값을 복사한다면 그만큼 효율성은 떨어질 수밖에 없습니다. 이런 이유로 참조 자료형은 변경이 가능하도록 설계되어 있습니다.

arr[3] = '3';
arr.push(4);
arr.shift();
 
console.log(arr); // [1, 2, '3', 4]

위 코드가 실행되면, 변수가 참조하고 있는 주소에 저장되어 있는 값을 변경합니다.

230922-145246

문자열은 원시 자료형이지만 배열처럼 인덱스로 문자열의 각 문자에 접근이 가능합니다.

console.log(str[0]) // 's'
console.log(str[2]) // 'a'

하지만 배열과는 달리 인덱스에 직접 다른 문자를 할당하여 값을 변경할 수 없습니다. 문자열도 원시 자료형이기 때문에 값을 변경할 수 없기 때문입니다.

얕은 복사와 깊은 복사

원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어 납니다. 값 자체가 복사된다는 것은 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다는 것을 의미합니다.

반면, 참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사됩니다.

따라서 둘 중 하나를 변경하면 해당 변수가 참조하고 있는 주소에 있는 값이 변경되기 때문에 다른 하나에도 영향을 미치게 됩니다. 예를 들어 배열을 할당한 변수 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

230922-145306

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

얕은 복사


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

유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 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

230922-145325

깊은 복사


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

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

230922-145349

간단하게 깊은 복사를 할 수 있는 것처럼 보이지만, 이 방법 또한 깊은 복사가 되지 않는 예외가 존재합니다. 대표적인 예로 중첩된 참조 자료형 중에 함수가 포함되어 있을 경우 위 방법을 사용하면 함수가 null로 바뀌게 됩니다. 따라서 이 방법 또한 완전한 깊은 복사 방법이라고 보기 어렵습니다.

완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 됩니다. 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
javascriptprimitivereference

프로필 사진
TaeWoo Kim
Junior Frontend Engineer