Immutability
불변성은 한 번 만든 값을 그 자리에서 바꾸지 않는다는 약속입니다. 값을 수정하고 싶으면 원본을 덮어쓰는 대신, 바뀐 내용을 반영한 새 값을 만들어 돌려줍니다. 원본이 그대로 남아 있으니 같은 데이터를 여러 곳에서 함께 봐도 서로 발을 밟지 않습니다. 어느 시점에 어떤 값이었는지 추적하기 쉬워지고, 순수 함수도 훨씬 지키기 쉬워집니다.
▶아키텍처 다이어그램
🔄 프로세스 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
객체나 배열을 여러 함수가 같이 들고 있을 때, 누군가 조용히 내용을 바꿔 버리면 다른 쪽에서는 값이 '왜 갑자기 달라졌지?' 하는 순간을 맞게 됩니다. 이런 버그는 재현도 어렵고 원인도 찾기 힘듭니다. 언제 바뀌었는지, 누가 바꿨는지, 읽는 쪽이 잘못 본 건지 추적해야 할 범위가 너무 넓기 때문입니다. 멀티스레드 환경이라면 문제는 더 커져서 같은 값을 동시에 건드리는 순간 결과가 예측 불가능해집니다. 그래서 아예 '원본을 바꾸지 않는다'는 규칙으로 상태 변경의 난장을 줄이려는 게 불변성입니다.
명령형 언어에서 변수에 값을 대입하고 객체 속성을 바꾸는 방식은 너무 자연스러워서 오랫동안 기본값이었습니다. 하지만 싱글 페이지 애플리케이션처럼 상태가 복잡한 UI가 퍼지고, 서버 측에서도 동시성 처리가 일상이 되면서 '언제 무엇이 바뀌었는지'를 추적하는 비용이 빠르게 커졌습니다. React가 state 직접 수정 대신 setState를 요구하고, Redux가 리듀서에서 새 객체를 반환하도록 강제한 것은 이 흐름의 일부입니다. 값을 바꾸지 않으면 렌더링 여부를 참조 비교 하나로 판단할 수 있다는 이점도 큽니다. Clojure와 Scala 같은 언어는 아예 불변 컬렉션을 기본으로 제공해 업계에 '불변이 기본, 가변이 예외'라는 감각을 퍼뜨렸습니다.
불변성을 실천할 때 핵심은 '복사해서 수정한 새 값을 돌려준다'는 패턴입니다. 객체에서 한 필드만 바꾸고 싶다면 나머지 필드를 그대로 복사한 새 객체를 만들고, 배열에 항목을 추가하려면 기존 배열을 그대로 둔 채 새 배열에 요소를 덧붙여 반환합니다. 겉보기에는 매번 전체를 복사하는 것처럼 보이지만, Immutable.js나 언어 내장 자료구조는 구조 공유(Structural Sharing) 기법을 써서 바뀌지 않은 부분은 실제로 복사하지 않고 참조만 공유합니다. 덕분에 큰 객체에서 일부만 바꿔도 비용이 크게 늘어나지 않습니다. React는 이전 state와 새 state의 참조가 같은지만 비교해 리렌더링 여부를 결정하는데, 이는 불변성이 지켜져야 성립하는 최적화입니다.
가변 수정 vs 불변 업데이트
// 가변 방식 — 원본 수정
const user = { name: "준영", age: 30 };
user.age = 31; // 원본이 바뀜
// 불변 방식 — 새 객체 생성
const user = { name: "준영", age: 30 };
const updatedUser = { ...user, age: 31 };
// user는 그대로, updatedUser가 새 값
// 배열도 마찬가지
const items = [1, 2, 3];
items.push(4); // 가변: 원본 수정
const newItems = [...items, 4]; // 불변: 새 배열
// React state 업데이트
setUser({ ...user, age: 31 }); // 새 객체를 넘겨야 리렌더링 감지가변 방식은 원본을 직접 고치고, 불변 방식은 spread 연산자로 기존 값을 복사한 새 값을 만듭니다. React의 상태 업데이트가 불변 방식을 요구하는 이유는 참조 비교로 변경을 감지하기 때문입니다.
불변성과 순수 함수는 서로를 강화하는 관계입니다. 순수 함수는 '외부 상태를 바꾸지 않는다'를 함수 쪽에서 보장하고, 불변성은 '값 자체가 바뀌지 않는다'를 데이터 쪽에서 보장합니다. 가변 데이터 위에서 순수 함수를 지키려면 매번 주의해야 하지만, 불변 데이터를 쓰면 순수성이 자연스럽게 따라옵니다. 반대로 순수하지 않은 코드에서도 불변성만 적용할 수 있지만, 효과가 절반으로 줄어듭니다. 실무에서는 두 개념을 함께 가져가면서, 상태 변경이 정말 필요한 경계 영역만 가변으로 남겨 두는 방식이 자리 잡았습니다.
Gain 데이터가 언제 바뀌었는지 추적할 필요가 없어져 동시성 버그가 근본적으로 줄어들고, 참조 비교만으로 변경 감지가 가능해 React 같은 UI 프레임워크가 효율적으로 동작합니다. Cost 값을 바꾸고 싶을 때마다 새 객체를 만들어야 하니 코드가 조금 장황해지고, 큰 데이터 구조를 자주 갱신하는 경우 구조 공유가 없는 순수 복사는 메모리와 시간을 더 씁니다. 깊게 중첩된 객체의 내부를 업데이트하는 코드는 spread 연산자가 층층이 쌓여 읽기 어려워질 수 있어 Immer 같은 라이브러리의 도움이 필요합니다. Decision Scale 상태를 여러 곳이 공유하거나 변경 이력을 추적해야 하는 UI 상태 관리, 서버 상태 캐시, 동시성 환경에서는 불변성이 거의 필수적입니다. 반대로 짧은 수명의 지역 변수나 성능이 극단적으로 중요한 루프 내부 연산에서는 가변 업데이트가 여전히 더 실용적입니다.
불변성은 React, Redux, Vuex, Zustand 같은 상태 관리 도구에서 거의 기본 전제로 깔립니다. 함수형 언어의 컬렉션은 아예 불변을 기본값으로 두고, JavaScript에서는 Immer나 Immutable.js가 깊은 업데이트를 조금 덜 고통스럽게 만들어 줍니다. 실무에서는 '새 값을 어디서 만들지', '어디까지 복사할지'에 대한 감각이 중요합니다. 전체적으로는 불변 업데이트를 유지하되, 정말 성능이 민감한 좁은 구간만 예외로 두는 식의 균형을 많이 택합니다.