likeornament 님의 블로그
왜 자바스크립트는 깊은 복사를 기본으로 제공하지 않을까? 본문
자바스크립트의 불변성에 대해 공부하던 중 한 가지 궁금증이 떠올랐다.
"왜 자바스크립트는 객체를 복사할 때 깊은 복사를 기본으로 제공하지 않을까?"
이 질문에 답하기 위해,
먼저 자바스크립트의 불변성에 대해 살펴보자.
불변성 (immutability)
불변성이란 한 번 생성된 데이터는 그 메모리 자체를 변경할 수 없다는 원칙을 의미한다.
자바스크립트에서 데이터의 타입은 기본형 데이터와 참조형 데이터로 나눌 수 있는데 각각의 특성이 다르다.
기본형 데이터와 불변성
기본형 데이터(number, string, boolean 등)는
값 자체가 데이터 영역에 저장되며 한 번 생성한 값은 변경할 수 없다.
새로운 값을 할당하면 기존 값을 수정하는 것이 아니라,
완전히 새로운 값이 생성되고 변수는 그 주솟값을 다시 가리키게 된다.
let a = 1;
let b = 1;
b = 2;
이 코드는 내부적으로 다음과 같이 동작한다.
1. 데이터 영역에서 1 을 찾고, 없다면 새로 생성한 뒤 그 주소를 a에 할당한다.
2. 데이터 영역에서 다시 1 을 찾는다. 이미 존재하므로 동일한 주소를 b에 할당한다.
3. 데이터 영역에서 2 를 찾고, 없다면 새로 생성한 뒤 그 주소를 b에 할당한다.
결과적으로 1 과 2 는 모두 변경되지 않는 값이며,
이것이 기본형 데이터가 불변이라고 불리는 이유다.
참조형 데이터와 가변성
반면 객체와 같은 참조형 데이터는 동작 방식이 다르다.
객체는 값이 담긴 주소를 복사하지 않고,
값들이 담긴 주소들의 묶음을 가리키는 주소를 복사한다.
let obj1 = { a: 1, b: "b" };
let obj2 = obj2;
obj2.a = 2;
이 경우 obj1과 obj2는 동일한 객체를 참조하고 있으므로,
obj2.a를 수정하면 obj1.a 역시 함께 변경된다.
객체의 주솟값은 유지된 채 내부 프로퍼티만 변경되기 때문에,
이러한 성질을 객체의 가변성이라고 부른다.
하지만 아래와 같이 새로운 객체를 할당하면 이야기가 달라진다.
obj2 = { a: 1, b: "b" };
이 경우에는 기존 객체를 수정한 것이 아니라
완전히 새로운 객체를 만들어 재할당한 것이므로, 참조형 데이터의 값 자체가 변경된다.
즉, 참조형 데이터가 가변값이라고 할 때의 '가변'이란
데이터 자체를 교체하는 경우가 아니라 내부 프로퍼티를 수정하는 경우에만 성립한다.
얕은 복사와 깊은 복사
그렇다면 객체의 내부 프로퍼티를 수정해야 할 때마다
항상 새로운 객체를 만들어 재할당한다면 참조형 데이터인 객체 역시 불변성을 유지할 수 있을 것이다.
여기서 등장하는 개념이 얕은 복사와 깊은 복사다.
- 얕은 복사: 바로 아래 단계의 값만 복사
- 깊은 복사: 내부의 모든 값들을 전부 복사
얕은 복사의 한계
let profile = {
name: 'Kim',
links: {
hompage: 'https://homepage.com',
blog: 'https://likeornament.tistory.com',
github: 'https://github.com/coral0723'
}
};
let copiedProfile = { ...profile };
스프레드 연산자를 사용해 profile 객체를 copiedProfile에 얕은 복사를 수행했다.
copiedProfile.name = "Park";
console.log(profile.name === copiedProfile.name); // false
최상위 프로퍼티인 name을 수정하면 새로운 값으로 분리된다.
하지만 중첩된 객체는 다르다.
profile.links.homepage = 'https://new.hompage.com';
console.log(profile.links.homepage === copiedProfile.links.homepage); // true
두 단계 아래에 있는 links.homepage를 수정하면 두 값은 여전히 같은 값을 참조하고 있다.
이는 얕은 복사는 최상위 객체만 새로 만들고,
그보다 깊은 단계의 프로퍼티는 기존 데이터를 그대로 참조하기 때문이다.
따라서 객체 내부의 모든 값을 완전히 복사하기 위해서는 깊은 복사가 필요하다.
여기까지 살펴본 내용을 통해,
참조형 데이터의 구조상 얕은 복사만으로는 불변성이 깨질 수 밖에 없다는 점은 이해했다.
하지만 여기서 한 가지 의문이 남는다.
"왜 자바 스크립트는 기본 복사 연산에서 깊은 복사를 제공하지 않을까?"
스프레드 연산자는 항상 얕은 복사로 동작하며,
깊은 복사가 필요한 경우에는 개발자가 한 단계씩 직접 복사해야 한다.
이 질문을 곱씹다 보니,
'깊은 복사'라는 말 자체가 생각보다 모호하다는 느낌이 들었다.
"어디까지가 깊은가?"
const state = {
createdAt: new Date(),
onSuccess: () => console.log("done"),
cache: new Map([
["a", 1],
["b", 2],
]),
};
이 객체를 깊은 복사한다고 가정해 보자.
- Date 객체는 내부 타임스탬프까지 복사해야 할까?
- Function은 복사 대상일까, 아니면 그대로 공유해야 할까?
- Map과 Set은 내부 엔트리까지 전부 새로 만들어야 할까?
과연 자바스크립트는 어느 정도의 깊이까지 복사해야 할까?
const node = {};
node.self = node;
이런 구조를 깊은 복사하려고 하면 단순한 재귀 복사는 즉시 무한 루프에 빠진다.
이 문제를 해결하려면 방문한 객체를 기록하고,
동일한 참조를 다시 연결하는 별도의 로직이 필요하다
즉 깊은 복사는 단순히 구현이 어려운 문제가 아니라,
언어 차원에서 '어디까지를 같은 값으로 볼 것인지'를 정의해야 하는 문제다.
자동 깊은 복사의 숨겨진 비용
그렇다면 자바스크립트가 기본적으로 깊은 복사를 제공한다면 어떨까?
function updateState(state) {
const nextState = deepClone(state);
nextState.count += 1;
return nextState;
}
만약 깊은 복사를 실행하는 함수인 deepClone이 자바스크립트의 기본 동작이라면,
이 함수는 호출 될 때마다 객체의 크기와 깊이에 비례한 비용을 개발자가 인지하지 못한 채 지불하게 된다.
코드만 보면 단순한 값 증가처럼 보이지만,
실제로는 객체 전체를 재귀적으로 순회하는 비용이 매번 발생하는 셈이다.
문제는 이 비용이 코드에서 전혀 드러나지 않는다는 점이다.
자동 깊은 복사는 단순히 "느리다"라는 문제가 아니다.
비용이 코드에서 드러나지 않아 성능을 예측할 수 없게 만든다.
얕은 복사는 의도된 기본값
이 지점에서 관점이 바뀌기 시작했다.
얕은 복사는 "불완전한 복사"가 아닌
객체의 동일성을 유지한 채 전달하겠다는 의도된 기본값이다.
즉 자바스크립트는 깊은 복사를 지원하지 않는 불친절한 언어가 아니라,
비용이 큰 연산일 수록 명시적으로 드러내도록 설계된 언어에 가깝다는 것이다.
const settings = {
theme: { color: "dark" },
};
const nextSettings = { ...settings };
settings와 nextSetting은 서로 다른 객체지만,
theme은 여전히 같은 대상을 가리킨다.
이 동작은 얕은 복사는 객체를 완전히 분리하지 않고,
객체 그래프를 연결한 채로 전달하며 참조를 공유한다는 명확한 의미를 가진다.
조금 더 찾아보니,
이 참조 공유 특성이 자바스크립트에서 꽤나 많은 것을 가능하게 해준다.
1. 상태 관리
React를 비롯한 많은 상태 관리 방식은 객체의 참조가 바뀌었는지를 기준으로 변경 여부를 판단한다.
prevState === nextState;
만약 모든 복사가 기본적으로 깊은 복사였다면,
객체는 매번 새로운 참조를 갖게 되고 이 비교는 사실상 의미를 잃게 된다.
2. 동일성 비교
자바스크립트에서 객체 비교는 값 비교가 아니라 참조 비교다.
cache.get(key) === value;
얕은 복사는 "같은 객체를 공유하고 있다"는 사실을 유지한 채 객체를 전달할 수 있게 해준다.
3. 캐싱과 메모리 효율
모든 객체를 자동으로 깊은 복사한다면,
동일한 구조의 데이터가 메모리 곳곳에 중복 생성된다.
반면 얕은 복사는 필요한 부분만 새로 만들고 나머지는 기존 데이터를 재사용하기 때문에
이는 메모리 모델과 잘 맞는 방식이다.
결론
자바스크립트가 깊은 복사를 기본적으로 제공하지 않는 이유는 기술적인 한계 때문이 아니다.
'깊음'을 정의하기 어렵고, 자동화하면 비용이 숨겨지며, 객체의 동일성이 무너질 수 있기 때문이다.
자바스크립트는 개발자가 모르는 사이 "알아서 안전하게 처리해주는 언어"가 아니라,
- 어디까지를 같은 데이터로 볼 것인지
- 어디서부터 새로운 값으로 취급할 것인지
- 그에 따른 비용을 감수할 것인지
이 모든 선택을 개발자에게 명시적으로 요구하는 언어에 가깝다.
이러한 점을 이해하고 나니,
앞으로 자바스크립트를 공부할 때 그 설계 의도를 더 의식하며 바라볼 수 있을 것 같다.
'공부 > javascript' 카테고리의 다른 글
| 콜백은 왜 신뢰할 수 없는가 - 비동기 제어권의 문제 (1) | 2026.01.16 |
|---|---|
| this는 왜 스코프 규칙을 따르지 않을까 (0) | 2026.01.15 |
| VariableEnvironment는 왜 ‘쓰이지 않도록’ 설계되었을까 (0) | 2026.01.13 |
| [JAVASCRIPT] 호출 스택과 이벤트 루프(Call stack and Event loop) (0) | 2024.08.06 |
| [JAVASCRIPT] if문 중첩 제거하기 (0) | 2024.08.06 |