공부를 진행하면서 어느 정도 JavaScript 문법에 대해서 알고 있는 부분들도 생기고 직접 코드를 작성해보는 경험도 쌓이고 있지만 정확하게 이해하지 못하는 부분들이 생길때가 있다. 이런 부분을 보완하기 위해서 멘토님께서 추천해주신 명작, 코어 자바스크립트를 읽으며 정리해보려고 한다.
자바스크립트 데이터 타입에는 크게 두 종류가 있다. 기본형(primitive type)과 참조형(reference type)이다.
기본형(primitive type)
참조형(reference type)
흔히 기본형은 할당 및 연산시 복제되고 참조형의 경우에는 참조된다고 알려져 있다. 하지만, 엄밀히 말하면 두 타입 모두 복제를 하지만 기본형의 경우 값이 담긴 주소를 바로 복제하는 반면에 참조형은 값이 담긴 주소들의 묶음을 가리키고 있는 주소를 복제한다는 점이 다르다. 즉, 참조형의 경우에는 2단계로 주소를 가리키고 있기 때문에 복사 된 주소를 검색해도 또 다른 주소가 나온다는 점이다.
컴퓨터는 모든 데이터를 0 또는 1로 바꿔 기억한다. 0 또는 1만 표현할 수 있는 하나의 메모리 조각을 비트(bit)라고 한다. 메모리는 매우 많은 비트들로 구성되어 있고 고유한 식별자를 통해 위치를 확인할 수 있다. 하지만 0과 1만 표현할 수 있는 비트 단위로 위치를 확인하는 것은 매우 비효율적이기 때문에 8개의 비트를 묶을 수 있는 바이트(byte)라는 단위가 생겨났다. 1 바이트의 경우에는 총 256(2^8)의 값을 표현할 수 있다.
C나 자바 등의 정적 타입 언어는 메모리 낭비를 줄이기 위해서 데이터 타입에 따라서 할당할 메모리 영역이 정해져 있지만, 상대적으로 메모리 압박에서 여유로운 상황에서 등장한 자바스크립트의 경우에는 타입을 구분하지 않고 64비트 즉, 8바이트를 확보하게 되어 있다. 그 덕분에 동일한 변수에 다른 데이터 타입을 할당하면서 형 변환을 걱정하지 않을 수 있다.
변수(variable)와 식별자(identifier)는 혼용되는 경우가 많다.
변수(variable)
: 변할 수 있는 수, 즉 변할 수 있는 데이터를 뜻함.
식별자(identifier)
: 데이터를 식별할 때 사용하는 이름, 즉 변수명.
let v; // 변할 수 있는 데이터를 만들고 식별자는 v!
예시처럼 변수를 선언하면 v는 undefined
를 가지게 된다. 언제든 변할 수 있는 값이기 때문에 필요에 따라서 다른 값을 할당해서 사용할 수 있다. 이렇게 보면 변수
란 변경 가능한 데이터가 담길 수 있는 공간이나 그릇이라고 볼 수 있다.
사용자가 할당된 데이터를 사용하고 싶은 경우에는 v라는 식별자를 검색해서 해당 공간에 담긴 데이터를 반환 받을 수 있다.
let userName; // 변수 userName 선언
userName = '철진'; // 변수 userName에 데이터 할당
let userName = '철진'; // 변수 선언과 할당을 동시에 진행
변수를 선언한 뒤에 따로 할당을 하는 것과 선언과 할당을 동시에 하는 것은 결과적으로 동일한 결과값을 보여준다. 메모리에서 비어있는 공간을 확보하고 그 공간에 userName
이라는 이름을 설정, 이후에 userName
의 이름을 가진 주소를 검색 후에 데이터 '철진'
을 할당한다.
여기서 중요한 점은!! 데이터를 할당할 때 실제로 직접 저장하지 않는다는 점이다! 데이터를 넣기 위한 별도의 메모리 공간을 따로 확보해서 '철진'
을 저장하고 그 주소를 변수에 저장한다.
정리하자면 변수로 1번길 3번지에 식별자 userName
를 저장하고 데이터가 할당되면 데이터로 5번길 4번지 에 실제 데이터 '철진'
을 저장한 후에 그 주소를 변수로 1번길 3번지 에 저장해주는 것이다.
굳이 두 단계를 거치도록 하는 이유는 무엇일까? 이는 데이터의 변환을 자유롭게 할 수 있도록 함과 동시에 메모리를 효율적으로 관리하기 위해서이다.
let userName = '철진'; // 변수 선언과 할당을 동시에 진행
userName = "손흥민" // '철진' 대신 '손흥민' 할당
만약, 위와 같은 코드를 실행한다면 어떤 일이 생기는지 보자. 만약, 직접 데이터를 넣어야 했다면 확보된 공간을 늘리는 작업을 선행해야 하고 또, 내용에 따라 기존에 있던 데이터를 쫓아내는 일이 선행되어야 할 수도 있다. 위의 경우에는 변수로 1번길 3번지 에 있던 '철진'
은 쫓겨나고 새로 '손흥민'
이 들어왔을 것이다.
하지만, 자바스크립트의 경우 아래와 같이 진행한다.
철진
을 그대로 둔다.손흥민
을 저장한다.손흥민
의 주소를 저장한다. 이를 통해서 매번 재할당을 할 때마다 데이터를 옮기고 지우는 등의 작업을 할 필요가 없이 효율적으로 데이터 변환을 처리할 수 있다.
추가적으로, 만약 500개의 변수에 1
이라는 값을 할당하고 싶을 때, 데이터를 직접 할당한다면 500개의 변수 영역을 확보해야 하고 4000 바이트(500*8) 를 사용하게 될 것이다. 하지만, 변수 영역과 데이터 영역을 따로 분리해뒀기 때문에 500개의 변수 영역에는 1
이 있는 주소만 저장하면 되기 때문에 효율이 높아지게 된다.
정식 명칭은 아니지만 변수의 식별자, 데이터의 주소가 저장될 공간을 변수 영역, 데이터가 직접 저장되는 공간을 데이터 영역이라고 표현하겠다.
변수와 상수를 구분하는 성질은 변경 가능성
이다. 불변값과 상수의 경우에는 같은 개념으로 오해되기가 쉬운데 상수와 변수를 구분 짓는 변경 가능성의 대상은 변수 영역에 있는 메모리이다. 한 번 데이터 할당이 이루어진 변수 공간에 다른 데이터를 재할당 할 수 있는지가 관건이다. 반면 불변성 여부를 구분할 때의 영역은 데이터 영역 메모리이다.
예를 들어서, 기본형 데이터인 숫자, 문자열, boolean, null, undefined, Symbol은 모두 불변값을 가지고 있다.
// 아래의 코드를 실행하면!
// '철진'은 그대로 남아 있고 변수 영역의 주소만
// '손흥민'의 주소로 변경된다.
let userName = '철진';
userName = "손흥민"
이전에 제시했던 예시를 살펴보면 문자열인 "철진"은 변경되지 않고 새로운 메모리 공간을 확보해서 '손흥민'을 저장했다. 한가지 예를 더 보자면.
let a = "abc"
a = a + "def";
위의 예시의 경우에 최종적으로 a에는 "abcdef"가 들어가게 될 것이다. 이 때, 뒤에 "def"만 추가됐다고 해서 같은 공간에 추가하며 들어오는 것이 아니라 아예 새로운 공간에 "abcdef"라는 문자열을 저장한다. 둘은 완전히 별개의 데이터이기 때문이다.
이것이 바로 불면값의 성질이다. 가비지 컬렝팅을 거치지 않는다면 한 번 만들어진 값은 계속 변하지 않는다.
기본형을 제외하고 참조형 데이터들이라고 해서 모두 가변값을 가지고 있지는 않다. 하지만, 참조형 데이터들의 경우 가변값을 가지는 경우가 많기 때문에 어떤 방식으로 변경이 일어나는지 확인해 보자.
const 사람1 = {
이름: "철진",
나이: 19,
직업: "개발자"
};
기본형 데이터와 참조형 데이터가 할당될 때 차이점은 참조형의 경우에는 객체의 변수(프로퍼티) 영역이 따로 존재한다는 점이다. 그렇기 때문에 데이터 영역에 직접 데이터가 들어가 있는 것이 아니라 객체의 변수 영역의 주소들이 들어가 있다. 또, 객체의 변수 영역에는 각 데이터가 직접 들어가 있는 데이터 영역 의 주소가 들어가 있기 때문에 변수 영역의 정보가 변경될 수 있는 것이다. 이로 인해서 불변 하지 않는다고(가변값이다) 할 수 있다.
const 사람1 = {
이름: "철진",
나이: 19,
직업: "개발자"
};
사람1.나이 = 24;
만약 사람1
에 다른 값을 재할당하면 어떻게 진행되는지 보자. 위 코드의 경우 사람1
의 나이
프로퍼티에 숫자 24를 할당하려고 한다. 데이터 영역에서 24를 검색한 결과가 없기 때문에 데이터 영역에 새로운 공간을 확보하여 데이터를 저장하 후에 이 주소를 객체의 변수 영역에 저장한다.(원래, 19의 주소를 가지고 있던 공간) 이 때, 변수 사람1
이 가리키고 있는 주소는 변경되지 않는다. 즉, 새로운 객체가 만들어진 것이 아니고 기존 객체의 내부 값만 변경된 것이다.
const 철진 = {
이름 : "주철진",
취미 : ["영화", "독서", "산책"],
나이 : 20
};
위의 경우처럼 객체 안에 또 다시 참조형 데이터를 프로퍼티에 넣는 경우에 또 다른 배열의 변수 영역을 확보하고 똑같이 반복한다.
철진.취미 = "드라마";
가비지 컬렉팅이란 위처럼 참조형 데이터가 들어있던 프로퍼티에 기본형 데이터를 넣은 경우에 배열의 변수 영역의 경우 더이상 참조할 영역이 남아있지 않기 때문에 수거 대상이 된다.
기본형과 참조형의 경우 둘다 변수를 복사하는 과정에서 같은 주소를 바라보게 되는 점은 동일하다. 하지만, 데이터를 할당하는 과정에서 차이가 있기 때문에 이후 동작에서 큰 차이를 보인다.
let a = 10;
let b = a;
let obj1 = { c: 10, d:'add'};
let obj2 = obj1;
b = 15;
obj2.c = 20;
위의 경우를 보자. b
의 경우에는 a
가 가지고 있는 변수 영역 내의 정보를 통해서 데이터 영역에 있는 10이라는 값의 주소를 가져 올 것이다. obj2
역시 obj1
을 따라서 동일한 주소 값을 변수 영역에 저장할 것이다. 하지만 데이터를 재할당하는 과정에서 차이가 발생하게 된다.
먼저, b
의 경우에는 15라는 값이 들어갈 공간을 확보하고 해당 데이터 영역의 주소를 다시 변수 영역에 저장할 것이다. 그렇기에 b
의 변화가 a
전혀 영향을 주지 않는다.
하지만, obj2
의 경우에는 20이라는 값이 들어갈 공간을 확보하는 점에서는 같지만 객체의 변수 영역에 있는 주소가 변경될 뿐 변수 obj1과 obj2가 바라보고 있는 객체는 같은 상태가 된다. 그렇기 때문에 "기본형은 값을 복사하고 참조형은 주소값을 복사한다" 라는 설명이 나오게 된 것이다. 물론, 실제로는 둘 다 주소값을 복사하지만 참조형은 한 단계를 더 거치게 된다는 차이가 있다.
// 객체에 대한 변경일 때도 값이 변경되는 경우!
let obj1 = { c: 10, d:'add'};
let obj2 = obj1;
obj2 = { c: 20, d: 'add'};
만약, 복사한 obj2
에 새로운 객체를 할당하게 된다면 새로운 객체를 참조하게 되고 obj2
의 변경이 obj1
에 영향을 미치지 않도록 할 수 있다. 결국, 참조형 데이터의 가변은 참조형 데이터 자체를 변경하는 경우가 아닌 내부 프로퍼티를 변경하는 경우에 성립한다.
객체의 경우에 불변성을 확보하고 싶을 때가 생길 수 있다. 값으로 전달 받은 객체에 변경을 가하게 되더라도 원본 객체는 변하지 않아야 하는 경우가 종종 발생하기 때문이다.
let user1 = {
name: "철진",
gender: "남자"
};
const changeName = (user, newName) => {
return {
name: newName,
gender: "남자"
};
};
let user2 = changeName(user1, "테디");
위의 경우를 보자. changeName()
라는 함수를 통해서 새로운 객체를 반환하도록 했다. 이를 통해서 user1
과 user2
가 서로 다른 객체를 참조할 수 있도록 했다. 이제 user2
를 자유롭게 이용해도 user1
가 변형되지 않도록 만들었지만 단점이 있다. 바로, 하드코딩을 통해서 변경되지 않는 값들을 다 적어줘야한다는 점이다. 위의 경우에는 프로퍼티가 2개였지만 처리할 정보가 많을 수록 힘들어 질 것이다.
// 얕은 복사
const copyObject = (target) => {
let result = {};
for (let prop in target) {
result[prop] = target[prop];
}
return result;
}
함수 안에서 새로운 객체 result를 생성하고 모든 프로퍼티를 복사한 뒤에 반환하는 함수이다. 위와 같은 방법을 얕은 복사
라고 한다. 바로 아래 단계의 값만 복사하기 때문에 만약 객체 안에 중첩된 객체가 또 존재한다면 위에서 살펴봤던 가변이 여전히 일어나게 된다.
// 중첩된 객체에게 재귀함수를 실행하는 깊은 복사!
const copyObjectDeep = (target) => {
let result = {};
if (typeof target === 'object' && target !== null){
for(let prop in target){
result[prop] = copyObjectDeep(target[prop]);
}
} else {
result = target;
}
return result;
};
위의 경우는 target
이 객체인 경우에 만약 안에 또 다른 객체가 있다면 재귀적으로 copyObjectDeep()
를 다시 호출하며 모든 프로퍼티에 있어서 불변성을 유지하며 복사할 수 있도록 했다.
위의 함수를 활용하는 방법을 제외하고 JSON을 활용하는 방법도 있는데, 이는 메서드는 무시한다는 단점이 있다.
둘 모두 결과적으로는 '없음'을 나타내는 데이터이지만 null
의 경우에는 사용자가 명시적으로 없음을 표시하는 것이고 undefined
의 경우에는 정의되지 않은 값들에 대해서 자동으로 부여될 수도 있다.
null
의 경우에는 typeof
의 경우에 object를 반환하는 자바스크립트 자체의 버그도 있기 때문에 사용에 주의가 필요하다.
둘의 경우에는 똑같지 않다는 점을 기억하자.
마치며! ✨
이렇게 코어 자바스크립트의 시작, 데이터 타입에 대해서 정리를 해봤다. 비교적 알고 있는 내용이라고 생각했었지만, 착각임을 분명하게 깨달았다. 다소 어려웠던 점은 있었지만, 앞으로 갈 개발자의 길에 큰 도움이 될 것이라 믿으며 자바스크립트... 계속 뿌셔볼 예정이다.