변수의 값 할당
변수에 값을 할당할 때는 메모리 주소를 가리키는 것(참조) 하는 것이다.
let hello = "Hello Jwu World!"
위의 코드는 Hello Jwu World!
라는 값이 메모리에 생기고, hello
라는 변수가 이 메모리의 주소를 가리키는 것이다.
원시타입과 객체타입
자바스크립트에서 제공하는 7가지(숫자, 문자열, 불리언, null, undefined, Symbol, 객체 타입) 타입은 크게 원시타입
과 객체타입
으로 구분 지을 수 있다.
원시 타입은 불변의 값이다. 이에 반해 객체 타입은 변경 가능한 값이다.
원시 타입은 변수에 할당 시 메모리에 실제 값이 저장된다. 이에 반해 객체 타입은 메모리에 참조 값이 저장된다.
원시 값을 갖는 변수를 다른 변수에 할당 시 원본의 원시 값이 복사된다. 이에 반해 객체 타입은 참조 값이 복사 된다.
원시타입
위처럼 크게 3가지정도로 나뉘어 봤는데 아직은 이해하기 힘들다.
원시 타입은 불변의 값이라는데 원시 타입으로 저장한 값은 변경 불가능한 값이라는 걸까?
값을 변경할 수는 없지만 재할당으로 바뀐 것 처럼 보이게는 할 수 있다.
말이 어려운데.. 직접 해보자!
var score = undefined;
score = 80;
score = 90;
// score = 90;
위와 같은 그림으로 설명할 수 있다.
- 변수 선언 시 새로운 메모리 공간에 자리를 만든다. 하지만 값이 없기에 undefined로 저장
- 값을 할당 했을 때 같은 메모리 공간에서 undefined → 80 으로 바뀌는게 아닌 새로운 메모리 공간을 만들고 80이라는 값을 할당한다. 이후에 score가 가리키던 메모리주소를 변경한다.
- 변수 재할당 시에도 같다. 겉으로 보기에는
score = 80
→score = 90
으로 바뀐 것 같지만 실제로 내부에서는 새로운 메모리 공간이 할당되어, 값이 바뀐 것이 아닌 score라는 변수가 가리키는 것이 새로운 메모리 공간으로 바뀌는 것이다.
값의 이러한 특성을 불변성
이라고 한다.
잉.. 불변성을 지키는 것이 뭘까? 중요할까??
불변성을 지키지 못한다면 데이터가 어디서 어떻게 흘러가는지 쫓기 어렵고 상태변경을 추적하기 힘들다. 이는 버그로 이어지게 된다. 불변성을 지킨 코드는 다른 사람이 보았을 때도 코드를 읽는 흐름을 따라가 이해하는 것을 돕는다.
내가 아닌 다른사람이 보더라도 쉽게 이해할 수 있는 코드를 작성하는 것이 개발자의 목표라고 생각하는데, 그 과정중에 불변성이 포함되는 것이다!
그러면 다시 아까 그림으로 올라가서, 여기서 궁금한게 생긴다.. 메모리 안의 값인 undefined와 80은 어떻게 되는 걸까?
자바스크립트와 같은 고수준 언어들은 가비지 컬렉션(GC)
이라는 자동 메모리 관리 방법을 사용한다. 가비지 컬렉터의 목적은 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않게 됐는지 판단하여 회수한다. 자동으로 하기에 메리트가 있지만, 개발자가 수동으로 하는 방법에 비해 궁극의 방법은 아니다.
따라서 더 이상 어디에도 참조하지 않는 undefiend와 80이 저장된 메모리는 가비지 컬렉터에 의해 자동으로 수거된다.
객체 타입
자바스크립트 객체는 프로퍼티 키를 인덱스로 사용하는 해시 테이블이라고 생각할 수 있다. 대부분의 JS 엔진은 해시 테이블과 비슷하지만 높은 성능을 위해 더 나은 방법으로 객체를 구현한다고 한다.
객체(참조) 타입의 값, 객체는 변경 가능한 값이다.
변수에 객체를 할당해보자.
var person = {
name: "Jwu"
}
객체의 동작방식은 원시타입과 다르다. 변수엔 객체가 그대로 저장되는게 아닌 객체가 저장되어 있는 메모리 주소인 객체에 대한 참조값이 저장된다.
따라서 객체가 변수를 복사할 땐 참조 값이 복사되고 객체값이 복사되진 않는다.
var person= {
name: "Jwu"
}
var admin = person;
// 같은 메모리 주소를 가리킨다. 참조 값이 복사됨.
원시 값은 변경 불가능한 값이므로 값을 변경하려면 재할당 외에는 방법이 없다. 하지만 객체는 변경 가능하다. 따라서 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있다.
person.name = "Park"; // 프로퍼티 값 업데이트
person.address = "Seoul"; // 프로퍼티 동적 생성
console.log(person); // {name: "Park", address: "Seoul"}
객체를 할당한 변수에는 재할당하지 않았으므로 새로운 메모리를 재할당할 필요 없이 객체를 할당한 변수의 참조값은 변하지 않는다.
엇.. 근데 원시값과 객체값은 왜 차이가 있는걸까?
객체를 생성하고 관리하는 방식은 매우 복잡하고 비용이 많이 든다! 객체를 변경할 때 원시 값처럼 이전 값을 복사해 새롭게 생성한다면 불변성으로 인해 눈에 확연히 보이고 신뢰성이 확보 되겠지만, 객체의 크기는 매우 클수도 있고 원시 값 처럼 일정하지 않기에 복사해서 생성하는 비용이 많이 든다. 다시 말해 이러한 방법은 메모리의 효율적 소비가 어렵고 성능이 나빠진다..
따라서 메모리를 효율적으로 사용하기 위해, 객체를 복사해 생성하는 비용을 절약해 성능을 향상시키기 위해 객체는 구조적인 단점을 감안한 설계라고 한다.
원시 값이 복사될 경우 불변성으로 인해 흐름을 쉽게 파악할 수 있는 반면에 객체가 마구마구 복사될 경우 흐름을 찾기 어렵다. 그 이유는 여러 개의 식별자가 하나의 객체를 공유할 수도 있기 때문이다!
공유하게 되면 문제가 생길 수 있다.
var person = {
name : "Lee"
}
// copy와 person은 같은 객체를 참조한다. (얕은 복사)
var copy = person;
// copy를 통해 객체를 변경한다.
copy.name = "Park";
// person을 통해 객체를 변경한다.
person.address = "Seoul";
// copy와 person은 동일한 객체를 가리킨다.
// 서로에게 영향을 주고 받는다.
console.log(person); // {name: "Park", address: "Seoul"}
console.log(copy); // {name: "Park", address: "Seoul"}
얕은 복사와 깊은 복사
객체안의 객체 즉, 객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 깊이가 1까지 복사된 것을 얕은 복사
라고 하며, 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 깊은 복사
라고 한다.
다만, 원시값을 할당한 변수를 다른 변수에 할당하는 것에 대한 행동을 깊은 복사
, 객체를 할당한 변수를 다른 변수에 할당하는 것을 얕은 복사
라고 부르는 경우도 있다.
얕은 복사
// 얕은 복사
const o = { x: { y: { z: 1 } } };
const abc = { ...o };
console.log(abc === o); // false
console.log(abc.x === o.x); // true
console.log(abc.x.y === o.x.y); // true
객체의 얕은 복사의 경우.. 1단계의 깊이 비교에는 false라고 뜨며 복사가 잘 되었다. 하지만 2단계부터의 깊이는 참조값을 서로 같은 것을 가리키며 복사가 되지 않았다!
깊은 복사
깊은 복사에는 여러가지 방법이 있다.
1. JSON 객체의 메소드를 이용
const original = {
a: 1,
b: {
c: 2,
},
}
const copied = JSON.parse(JSON.stringify(original));
original.a = 1000
original.b.c = 2000
console.log(copied.a) // 1
console.log(copied.b.c) // 2
얕은 복사와 달리 값이 바뀌어도 각자 다른 곳을 참조함.
JSON.stringfy
는 자바스크립트 객체를 JSON 문자열로 변환한다.
JSON.parse
는 JSON문자열을 자바스크립트로 다시 변환한다.
JSON 으로 변환 했다가 다시 객체로 변환하기에 객체에 대한 참조가 없어졌지만 이 방법에는 문제가 있다고 한다.
다른 깊은 복사 방법에 비해 성능적으로 느리며, JSON.stringfy
메서드가 객체 안의 함수까지 복사하진 못해 그것을 undefined
으로 처리한다고 한다.. ㅠㅠ
2. Lodash의 deepclone 함수 사용
// npm install lodash로 설치, node.js의 환경에서 실행해야 한다.
const lodash = require("lodash");
const original = {
a: 1,
b: {
c: 2,
},
d: () => {
console.log("hi")
},
}
const deepCopied = lodash.cloneDeep(original);
original.a = 1000
original.b.c = 2000
console.log(deepCopied.a) // 1
console.log(deepCopied.b.c) // 2
console.log(deepCopied.d()) // 'hi'
Lodash는 많은 사람들이 사용해오고 안정성이 증명된 라이브러리다.
그 중 하나인 deepclone
메소드를 사용하면 따로 구현을 안해도 쉽게 깊은복사 가능!
3. 재귀적으로 직접 구현하기
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj
}
const result = Array.isArray(obj) ? [] : {}
for (let key of Object.keys(obj)) {
result[key] = deepClone(obj[key])
}
return result
}
const original = {
a: 1,
b: {
c: 2,
},
d: () => {
console.log("hi")
},
}
const copied = deepClone(original)
original.a = 1000
original.b.c = 2000
original.d = () => {
console.log("bye")
}
console.log(copied.a) // 1
console.log(copied.b.c) // 2
console.log(copied.d()) // 'hi'
🍚 References
https://sustainable-dev.tistory.com/156 https://junwoo45.github.io/2019-09-23-deep_clone/
https://ko.javascript.info/object-copy
https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management
'개발 (Dev) > Javscript' 카테고리의 다른 글
[JSON.stringify] JSON -> String 으로 정렬하기 (0) | 2024.02.19 |
---|