상태 불변성

psi·2025년 3월 9일

상태 불변성

상태 불변성이란 한번 생성된 상태(state)를 직접 수정하지 않고, 상태를 변경해야 할 때는 항상 원본 상태를 기반으로 새로운 상태 객체를 생성하여 대체하는 프로그래밍 원칙.

JS는 기본적으로 어떠한 값을 저장할 때,
원시 타입의 경우 값이 콜 스택에 저장되고
참조 타입의 경우 실제 값은 메모리 힙, 그 값을 저장한 메모리 힙주소 값은 콜 스택에 저장된다.

  • 원시 타입 - String, Number, Boolean, Null, undefined 등
  • 참조 타입 - Array, Object, function 등

React에서의 불변성

React는 상태 변화 감지를 얕은 비교를 통해 수행하며,
변화 감지 기준은 콜 스택의 주소값입니다.
즉, [1,2,3] 배열(참조 타입)에서 0번째 index수를 4로 바꾸었다 하더라도 ( [4,2,3]) 메모리 힙에서의 값이 바뀔 뿐 콜 스택에서의 주소값은 변화가 없기 때문에 React는 변화를 감지하지 못합니다.

React가 얕은 비교를 통해 상태 감지를 하는 이유

  • 얕은 비교(Shallow comparison): 변수가 가르키는 주소값을 비교
  • 깊은 비교(Deep Compare): 변수가 가르키는 주소값 내부의 값을 비교

얕은 비교의 경우 주소값만을 비교하기 때문에 시간복잡도 O(1)의 시간이 소요되지만, 깊은 비교의 경우 주소값 내부의 값을 비교하기 때문에 O(N)의 시간이 소요되어 상대적으로 더 많은 시간이 걸린다.

원시타입 상태 변화 시

let A = "Hello" // 주소 AA : "Hello"
A = "Bye"		// 주소 BB: "Bye"

원시 타입의 경우 값 상태 변화시 새로운 주소값 생성 후 변화된 값을 저장하기 때문에 콜 스택에서 변화를 감지할 수 있다.

참조타입 상태 변화 시

let arr = [1,2,3] // 주소값 500 : [1, 2, 3]
arr[0] = 4        // 주소값 500 : [4, 2, 3]
arr = [...arr]    // 주소값 600 : [4, 2, 3]

참조 타입의 경우 값을 변화하면 주소값이 가지고 있는 값의 변화만 발생하여 콜 스택에서의 주소값은 변하지 않아 React에서 변화 감지를 하지못한다. 하지만 spread 연산을 통해 값을 새 배열을 생성하면 주소값이 변경되면서 상태 변화를 감지할 수 있다.

새로운 주소 값 생성 후, 값을 할당 받은 경우 이전에 사용했던 주소 값은 Garbage Collection(가비지 컬렉션)에 의해 불필요한 메모리 공간이 정리된다.

그렇기 때문에 React에서 참조 타입 수정 시 map, filter, reduce, spread 연산자 등을 활용해서 값을 수정한다.

// map을 사용하여 모든 요소를 2배로 만든 새 배열 생성
const doubled = numbers.map(num => num * 2);

// 배열에 새 요소 추가
const originalArray = [10, 20, 30];
const newArray = originalArray.concat([40, 50]);

// 특정 요소 업데이트
const tasks = [
  { id: 1, name: "운동", completed: false },
  { id: 2, name: "공부", completed: false },
  { id: 3, name: "청소", completed: true }
];

const updatedTasks = tasks.map(task => 
  task.id === 2 ? { ...task, completed: true } : task
);

React에서의 상태 변화 코드 방식

// 일반적인 방식
setCount(count + 1);

// 함수형 업데이트 (더 안전함)
setCount(prevCount => prevCount + 1);

React의 setState나 useState의 setter 함수가 함수형 업데이트를 지원하며, 이를 통해 이전 상태 기반으로 새 상태를 안전하게 생성할 수 있다

function handleClick() {
  setCount(count + 1); // count가 0이라면 1로 설정
  setCount(count + 1); // 여전히 count는 0이므로 1로 설정
  setCount(count + 1); // 여전히 count는 0이므로 1로 설정
}
function handleClick() {
  setCount(prevCount => prevCount + 1); // 0 -> 1
  setCount(prevCount => prevCount + 1); // 1 -> 2
  setCount(prevCount => prevCount + 1); // 2 -> 3
}
profile
사용자 경험을 최우선하며 논리적 문제 해결을 즐기는 개발자

0개의 댓글