React state의 불변성 유지

WONNY_LOG·2023년 6월 1일

불변성이란?

어떠한 값을 직접적으로 변경하지 않고 기존의 상태 값을 유지하면서 새로운 상태 값을 추가하는 것이다.

불변성을 유지하지 않는다면?

리액트에서 state가 불변성을 유지하지 않는다는 것은, setState로 state값을 할당하는 것이 아닌 state 자체 값을 변경하는 것이다.

React는 이전 state와 새로운 state를 참조 비교(reference comparison)사용하여 변경된 값을 인지한다.
그런데 state에 직접 값을 변경하면(=state 자체 값 변경) 이전 state와 새로운 state 모두 같은 참조값을 갖고있기때문에, 변경된 값을 인지하지 못하게 된다.

불변성을 유지하려면?

초기에 할당한 값 자체를 변경하지 않으면 된다!
리액트에서 상태를 변경하려면 오직 setState 사용해야 한다.
setState 함수를 활용하여 인수로 새로운 값을 할당하면 이전 객체와는 다른 참조 주소를 지닐 수 있다.
때문에 리액트는 이전 state와 새로운 state를 다른 참조 주소로 인식하여 변화된 값을 올바르게 변경준다.

const arr = [1,2,3]
const [value,setValue] = useState(arr)

setValue([...value,4])
//...(스프레드 연산자)를 사용하면 해당 배열의 값을 꺼내어 새로운 배열의 값으로 할당해줄 수 있다.

얉은 복사(Spread Operator)

객체, 배열, 함수 같은 참조 타입들을 실제 내부 값까지 비교하지 않고 동일한 참조인지만을 비교하는 것

const user = { name: 'Choi', age: 25 }
const otherUser = { ...user };
user.name = 'Lee';
/* 
user = { name: 'Lee', age: 25 }
otherUser = { name: 'Choi', age: 25 }
*/

user === otherUser  // false 서로 다른 참조 값을 가지고 있음

새로운 객체에 ...라는 spread operator를 사용하여 복사를 한다. otherUser는 새로운 객체를 할당 받았기 때문에 user와 otherUser가 가리키는 객체의 참조 값은 다르게 됩니다.
따라서 user의 값 변경은 otherUser의 객체에 영향을 주지 않는다. (불변성이 잘 지켜졌다고 할 수 있다.)

하지만 객체의 깊이가 깊어지면 여전히 문제가 생긴다.
얕은 복사는 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사된다.
이를 보완하기 위해선 깊은 복사(deep copy)를 해야 한다.

깊은 복사

깊은 복사는 불변성을 유지해 줄 수 있지만 객체의 구조가 복잡해질수록 불변성 유지가 힘들어진다.
for문을 이용해서 깊은 복사를 구현하거나 JSON으로 stringfy시킨후 다시 parse하는 과정을 거치면 완전한 참조가 끊기는 깊은 복사가 구현된다.
JSON.parse(JSON.stringify(a))
단, 이 방법의 경우 성능상의 문제가 있는 방법이라고 얘기해서 lodash 혹은immer과 같은 라이브러리를 사용해 불변성을 유지해 주기도 한다.

const user = { name: 'Choi', age: 25, friends: ['Park', 'Kim']}
const otherUser = { ...user, friends: [...user.friends] };
user.name = 'Lee';
user.friends.push('Kang');
/* 
user = { name: 'Lee', age: 25, friends: ['Park', 'Kim', 'Kang'] }
copyUser = { name: 'Choi', age: 25, friends: ['Park', 'Kim'] }
*/

user === otherUser  // false
user.friends === otherUser.friends // false

객체를 복사하는 법
Object.assign()
...(스프레드 문법)

배열을 복사하는 법
slice()
...(스프레드 문법)

❗️결론은 필요한 값을 변형해서 사용하고 싶다면 어떤 값의 사본을 만들어서 사용해야 한다.





리덕스의 3가지 특징

  1. Store는 1개만 쓴다.
    : 리덕스는 단일 스토어 규칙을 따릅니다. 한 프로젝트에 스토어는 하나만 씁니다.
    (단일 스토어에 리듀서는 여러 개일 수 있다.)
  2. store의 state(데이터)는 오직 action으로만 변경할 수 있다!
    : "나 이거 바꿔줘"라고 Reducer에 요청하고 바꾸는 건 Reducer에게 맡겨야 한다.
    (리덕스에 저장된 데이터 = 상태 = state는 읽기 전용)
  3. 순수한 함수 reducer
    : 리듀서는 순수한 함수여야 한다.
    (가지고 있던 값을 수정하지 않고, 새로운 값을 만들어서 상태를 갈아끼운다.)
    Redux의 reducer 함수가 toolkit => createReducer로 재탄생함
- 파라미터 외의 값에 의존하지 않아야 한다.
(전역 변수에서 값을 가져와서 사용하면 안되고 무조건 액션에서 넘어온 파라미터 값만 사용해야 한다.)
- 이전 상태는 수정하지(=건드리지) 않는다. (변화를 준 새로운 객체를 return 해야합니다.)
(State를 직접적으로 건드리면 안된다, 새로운 State만 반환 해야 한다.)
- 파라미터가 같으면, 항상 같은 값을 반환
(예를 들어 랜덤 함수를 사용했을 때는 1을 받았는 데 랜덤 함수 때문에 매번 다른 값이 나오면 순수한 함수가 아니다.)
- 리듀서는 이전 상태와 액션을 파라미터로 받는다.

리듀서 내부에서 불변성을 지키는 이유?

reducer = 기본 state를 action을 통해 새로운 state로 변형시키고, 그 값을 store에 전달한다.
리덕스는 항상 상태를 업데이트할때 기존 객체를 건드리지 않고 새로운 객체를 생성한다.
내부적으로 데이터가 변경됨을 감지하기 위해 얕은 비교 검사를 하는데, 객체 깊이 비교하지 않기 때문에 좋은 성능을 유지하는 장점이 있다.
이때문에 리듀서함수 내부의 객체는 항상 초기값이여야한다.

(리덕스의 상태관리 도구 리듀서!)






참조 값: (객체가 저장되어 있는 메모리 주소)
참조 비교: 두개의 객체를 비교하는 것
Spread Operator: Spread Operator로 복사한 객체(배열)은 1depth의 값에서만 깊은 복사를 실행한다

리액트의 참조비교
깊은복사, 얉은 복사
깊은복사
immer
Redux-reducer
리듀서
원조타입, 참조타입

0개의 댓글