💻 프로그래밍

Immutable

불변

생성 후 변경할 수 없는 객체. 함수형 프로그래밍의 핵심 개념.

📖 상세 설명

Immutable(불변)은 한 번 생성된 후에는 절대 변경할 수 없는 데이터를 의미합니다. 불변 데이터를 수정하고 싶다면 기존 값을 변경하는 대신 새로운 데이터를 생성해야 합니다. JavaScript에서 문자열과 숫자 같은 원시 타입은 기본적으로 불변이며, 객체와 배열은 mutable(가변)이지만 불변적으로 다룰 수 있습니다.

불변성은 함수형 프로그래밍의 핵심 원칙 중 하나입니다. 데이터가 변경되지 않으면 프로그램의 상태를 추적하기 쉬워지고, 예상치 못한 부작용(side effect)을 방지할 수 있습니다. 특히 여러 곳에서 동일한 데이터를 참조할 때, 불변성을 유지하면 한 곳의 변경이 다른 곳에 영향을 미치는 버그를 원천적으로 차단합니다.

React, Redux 같은 현대 프론트엔드 라이브러리들은 불변성을 핵심 설계 원칙으로 채택하고 있습니다. React는 상태(state)의 참조가 변경되었는지를 비교하여 리렌더링 여부를 결정하므로, 불변적 업데이트가 필수입니다. 이를 통해 효율적인 변경 감지와 성능 최적화가 가능해집니다.

실무에서는 스프레드 연산자(...), Object.assign(), 배열 메서드(map, filter, concat) 등을 사용하여 불변성을 유지합니다. 복잡한 중첩 구조에서는 Immer나 Immutable.js 같은 라이브러리가 불변적 업데이트를 더 직관적으로 작성하도록 도와줍니다.

💻 코드 예제

// 1. 원시 타입은 기본적으로 불변
let str = "Hello";
str[0] = "h";  // 작동하지 않음
console.log(str); // "Hello" (변경되지 않음)

// 2. 객체의 불변적 업데이트 (스프레드 연산자)
const user = { name: "김철수", age: 25, city: "서울" };

// ❌ 잘못된 방법: 직접 수정 (mutable)
// user.age = 26;

// ✅ 올바른 방법: 새 객체 생성 (immutable)
const updatedUser = { ...user, age: 26 };
console.log(user.age);        // 25 (원본 유지)
console.log(updatedUser.age); // 26 (새 객체)

// 3. 배열의 불변적 업데이트
const numbers = [1, 2, 3, 4, 5];

// ❌ 잘못된 방법: push, splice 등은 원본을 변경
// numbers.push(6);

// ✅ 올바른 방법: 새 배열 생성
const added = [...numbers, 6];           // [1, 2, 3, 4, 5, 6]
const removed = numbers.filter(n => n !== 3); // [1, 2, 4, 5]
const doubled = numbers.map(n => n * 2);      // [2, 4, 6, 8, 10]

// 4. 중첩 객체의 불변적 업데이트
const state = {
    user: { name: "박영희", profile: { email: "yh@email.com" } },
    posts: [{ id: 1, title: "첫 글" }]
};

// 깊은 중첩 구조 업데이트 (번거롭지만 불변)
const newState = {
    ...state,
    user: {
        ...state.user,
        profile: { ...state.user.profile, email: "new@email.com" }
    }
};

// 5. Immer 라이브러리로 간단하게 (produce 함수)
// import { produce } from 'immer';
// const newState = produce(state, draft => {
//     draft.user.profile.email = "new@email.com"; // 마치 직접 수정하는 것처럼
// });

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"Redux 상태 업데이트할 때 불변성을 지켜야 합니다. 직접 state를 수정하면 리렌더링이 안 되니까, 스프레드 연산자로 새 객체를 만들어서 반환하거나 Immer를 사용하면 편합니다."
💬 면접에서
"React에서 불변성이 중요한 이유는 얕은 비교(shallow comparison)로 변경을 감지하기 때문입니다. 객체를 직접 수정하면 참조가 같아서 React가 변경을 인식하지 못하고, 새 객체를 만들어야 리렌더링이 트리거됩니다."
💬 코드 리뷰에서
"여기서 배열에 push를 쓰셨는데, 이러면 원본 배열이 변경됩니다. 불변성을 위해 스프레드 연산자로 `[...array, newItem]` 형태로 바꿔주세요. 나중에 디버깅할 때 상태 추적이 훨씬 쉬워집니다."

⚠️ 흔한 실수 & 주의사항

얕은 복사의 함정

스프레드 연산자는 1단계만 복사합니다. 중첩 객체는 여전히 같은 참조를 공유하므로, 깊은 중첩 구조를 수정할 때는 모든 레벨에서 새 객체를 생성해야 합니다.

배열 메서드 혼동하기

push, pop, splice, sort, reverse는 원본을 변경하는 mutable 메서드입니다. map, filter, concat, slice, toSorted, toReversed는 새 배열을 반환하는 immutable 방식입니다.

성능을 위한 구조적 공유

불변성이 매번 전체를 복사하는 것은 아닙니다. Immutable.js 같은 라이브러리는 구조적 공유(structural sharing)로 변경된 부분만 새로 만들어 메모리와 성능을 최적화합니다.

🔗 관련 용어

📚 더 배우기