자바스크립트내 타입은 크게 원시타입(primitive type) 과 객체타입(object/reference type) 으로 나뉜다고 배웠다. 앞에서 계속해서 원시타입값들에 대해 다루어봤고, 객체타입으로 따로 나뉜다는 것 자체부터 자바스크립트에서 객체 타입이 차지하는 비중이 작지 않다는 것을 유추할 수 있다.
이 두가지 타입의 차이점을 요약하면 크게 3가지로 나눌 수 있다.
- 원시 값은 변경 불가능한 값(immutable value)이다. 반대로 객체는 변경이 가능한 값(mutable value) 이다.
- 원시 값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장된다. 반면 객체를 변수에 할당하면(확보된 메모리공간)에서는 참조 값이 저장된다.
- 원시값을 가지는 변수를 다른 변수에 할당하면 원시값이 복사되어 전달된다. 이를 값에 의한 전달(pass by value)이라고 한다. 반면 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다. 이를 참조에 의한 전달(pass by reference)이라 한다.
11-1. 원시 값
위 3가지 차이점을 바로 이해하긴 힘드니, 하나하나 살펴보자. 우선 변경이 불가능하다라는 말부터 생각해보자. 흔히 생각하기에 변수는 재할당이 가능하지 않은가? 근데 왜 변경이 불가능하다는 것일까.
여기서 변경이 불가능하다는 것은 원시 값 그 자체다. 즉 변경이 불가능하다는 것은 변수가 아니라 값에 대한 진술이라 알면 된다. 우리가 변수에 값을 할당할 때, 일정한 메모리 공간 자체에다가 저장한다고 배웠다. 여기서 변수에 다른 값을 재할당할 시 기존의 값을 변경하여 바꾸는것이 아니라 메모리 공간 자체를 변경하게 되고 그렇기에 원본 값은 그대로 불변이다.
위 사진처럼 값은 변하지 않으며, 새로운 값을 기존 변수에 할당할 때는 다른 메모리 공간에 저장하는 것이다.
그렇다면 const 의 경우는 어떤 경우일까. const 는 변수가 아닌 상수다. 그냥 알고있기론 상수는 변하지 않는다 라고만 파악할 수 있지만, 사실 상수는 재할당이 되지 않는 변수라고 생각하면 된다. 상수 역시 메모리공간이 필요하다. 허나 재할당은 되지 않는다. 이를 값이 변하지 않음과 동일시 하지 말아야 한다.
만일 원시값을 변경할 수 있다면 새로운 메모리 공간은 필요 없을 수 있다. 아래 사진처럼 말이다.
불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없다.
이러한 규칙은 문자열에서도 적용이 된다. 다른 언어에서는 문자열을 객체로 정의하거나 C 같은 경우 char 데이터 타입만 존재하는것에 비해 자바스크립트는 좀 더 편의를 위해 원시타입인 문자열 타입을 제공한다. 그렇기에 변경 불가능하다.
문자열이 원시타입이라는 것은 사실 어색할 수 있다. 문자열이라는 것은 문자들의 집합이다. 그러다 보니 자바스크립트에선 문자열을 유사 배열 객체로서 판단하기에 각 문자 하나하나에 접근할 수 있다. 반복문도 가능한다. (유사배열은 추후에 다시)
var str = 'string';
str[0]; // 's';
// 허나 변경은 안된다.
str[0] = 'S';
console.log(str); // string
문자열이 유사 배열이라는 점은 위처럼 배열처럼 접근할 수 있다는 의미를 가진다. 인덱스로 이루어진 키값을 가지며 length 프로퍼티를 가지는 객체라고 생각하면 된다. 어찌되었던 각 문자에 접근하는것은 가능해도 이를 수정하는 것은 불가능하다. 왜냐하면 문자열은 원시값이기에 불변이기 때문이다. 그래서 마지막 하단 콘솔창에는 변경 전 값이 뜨게 된다.
아, 물론 재할당은 가능하다. 다른 메모리공간을 차지하는 것이기에.
이제 원시값이 복사되어 전달된다는 의미를 파악해보자. 아래 예제를 보면
var score = 80;
var copy = score;
console.log(score, copy); // 80 80
console.log(score === copy); // true
score = 100;
console.log(score, copy); // 100 80
console.log(score === copy); // false
score라는 변수에 원시값 80 을 할당해 주었다. 그렇다면 score 는 하나의 메모리 공간을 차지하게 될 것이다. 앞에서 배웠다시피 값을 변수에 할당할 때 변수가 가르키는 것은 메모리의 주소다.
다음 copy 변수에 변수 score 를 할당했다. "변수에 변수를 할당한다", 이 문장이 가지는 의미는 결국 변수는 값으로 평가되기에 값을 할당한것과 같게된다. 따라서 숫자 80 이 할당된것이라 보면 된다. 물론 메모리 주소는 다르게 된다.
이처럼 변수에 원시값을 갖는 변수를 할당하면 할당받는 변수(copy)에는 할당되는 변수(score)의 원시값이 복사되어 전달된다. 이를 값에의한 전달이라 한다.
중요한 점은 두 변수의 메모리 공간은 별개의 공간이다. 그런 의미로 score 에 100을 재할당 하게 된다면, 아래처럼 copy 의 평가값은 그대로 유지되며, 결국 100 과 80간의 원시값의 차이를 보이게 되기에 비교 시 fasle 가 나오게 된다.
엄밀하게 얘기하자면 값에 의한 참조 역시 사실 값을 전달하는 것이 아니라 메모리 주소를 전달하는 것이다. 다만 전달된 메모리 주소를 통해 메모리 공간에 접근하면 값을 참조할 수 있는 것이다.
그럼에도 확실한것은 결국은 두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다 라는 점이다.
11-2. 객체
원시값과 달리 객체는 변경 가능한 값이라고 하였다. 변경이 가능하다는 말은 새로운 메모리공간을 할당하여 할당값을 변경하는 과정은 아니라는 의미.
아마도 큰 이유라면 객체는 원시값과 다르게 크기가 매우 큰 메모리를 가질 수 있기 때문에, 매번 재할당을 통해 메모리 공간을 확보해야 하는 압박을 좀 덜어주기 위함이라 해석하기도 한다. 그렇다면 어떠한 방식으로 값을 변경하게 되는지 살펴보자.
var person = {
name: 'Lee'
};
console.log(person); // { name: 'Lee' }
이전 원시값과의 차이점은, 원시값은 변수가 가진 메모리 공간 내에 값을 직접 저장하였다면, 객체의 경우 할당한 변수를 참조하면 메모리에 저장되어 있는 참조 값을 통해 실제 객체에 접근한다. 일단 객체를 한번 선언하며 할당하면 2개의 메모리 공간이 필요하게 된다. 이러한 의미로 원시값과 다르게 객체는 "변수는 객체를 참조하고 있다" 라고 말하는 것이다.
원시값은 변경 불가능한 값이므로 원시 값을 갖는 변수의 값을 변경하려면 재할당 외에는 방법이 없다. 하지만 객체는 변경 가능한 값이다. 따라서 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있다. 즉, 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고 프로퍼티 값을 갱신할 수도 있으며 프로퍼티 자체를 삭제할 수도 있다.
ver person = {
name: 'Lee'
};
person.name = 'kim';
person.adress = 'Seoul';
위처럼 기존의 프로퍼티를 수정할 수도 있으며, 없는 프로퍼티를 추가할 수도 있다. 그리고 이렇게 변화를 준 값은 복사본이 아닌 객체 원래의 값이다. 변수 person 은 객체 { name: 'Lee' } 를 참조하고 있었다. 이러한 참조를 활용해 변수를 타고 들어가 원본 객체를 수정하는 것이다.
위 처럼 따로 재할당을 하여 새로운 메모리 공간을 확보하지 않아도 기존 객체의 값을 변경할 수 가 있다. 이렇게 하면 확실히 메모리공간을 아낄 수 있다는 장점이 있다. 하지만 그로 인해 하나의 객체를 여러 식별자가 참조할 수 있다는 단점을 가지게 된다. 이와 관련된 얕은 복사와(shallow copy) 깊은 복사(deep copy)를 좀 살펴보도록 하자.
참고: 얕은 복사 & 깊은 복사
각 변수들에 객체가 할당된다면, 그 식별자들은 원본 객체의 주소를 참조하고 있다는 것을 위에서 살펴보았다. 그렇다면 각 변수들이 참조하는 객체는 모두 하나를 가르키고 있다. 어떠한 문제가 발생할 수 있을까? 예를 들어보자
const a = { age: 20 };
const b = a;
const c = a;
b.age = 30;
console.log(a.age); // 30
console.log(b.age); // 30
console.log(c.age); // 30
변수 a 에 객체를 할당하였고, 그 다음 각각 변수 b, c 에 변수 a 를 할당하였다. 사용자가 의도한 것은 같은 값을 가진 변수들을 복사하고 싶었다. 원본값을 건드리고 싶지 않았기 때문이다. 그래서 이후 변수 b 의 프로퍼티를 30 으로 수정하였고 이를 사용하려 하였는데, 콘솔창의 결과를 보니 원본값이 변경되어있다.
이러한 문제가 발생하는 것은 변수 a,b,c, 가 하나의 객체값을 참조하기 때문이다. 그렇기에 값 변경시 이 값을 참조하는 모든 식별자가 표현하는 값 역시 같이 바뀌게 된다. 아니 엄밀히 얘기하면 참조를 하고 있는것이기에 그렇게 보여질 뿐이다. 바뀐건 원본 객체의 값밖에 없다.
원시값과 객체값은 둘 다 식별자가 기억하는 메모리 공간에 저장된 있는 값을 복사해서 전달한다는 면에서는 동일하다. 하지만 저장된 값이 원시값이냐 참조값이냐에 따라 달라질 뿐이다.
이러한 단점은 원본 불변성의 법칙을 강조하는 리엑트 같은 라이브러리 에서는 문제가 될 수 있다. 변화를 감지해야 하는 리엑트가 원본값이 바뀌어 버리면 비교 대상이 없어지기 때문이다. 그래서 리엑트는 차이점을 인식하지 못하게 되어 사용자의 의도와는 다른 랜더링을 초래하게 된다.
이러한 객체의 특성을 인지하여, 실제 객체에 대해 정말 복사본을 만드는 방법이 있는데, 얕은 복사와 깊은 복사가 있다.
객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 말하고, 깊은 복사는 객체에 중첩되어 있는 개체까지 모두 복사하는 것을 말한다. 한단계라는게 햇갈리는데, 예제를 보면
const o = { x : { y : 1 } };
//얕은 복사
const c1 = {...o} // 스프레드 연산자
console.log(c1 === o) // false
console.log(c1.x === o.x) // true
//깊은 복사
//lodash의 cloneDeep 활용
const _ = require('lodash');
const c2 = _.cloneDeep(o);
console.log(c2 === o); // false
console.log(c2.x === o.x); // false
얕은 복사는 분명 원본과 다른 별개의 객체를 가지지만, 객체에 중첩된 객체의 경우 복사할 때 참조값을 복사하게 된다. 따라서 프로퍼티 x 의 객체 값은 두 변수가 동일한 것이다. (동일한 객체를 참조한다)
반면 깊은 복사는 정말로 모두 복사해버린다. 그래서 모두 다 별개의 객체를 참조하게 된다.
마지막으로 햇갈릴 수 있는 부분을 집고 마무리 하겠다
var person1 = {
name: 'Lee'
};
var person2= {
name: 'Lee'
};
console.log(person1 === person2); // 1번
console.log(person1.name === person2.name); // 2번
1번은 true일까? 얼추 보면 true 라 생각할 수 있다. 하지만 우린 위에서 객체의 성질에 대해 학습하였고, 그렇기에 이것이 false 라는 것을 알 수 있다. 변수가 객체값을 할당 받을 때 원본 객체값을 참조하게 된다. 그런 의미로 두 가지 객체의 내용 자체는 같지만 객체 리터럴은 평가될 때마다 객체를 생성하기에 person1 과 person2 가 참조하는 객체값은 서로 다르기 때문이다.
2번은 참조하는 객체가 다르니 이것도 false 일까? 아니다. name 을 통해 얻는 'Lee' 의 경우 값으로 평가될 수 있는 표현식이다. 따라서 true이다.
자바스크립트에서 객체를 공부할 때 가장 햇갈리게 했던 부분을 이번 장에서 공부해봤다. 책을 보고 블로그를 작성하는 도중에도 머리속으로 계속 시뮬레이션을 돌리면서 다시 이해를 다잡으려고 노력했던 것 같다.
하지만 이렇게 노력해서 이해할만큼 자바스크립트에서 객체가 가지고 있는 비중은 크다. 지금 제대로 이해해야 추후에 다루게 될 내용들의 밑바탕이 될 것이라 확신한다.
다음에는 함수에 대해 살펴보자.
'Programing > Javascript' 카테고리의 다른 글
[Deep Dive] 스코프 (0) | 2022.11.07 |
---|---|
[Deep Dive] 함수 (0) | 2022.10.27 |
[Deep Dive] 객체 리터럴 (1) | 2022.10.23 |
[Deep Dive] 타입 변환과 단축 평가 (0) | 2022.10.22 |
[Deep Dive] 제어문 (0) | 2022.10.21 |