신비한 개발사전
객체와 참조 본문
한 변수에 저장된 객체(array 또는 object)를 다른 변수에도 저장할 땐 유의해야 할 점이 있다. 단순한 대입 연산자로 변수 x 자체를 변수 y에 할당해주면, 변수 x와 y는 같은 메모리 공간에 저장된 데이터를 불러오게 된다. 새로운 메모리 공간에 객체를 저장한 것이 아닌, 기존과 동일한 메모리에서 데이터를 불러오는 변수만 하나 더 생긴 것이다.
let x = [1, 2, 3];
let y = x;
console.log(x); // [1, 2, 3]
console.log(y); // [1, 2, 3]
y[0] = 5;
console.log(x); // [5, 2, 3]
console.log(y); // [5, 2, 3]
위 예시에서 y 변수에 [1, 2, 3]의 값을 가진 x를 저장했다. 콘솔창에 값을 찍어보면 y도 [1, 2, 3] 배열을 저장하고 있는 것을 확인할 수 있다.
하지만 y의 배열의 0번째 인덱스에 다른 값을 넣어준 뒤 콘솔창에서 x와 y의 값을 확인해보면, x가 가진 배열의 0번째 인덱스 값도 바뀌어있는 것이 보인다. 변수 x와 y는 둘 다 메모리 내 같은 저장 공간에 있는 배열을 참조하고 있다는 뜻이다.
저장한 값이 기본형 데이터 타입이라면 위와 같은 문제는 발생하지 않는다. 아래처럼 변수 y에 1이라는 숫자 값을 가진 x를 저장했을 때, y의 값을 5로 바꿔도 x의 값은 그대로 1로 남아있다.
let x = 1;
let y = x;
console.log(x); // 1
console.log(y); // 1
y = 5;
console.log(x); // 1
console.log(y); // 5
이런 차이가 발생하는 이유는 기본형 데이터 타입은 값 자체가 변수에 할당되는데, 객체의 경우 값이 아닌 그 값이 저장된 메모리 주소가 변수에 할당되기 때문이다. 이렇게 한 변수를 다른 변수명에 저장했을 때 값이 복사되는 경우를 "값에 의한 전달"이라 하고, 해당 값에 접근할 수 있는 주소만 복사되는 것을 "참조에 의한 전달"이라고 부른다.
참조에 의한 전달을 피하고 싶으면 아예 새로운 객체를 생성해야 하는데, 이 과정을 쉽게 만들어주는 연산자가 있다.
스프레드 연산자
스프레드(spread) 연산자는 한 변수의 객체를 참조 없이 다른 객체에 저장할 수 있게 해주는 연산자다. 변수명을 통해 값을 전달할 때, 변수명 앞에 ...를 붙인 후 새 객체를 생성할 때처럼 대괄호([ ])나 중괄호({ }) 안에 넣어 사용한다. 참조에 의한 전달로 인해 같은 배열을 참조하게 된 위 예시에서 스프레드 연산자를 사용하면, 아래와 같이 x의 값이 y의 변경에 영향받지 않는다는 것을 확인할 수 있다.
let x = [1, 2, 3];
let y = [...x];
console.log(x); // [1, 2, 3]
console.log(y); // [1, 2, 3]
y[0] = 5;
console.log(x); // [1, 2, 3]
console.log(y); // [5, 2, 3]
하지만 스프레드 연산자는 완벽한 해결책이 아니다. 객체 안에 또다른 객체가 네스팅되어 있을 경우, 네스팅된 객체는 스프레드 연산자의 도움을 받지 못한다.
아래 예시에서는 2번 인덱스에 숫자가 아닌 배열을 저장했다. 스프레드 연산자로 x의 값을 y에 복사한 후에, y의 배열 안에 숫자 타입으로 저장되어 있던 값과 객체 타입으로 저장되어 있던 값을 바꿔보았다.
let x = [1, 2, [3, 4]];
let y = [...x];
console.log(x); // [1, 2, [3, 4]]
console.log(y); // [1, 2, [3, 4]]
y[0] = 5;
y[2][0] = 6;
console.log(x); // [1, 2, [6, 4]]
console.log(y); // [5, 2, [6, 4]]
숫자 타입의 데이터를 갖고 있었던 0번 인덱스의 값을 바꿨을 땐 y에만 적용됐지만, 2번 인덱스에 네스팅된 배열에 들어있는 값을 바꾸니 x에도 동일한 위치의 값이 바뀌어버렸다. 네스팅된 객체들은 여전히 참조에 의한 전달로 값이 복사된다는 뜻이다. 스프레드 연산자를 사용해 새로운 메모리 공간에 데이터를 복사해도, 네스팅된 객체가 여전히 같은 메모리 주소를 참조하고 있는 것을 얕은 복사(shallow copy)라고 한다.
깊은 복사
네스팅된 객체까지 참조에 의한 전달로부터 자유롭게 하기 위해서는 깊은 복사(deep copy)가 필요하다. 깊은 복사는 별도로 빌트인된 함수가 존재하지 않아서 직접 함수를 만들거나 JSON 메소드를 사용해야 한다.
JSON이란 자바스크립트의 객체와 비슷한 형태로 데이터를 저장할 수 있는 일종의 데이터 포맷인데, JSON.stringify()라는 함수로 객체를 문자 타입으로 변환시키거나 JSON.parse() 함수로 객체의 문법으로 이루어진 문자를 다시 객체로 파싱하는 것을 가능케 한다. JSON.stringify()로 먼저 문자로 한 번 변환시킨 뒤에 JSON.parse()로 다시 객체로 되돌리면 네스팅된 객체의 참조를 끊을 수 있다.
let y = JSON.parse(JSON.stringify(x)); // 깊은 복사
다만 JSON.stringify()-JSON.parse()를 모든 상황에서 사용할 수 있는 것은 아니다. 객체에 JSON이 지원하지 않는 데이터 타입(함수, undefined 등ㅡ대신 null은 지원)이 있으면 stringify할 수 없으니, JSON 메소드로 깊은 복사를 하고 싶다면 객체가 저장하고 있는 데이터를 확인해볼 필요가 있다.