[10분 테코톡] 불변성

흑우·2023년 12월 13일

10분 테코톡 - 5기

목록 보기
16/16

불변성이라는 추상적인 단어를 이해하기 위해서는 JavaScript에서 Memory Heep과 Call Stack이 어떻게 변화하는지 알아야합니다.

원시 타입 vs 참조 타입

원시 타입 (난 불변해!)

  • Number, String, Boolean, Null, Undefined, Symbol

참조 타입 (난 변해!)

  • Object, Array, Function, Date, RegExp, Map, Set

Call Stack과 Memory Heap

let a = 10;
let b = [1, 2, 3];
let c = { name: "라잇", course: "FE"};

  • 원시 타입인 a는 Call Stack에 실제 값인 10이 담기게 되고, 참조 타입인 b, c는 Call Stack의 값이 메모리의 주소를 가르키고, Memory Heap 영역에 실제 값이 담기게 됩니다.

원시 타입의 변수 재할당

  • 처음 a와 b에 값을 할당하면 Call Stack에 값이 담기고 a와 b에 재할당을 진행하면 다음과 같이 10은 가비지 컬렉터의 대상이되고 Call Stack에 담기는 값은 변경됩니다.
  • 이때 Memory Heap에 담기는 값에 대한 변경은 없는데요. 이것이 바로 메모리 힙 영역의 값은 변경되지 않았다. == 불변하다 라고 표현합니다.

참조 타입의 변수 재할당

  • Memory Heap에 참조 타입 변수의 값이 저장되어 있는데요. push 메서드를 통해 배열을 변경하게 되면 Memory Heap애 있는 값이 변경됩니다.
  • 메모리 힙 영역의 값이 변경되었다! != 불변하지 않다(가변적이다)!

얕은 비교 vs 깊은 비교

const obj1 = { a: 1, b: 2};
const obj2 = { a: 1, b: 2};

console.log(obj1 === obj2); // false
console.log(JSON.strinify(obj1) === JSON.strinify(obj2)); // true
  • 여기서 Call Stack에 있는 값을 비교하는 것을 얕은 비교라고 하고, Memory Heap에 있는 값을 비교하는 것을 깊은 비교라고 합니다.
  • 얕은 비교를 했을 때는 false를 깊은 비교를 했을 때는 true를 반환합니다.

왜 React에서 불변성을 지켜야 하나요?

React의 상태 업데이트의 원리!

  1. setState
  2. 얕은 비교 -> 계산 리소스를 줄여줌.
  3. 콜 스택의 값이 이전 값과 다르다면 리렌더링을 진행

원시 타입의 setState

const PrimitiveTypeTest = () => {
	const [number, setNumber] = useState(0);
  
  	const increase = () => {
    	setNumber(number + 1);
    };
  
  	retrun (
    	<Container>
        	number: {number}
        	<Button onClick={increase}>증가하기</Button>
        </Container>
    )
}
  • 버튼을 클릭하면 정상적으로 state가 변경되고 컴포넌트가 리렌더링됩니다.

참조 타입의 setState

const PrimitiveTypeTest = () => {
	const [array, setArray] = useState([1,2,4]);
  
  	const increase = () => {
      	array.push(5)
    	setNumber(array);
    };
  
  	retrun (
    	<Container>
        	number: {number}
        	<Button onClick={increase}>증가하기</Button>
        </Container>
    )
}
  • array를 push를 통해 업데이트 해도 Call Stack의 값은 변경되지 않았기 때문에 React는 값이 변경했다는 것을 인지하지 못합니다. => 리렌더링 실패

React에서 setState할 때

  • 원시 타입을 쓸 때: 값을 바로 넣어줘도됨
  • 참조 타입을 쓸 때: 새로운 객체나 배열을 생성한 후 값을 넣어줘야함.
    • 즉, Call Stack의 값을 다르게 생성한 후 넣어줘야함
    • 즉, 메모리 힙 영역의 값은 변하면 안됨
    • 이는 불변성을 지켜야 하는 이유입니다.

React에서 불변성을 지키는 방법

E5

  • map, filter, slice, reduce 등 새로운 배열을 반환하는 메소드를 사용하기
  • splice, push는 원본 배열(메모리 영역)을 수정함

E6

  • spread operater [...numbers, 4] or {...obj} 새로운 배열이나 객체를 생성

