var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2.c = 20;
주소 | 1001 | 1002 | 1003 | 1004 | 1005 | ... |
---|---|---|---|---|---|---|
데이터 | 이름: a | 이름: b | 이름: obj1 | 이름: obj2 | ||
값: @5001 | 값: @5004 | 값: @5002 | 값: @5002 |
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | ... |
---|---|---|---|---|---|---|
데이터 | 10 | @ 7103 ~ ? | 'ddd' | 15 | 20 |
주소 | 7103 | 7104 | ... |
---|---|---|---|
데이터 | 이름: c | 이름: d | |
값: @5005 | 값: @5003 |
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
먼저, 위의 네 줄의 코드를 살펴봅시다. 변수 a라는 값에 데이터의 자리를 마련해놓습니다. 그리고 그 곳에 그 값을 가리키는 메모리 주소를 입력하면 된다고 설명드렸습니다. 메모리 주소에서 찾아보니 @5001번에 숫자 10이라는 데이터가 있으므로 해당 주소를 입력합니다. 원래 b라는 변수도 a와 동일한 주소를 가리켰으므로 같은 메모리 주소를 값으로 가집니다.
변수 obj1은 객체이므로 참조형 데이터로 위와는 다른 방식으로 동작합니다. 한 단계를 더 거치는 것이라고 이해하시면 쉽습니다. obj2가 같은 obj1와 같은 메모리 주소(@5002)를 바라보고 있습니다. 그 메모리 주소는 또 값으로 주소를 가지며, 객체 데이터 내에 2개의 프로퍼티를 갖고 있으므로 각각 두 개의 프로퍼티에 변수명 c와 d가 다른 곳을 바라보고 있을 것입니다. 아랫줄을 제외하고 생각해보면, c는 @5001, d는 @5003을 바라보고 있습니다.
b = 15;
obj2.c = 20;
이제 뒤의 두 줄을 통해 기본형과 객체의 프로퍼티 안에서 값 변경의 차이를 살펴봅시다. b는 원래 a와 마찬가지로 10이라는 값의 주소를 바라보고 있었지만, 값이 15로 변경되었습니다. 메모리에 15라는 값을 찾고 없다면, 새로 만들어 저장합니다. 그 주소가 @5004가 되겠습니다.
다음에서는 데이터 영역에 20이라는 값이 없으므로 마찬가지로 새로운 공간 @5005에 저장하고, 그 주소를 든 채로 변수 영역에서 obj2를 찾고(@1004), obj2의 값인 @5002가 가리키는 변수 영역에서 다시 c를 찾아(@7103) 그곳에 @5005를 대입합니다.
주소 | 1001 | 1002 | 1003 | 1004 | 1005 | ... |
---|---|---|---|---|---|---|
데이터 | 이름: a | 이름: b | 이름: obj1 | 이름: obj2 | ||
값: @5001 | 값: @5004 | 값: @5002 | 값: @5002 |
우리가 중요하게 봐야할 것은 위의 내용입니다. 이렇게 숫자와 객체가 담긴 변수를 각각 복사하고 값을 변경하였을 때의 결과입니다. 변수 a와 b는 서로 다른 주소를 바라보게 됐으나, 변수 obj1과 obj2는 여전히 같은 객체를 바라보고 있는 상태입니다. 이를 코드로 표현하면 다음과 같습니다.
a !== b
obj1 === obj2
이 결과가 바로 기본형과 참조형 데이터의 가장 큰 차이점입니다. 이 결과는 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있는 것입니다.
그렇다면, 이번에는 객체의 프로퍼티를 변경하는 것이 아니라 변수의 값(b)를 직접 변경할 때와 같이 객체 자체를 변경해보도록 합시다.
var a = 10;
var b = a;
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;
b = 15;
obj2 = { c: 20, d: 'ddd' };
이번에는 단순히 객체 프로퍼티만을 변경하는 것이 아니라 객체 자체를 변경하게 될 때의 차이를 살펴보도록 하겠습니다.
주소 | 1001 | 1002 | 1003 | 1004 | ... |
---|---|---|---|---|---|
데이터 | 이름: a | 이름: b | 이름: obj1 | 이름: obj2 | |
값: @5001 | 값: @5004 | 값: @5002 | 값: @5006 |
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 | ... |
---|---|---|---|---|---|---|---|
데이터 | 10 | @ 7103 ~ ? | 'ddd' | 15 | 20 | @8204 ~ ? |
주소 | 7103 | 7104 | ... | 8204 | 8205 |
---|---|---|---|---|---|
데이터 | 이름: c | 이름: d | ... | 이름: c | 이름: d |
값: @5005 | 값: @5003 | ... | 값: @5005 | 값: @5003 |
위와 같이 객체 자체를 변경했을 때는 기본형 데이터와 같이 값이 변경되었음을 확인할 수 있습니다. 즉, 참조형 데이터가 '가변값'이라고 설명할 때의 '가변'은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립합니다.
불변 객체는 최근의 React, Vue.js, Angular 등의 라이브러리나 프레임워크에서뿐만 아니라 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요한 기초가 되는 개념입니다.
바로 위에서 언급했듯이 참조형 데이터의 '가변'은 데이터 자체가 아니라 내부 프로퍼티를 변경할 때만 성립합니다. 데이터 자체를 변경하고자 하면 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않습니다. 그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있을 것입니다.
그럼 어떤 상황에서 불변 객체가 필요할까요? 값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우가 종종 발생합니다. 바로 이럴 때 불변 객체가 필요합니다.
var user = {
name: 'Jaenam',
gender: 'male'
};
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender;
};
};
var user2 = changeName(user, 'Jung');
console.log(user.name, user2.name) // Jaenam, Jung
console.log(user === user2) // false
changeName
함수가 새로운 객체를 반환하도록 수정했습니다. 이제 user와 user2는 서로 다른 객체이므로 안전하게 변경 전과 후를 비교할 수 있습니다. 다만 아직 미흡한 점이 보입니다. changeName
함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티(gender)를 하드코딩으로 입력했습니다.
이런 식으로는 대상 객체에 정보가 많을수록, 변경해야할 정보가 많을수록 사용자가 입력하는 수고가 늘어날 것입니다. 이런 방식보다는 대상 객체의 프로퍼티 개수에 상관 없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것입니다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
위 함수는 for in 문법을 이용해 result
객체에 target
객체의 프로퍼티들을 복사하는 함수입니다. 몇 가지 아쉬운 점은 있지만, 위의 user
객체에 대해서는 문제가 발생하지 않습니다.
var user = {
name: 'Jaenam',
gender: 'male'
};
var user2 = copyObject(user);
user2.name = 'Jung'
console.log(user.name, user2.name) // Jaenam, Jung
console.log(user === user2) // false
copyObject
함수를 통해 간단하게 객체를 복사하고 내용을 수정하는 데 성공했습니다. 협업하는 모든 개발자들이 user 객체 내부의 변경이 필요할 때는 무조건 해당 함수를 사용하기로 합의하였습니다. 이 전제 하에서는 user 객체가 불변 객체라고 볼 수 있습니다.
그렇지만 모두가 그 규칙을 지키리라는 인간의 신뢰에만 의존하는 것은 살얼음 판을 걷는 것과 같습니다.
이러한 아쉬움에 더해 copyObject
는 간단한 만큼 분명 아쉬운 점이 많습니다. 무엇보다도 '얕은 복사만을 수행한다'는 부분이 가장 아쉬운데, 이 부분을 보완하는 내용을 다뤄보겠습니다.
얕은 복사는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다. 위의 copyObject
함수는 얕은 복사만을 수행했습니다. 이 말은 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때, 그 주솟값만 복사한다는 의미입니다.
그러면 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 됩니다. 사본을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀝니다.
var user = {
name: 'Jaenam',
urls: {
portfolio: 'https://github.com/orosy',
blog: 'https://blog.com'
}
};
var user2 = copyObject(user); // 얕은 복사
user2.name = 'Jung';
console.log(user.name === user2.name); // false
user.urls.porfolio = 'https://portfolio.com';
user.urls.blog = '';
console.log(user.urls.portfolio === user2.urls.portfolio) // true
console.log(user.urls.blog === user2.urls.blog) // true
이처럼 user 객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 urls의 내부 프로퍼티들을 기존 데이터를 그대로 참조하는 것입니다. 이런 현상이 발새하지 않게 하려면 user.urls 프로퍼티에 대해서도 불변 객체로 만들 필요가 있습니다.
var user2 = copyObject(user);
user.urls = copyObject(user2.urls);
이처럼 코딩을 해주면 될 것 같습니다. 그러니까 어떤 객체를 복사할 때 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때, 객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사하면 되지만, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 합니다. 이 과정을 참조형 데이터가 있을 때마다 재귀적으로 수행해야만 비로소 깊은 복사가 되는 것입니다.
이 개념을 바탕으로 copyObject 함수를 깊은 복사 방식으로 고친 코드는 다음과 같습니다.
var copyObjectDeep = function(target) {
var result = {};
if (typeof target === 'object' && target !== null) {
for (var prop in target) {
result[prop] === copyObjectDeep(target[prop]);
}
} else {
result = target;
}
return result;
}
3번째 줄에서 target이 객체인 경우에는 내부 프로퍼티를 순회하며 copyObjectDeep ㅎ마수를 재귀적으로 호출하고, 객체가 아닌 경우에는 8번째 줄에서 target을 그대로 지정하게끔 했습니다. 이 함수를 사용해 객체를 복사한 다음에는 원본과 사본이 서로 완전히 다른 객체를 참조하게 되어 어느 쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않습니다.
이처럼 참조형 데이터를 가변값으로 여겨야 하는 상황임에도 이를 불변값으로 사용하는 방법에 대해 알아봤습니다. 위의 경우는 내부 프로퍼티들을 일일이 복사하는 깊은 복사에 대한 예시였습니다. 또한 이뿐만 아니라 라이브러리를 사용하는 방법도 있습니다.
얕은 복사와 깊은 복사는 최근 자바스크립트에서 가장 중요한 개념 중의 하나이므로 데이터 타입에 따라 생기는 복사에 대해 알고, 이에 따른 결과로 '불변성', '가변성'을 이해하면 자연스럽게 얕은 복사와 깊은 복사를 이해하실 수 있습니다.
저 또한 '코어 자바스크립트'를 오랜 시간 정독하며 100%까진 아니라도 약 90% 정도는 이해할 수 있었던 것 같습니다. 저보다 깊은 이해를 원하신다면, 꼭 해당 책을 추천드리도록 하겠습니다.
코어 자바스크립트에 대해 정리하는 것은 꽤나 오랜 시간이 걸리기 때문에 위코드 과정을 시작하는 다음 주부터는 불가능할 것 같네요. 이제부터는 위코드 과정을 진행하며, 제가 매일매일 배우는 것에 대한 정리 느낌으로 진행될 것 같습니다.
코어 자바스크립트는 데이터 타입, 실행 컨텍스트, this, 콜백 함수, 클로저, 프로토 타입 등의 6개의 챕터로 이루어져 자바스크립트에서 꼭 알아야만 중급 개발자로 넘어갈 수 있는 개념을 수록했기 때문에 책을 읽어보시면서 개인적으로 정리하시면 자바스크립트에 대한 더 깊은 이해를 갖게 되실 것입니다.
그럼 저는 다음부터는 위코드의 생생한 후기와 학습 정리로 찾아뵙도록 하겠습니다. 그 전에 아마 'Diary' 시리즈도 나올 예정이므로 기대해주시기 바라겠습니다. 감사합니다. 🤠