[React]React와 불변성

왜 배열이나 객체를 핸들링을 ‘그렇게’ 해야하는가?




불변성이란?

1. 자바스크립트에서의 불변성


불변성? immutable?

불변성이란 일반적으로 값이나 상태를 변경할 수 없는 것이라는 뜻이다.


그럼 자바스크립트에서 불변성이란 어떤 의미 일까? 아래 코드를 보자.

let string = "test1";
string = "test2";

변수 string에 값 test1test2로 재할당하였다. 이때, 값을 변경한 것이므로 불변성을 지키지 않은 것일까?
결론부터 말하자면, Yes! 불변성을 지킨 것이다.

자바스크립트에서 말하는 불변성은 같은 메모리 영역에서 값을 변경할 수 없다.라는 의미이기 때문이다.
위 예제 코드를 메모리 관점에서 보자.

let string = "test1"; // 메모리 1001에 test1 할당
string = "test2"; // 메모리 1002에 test2 할당, 이때 1001의 값이 지워지지 않는다.


즉, string 변수가 바라보고 있는 1001 메모리에 값을 변경한 것이 아니라, 1002에 새로운 값을 할당하고 변수가 새로운 메모리 주소를 바라보게 된 것이므로, 기존의 1001 메모리의 값은 변경되지 않고 유지된다. 이것이 자바스크립트에서 말하는 불변성인 것이다.

2. 원시타입과 참조타입

이 글을 읽고 있는 개발자들은 원시 타입과 참조 타입의 차이가 무엇인지 정도는 알고 있을 것이라고 예상하고 말하자면, 원시 타입은 결국엔 값을 변경시 언제나 불변성을 유지하는 타입이고, 참조 타입에서 요소의 일부 값을 변경하는 경우에는 불변성을 지키지 않는다는 것이다.

let arr1 = [0, 1, 2, 3, 4]; // 메모리 1001
let arr2 = [0, 1, 2, 3, 4]; // 메모리 1002

arr1.push(5); // 메모리 1001안의 배열에 5를 추가
arr2 = [0, 1, 2, 3, 4, 5]; // 메모리 1003이 새로운 배열을 할당하고 arr2가 1003을 참조

console.log(arr1, arr2); //[0,1,2,3,4,5] , [0,1,2,3,4,5]

위 예제에서 arr1,arr2는 결론적으로 값은 값을 가지지만, arr1는 기존의 메모리 영역에 값을 추가하여 변경되었으므로 불변성을 지키지 않은 것이고, arr2는 기존 메모리 영역을 건들지 않고 새로운 메모리에 배열을 만들어 할당한 것이므로 불변성을 지킨것이 된다!

즉, 참조 타입에서는 어떻게 배열이나 객체를 핸들링하냐에 따라 불변성을 지킨 것이 될 수도 있고, 지키지 않은 것이 될수도 있다는 것에 유의해야 한다.


리액트에서 불변성을 지켜야 하는 이유

1. 리액트의 상태 업데이트 방식

그럼 리액트에서 왜 불변성을 지켜가며 코드를 작성해야 할까?
가장 중요한 이유는 바로 리액트가 상태를 업데이트 하는 기준 때문이다. 즉, 리액트에서는 해당 변수가 참조하고 있는 메모리 주소가 바뀐 것을 기준으로 상태가 변경되었다고 감지하고 업데이트 하기때문이다.

따라서 우리가 불변성을 지키지 않고 배열의 값을 변경한다면 리액트는 해당 상태가 변경되었다고 감지하지 못하는 것이다.

2. 순수 함수와 사이드 이펙트

또 리액트가 지향하는 함수형 프로그래밍 관점과도 관련이 있다. 함수형 프로그래밍의 가장 큰 특징인 순수 함수는 함수가 실행되면서 외부의 값을 변경하는 사이드 이펙트가 일어나지 않는 것을 말한다.

이때, 리액트에서 메모리 주소 변경을 기준으로 상태 변경을 감지하고, 원본 데이터를 사용하는 것이 아닌 새로운 데이터를 사용하는 것이 사이드 이펙트를 방지하는 효과가 있고 이것이 리액트가 지향하는 함수형 프로그래밍이 지향하는 바와 일부 일맥상통한다는 점이다.



그렇다면 어떻게 불변성을 유지하는가?

1. map, filter, slice, splice, reduce 등을 사용하기

array의 메서드 중에 값을 다이렉트로 변경하는 pushshift, pop 등을 사용하는 것이 아니라, 새로운 배열이나 객체를 반환하는 메서드를 사용하면 새로 반환되는 배열이나 객체는 새로운 메모리에 할당되고 그 메모리 값을 새로 참조하게 된다.

const [students, setStudents] = useState(["철수", "민수", "은주", "영희"]);

students.pop(); //wrong
setStudents(students.slice(0, 3)); //good


2. spread 연산자 사용하기

spread operator는 해당 배열이나 객체를 펼쳐서 새로운 메모리에 할당한다. 따라서 spread operator를 함께 사용하면 불변성을 지키면서 프로그래밍 할 수 있다.

students.push("안나"); //wrong
setStudents([...students, "안나"]); //good


3. immer.js 사용하기

immer.jspushpop 메서드 등을 사용하면서도 불변성을 지키게 해주는 라이브러리이다. 이와 관련되서는 다른 포스트에서 redux와 함께 자세히 살펴보도록 하겠다.

Reference

모던 자바스크립트 deep dive
MDN Immutable