ES2023

  • toReversed(), toSorted() 기존의 reverse(), sort()는 원본 배열을 변경했지만 해당 함수는 새로운 함수를 반환

ES2023

  • toReversed(): 배열을 역순으로 바꾸고 새로운 배열로 반환
const languages = ["Java", "Type", "Coffee"];
const reversed = languages.toReversed();
console.log(reversed);
// => ["Coffee", "Type", "Java"]
  • toSorted(): 배열을 정렬 후 새로운 배열로 반환
const languages = ["Java", "Type", "Coffee"];
const sorted = languages.toSorted();
console.log(sorted);
// => ["Coffee", "Java", "Type"]
  • toSpliced(): 배열을 자르거나 이어 붙여서 새로운 배열로 반환
const languages = ["Java", "Type", "Coffee"];
const spliced = languages.toSpliced(2, 1, 'Dart', 'Web');
console.log(sorted);
// => ["Java", "Type", "Dart", 'Web']
  • with(): 배열의 특정 인덱스 값을 수정
const languages = ["Java", "Type", "Coffee"];
const spliced = languages.with(2, 'apple');
console.log(sorted);
// => ["Java", "Type", 'apple']

참조 타입의 데이터가 2 depth 이상 일때는?

const twoDepthArray = [
	[1, 1, 1],
  	[2, 2, 2],
  	[3, 3, 3],
]

const copiedArray = [...twoDepthArray];
console.log(twoDepthArray === copiedArray); // 1. false
console.log(twoDepthArray[0] === copiedArray[0]); // 2. true
  • 스프레드 연산자로 배열을 복사해도 1 depth까지는 정상적으로 복사가 되지만 2 depth 이후에는 참조 값을 복사하기 때문에 정상적인 복사가 이루어지지 않습니다.
  • 이럴 때는 얕은 복사가 아닌 깊은 복사를 통해 새로운 참조 타입 변수를 생성해야 합니다. => JSON.parse(JSON.stringify()), 커스텀 재귀함수 사용, lodash의 cloneDeep() 사용
  • 이러한 것들은 너무 귀찮기 때문에 제가 structuredClone() 이라는 DOM API 메서드를 소개해드릴려고 합니다.
const original = { name: 'MDN'};
original.itself = original;

const clone = structuredClone(original);

console.log(clone !== original);

console.log(clone.name === 'MDN')
console.log(original.itself === clone)
  • structuredClone 메서드는 인자에 복사하고 싶은 객체를 전달해주기만 하면 간편하게 깊은 복사를 진행할 수 있습니다.
const twoDepthArray = [
	[1, 1, 1],
  	[2, 2, 2],
  	[3, 3, 3],
]

const copiedArray = structuredClone(twoDepthArray);
console.log(twoDepthArray === copiedArray); // 1. false
console.log(twoDepthArray[0] === copiedArray[0]); // 2. false
  • 아까 코드에 적용하면 깊은 복사가 제대로 이루어졌기 때문에 새로운 Call Stack의 값을 가지게되고 false가 출력됩니다.

정리

  • 불변성이란 메모리 영역의 값이 변하지 않는 것을 의미합니다.
  • 리액트는 얕은 비교를 통해 상태를 업데이트 합니다.
  • 참조 타입의 setState시에는 새로운 배열이나 객체를 생성해서 set 하도록 유의하자!

마무리

이번 글에서는 불변성에 대해서 다뤘습니다. 이 영상을 보기전에 이미 알고 있는 내용이어서 다른 영상을 볼까 고민했는데요. 막상 보니까 정말 유익한 영상이었습니다. 얼마전에 기술 면접에서 리액트에서 setState를 다룰 때 왜 새로운 객체나 배열을 통해 상태를 업데이트 해줘야하나? 라는 질문에 state는 읽기 전용 값이기 때문이라는 애매한 답을 한 적이 있었습니다. 이번 영상을 보니까 제가 애매하게 알고 있었던 부분을 확실하게 알게되는 거 같아서 너무 좋았습니다. 그리고 영상 후반부에 있는 structuredClone 메서드 또한 정말 꿀팁입니다.. lodash나 커스텀 재귀함수 말고 공식적으로 지원하는 API가 있다는 게 정말 놀랍네요.

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글