Conceptly
← 전체 목록
🔒

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가 깊은 객체 업데이트를 간결하게 만들어 줍니다. 실무에서 불변성을 잘 활용하려면 '어디서 새 값을 만들어야 하는가'와 '어디까지 복사할 것인가'에 대한 감각이 필요합니다. 전체를 불변으로 가져가되 성능이 문제가 되는 루프 안쪽만 예외로 두는 식의 균형이 일반적입니다. 불변성을 전면 도입했는데 느려졌다면, 구조 공유를 지원하는 자료구조로 바꾸는 것이 다음 단계입니다.

상태 관리동시성 안전undo/redo 구현디버깅