불변성 유지하기
- 6.1 카피-온-라이트 원칙 세 단계
- 6.2 배열에 대한 카피-온-라이트
- 6.3 객체에 대한 카피-온-라이트
- 6.4 읽기와 쓰기 분류하기
- 6.5 얕은 복사와 구조적 공유
복사, 변경, 리턴 — 이 세 단계만 지키면 불변성이 보장돼.
카피-온-라이트의 세 단계는 정말 단순해. 복사본 만들기, 복사본 변경하기, 복사본 리턴하기. 이 세 단계를 지키면 원본 데이터는 절대 바뀌지 않아. 불변성이 보장되거든. 왜 이게 중요하냐면 — 불변 데이터는 공유해도 안전하기 때문이야. 여러 함수가 같은 데이터를 참조하고 있어도, 아무도 그 데이터를 바꾸지 않으니 서로 영향을 줄 수가 없어. 동시성 문제의 근본적 해결이지. 자바스크립트에는 불변 데이터 구조가 기본으로 없거든. 그래서 **규율(discipline)**으로 불변성을 유지해야 해. 카피-온-라이트가 바로 그 규율이야.
배열을 다루는 대표적인 연산들을 카피-온-라이트로 바꿔보면 패턴이 보여.
// 요소 추가
function add_element_last(array, elem) {
var new_array = array.slice(); // 복사
new_array.push(elem); // 변경
return new_array; // 리턴
}
// 요소 삭제
function remove_item_by_name(cart, name) {
var new_cart = cart.slice();
var idx = new_cart.findIndex(item => item.name === name);
if (idx !== -1) new_cart.splice(idx, 1);
return new_cart;
}
slice()로 복사하고, 복사본에서 변경하고, 리턴. 매번 같은 구조야. 자바스크립트의 배열 메서드 중 push(), pop(), splice(), sort() 같은 건 원본을 수정하는 메서드이니 주의해야 하고, slice(), concat(), map(), filter() 같은 건 새 배열을 리턴하니 안전해.
객체도 마찬가지야. Object.assign()이나 스프레드 연산자 {...obj}로 복사하고 수정하면 돼.
// 속성 변경
function set_price(item, new_price) {
var new_item = Object.assign({}, item);
new_item.price = new_price;
return new_item;
}
// 속성 삭제
function remove_field(object, field_name) {
var new_obj = Object.assign({}, object);
delete new_obj[field_name];
return new_obj;
}
객체 안에 배열이 있거나, 배열 안에 객체가 있는 중첩 구조에서는 주의가 필요해. Object.assign()과 slice()는 전부 **얕은 복사(shallow copy)**니까.
저자는 데이터 조작을 **읽기(read)**와 **쓰기(write)**로 분류하거든. 읽기는 데이터를 바꾸지 않고 정보를 얻는 것(cart.length, cart[0], get_total(cart))이고, 쓰기는 데이터를 바꾸는 것(cart.push(item), delete cart[0], cart[0].price = 10)이야. 카피-온-라이트의 핵심 규칙은 쓰기를 읽기로 바꾸는 것이야. 원본을 수정하는 쓰기를 복사본을 만들어서 수정하고 리턴하는 읽기로 바꾸면, 원본 입장에서는 아무 변경도 없으니 "읽기"가 되는 거지. 가끔 하나의 함수에서 읽기와 쓰기가 동시에 일어나는 경우가 있는데, 예를 들어 array.shift()는 첫 번째 요소를 제거하면서(쓰기) 그 값을 리턴(읽기)해. 이런 함수는 읽기와 쓰기를 분리하는 게 좋아.
slice()와 Object.assign()은 **얕은 복사(shallow copy)**야. 최상위 레벨만 새로 만들고, 중첩된 내부 객체는 원본과 같은 참조를 공유해.
var original = { name: "shirt", options: { color: "blue", size: "L" } };
var copy = Object.assign({}, original);
// copy.options === original.options → true! 같은 객체를 가리킴
이게 문제일 수도 있지만, 저자는 이걸 오히려 장점으로 봐. 이걸 **구조적 공유(structural sharing)**라고 하거든. 바뀌지 않은 부분은 원본과 공유하고, 바뀐 부분만 새로 만들어. 전체를 깊은 복사(deep copy)하는 것보다 메모리도 적게 쓰고 빠르다. name만 바꿨으니 name만 새로 만들고, options는 공유하는 거야. 중첩된 부분을 바꿔야 할 때만 그 경로에 있는 객체들을 새로 만들면 돼. 단, 구조적 공유가 안전한 건 모든 곳에서 카피-온-라이트를 지킬 때만이야. 누군가 공유된 내부 객체를 직접 수정하면 원본도 바뀌어버리거든. 규율이 중요해.
정리
6장 읽고 기억할 거 세 가지:
- 카피-온-라이트 세 단계: 복사, 변경, 리턴. 이 패턴만 지키면 불변성이 보장됨
- 쓰기를 읽기로 바꾸는 게 핵심. 원본 수정 대신 복사본 수정+리턴
- 얕은 복사 + 구조적 공유 = 효율적인 불변성. 전부 복사할 필요 없이 바뀐 부분만 새로 만들면 됨