프론트엔드 개발/리액트

리액트와 리덕스에서 불변성 (Immutability)

cozyzoey 2022. 6. 18. 18:42

이 글은 리액트에서 "불변성을 유지하라"는 말에서부터 시작하게 되었다.

불변성은 무엇인가부터, 리액트와 리덕스에서 불변성을 유지하는 것, 불변성을 유지하며 상태를 업데이트하는 것까지 살펴보고자 한다.

 

 

불변성이란?

 

불변성(Immutanility)은 리액트, 리덕스, 자바스크립트 등지에서 등장하곤 하는 개념이다. 극단적으로 얘기하자면 현재의 변수들을 유지하는 것이 아니라 지속적으로 새로운 값들을 만들어내고 이전 값을 대체하는 것을 의미한다. 몇몇 언어에서는 가변값을 아예 지원하지 않는다. (Erlang, ML 등)

자바스크립트에서 기본형 데이터인 숫자, 문자열, boolean, null, undefined, Symbol은 모두 불변값이다.

참조형 데이터는 기본적으로 가변값이며, Object.defineProperty, Object.freeze 등으로 불변값으로 설정할 수도 있다.

 

 

순수 함수의 원칙

순수 함수는 불변성과 관련이 있다. 함수가 순수하려면 아래 원칙을 따라야 한다.

 

1. 동일한 입력은 항상 동일한 값을 반환해야 한다. (same input → same return)
2. 사이드 이펙트가 없어야 한다. (no side effect)

 

 

 

사이드 이펙트(Side Effect)란 무엇인가?

사이드 이펙트란 해당 함수 스코프 바깥에 무언가를 변경하는 것이다. 아래는 사이드 이펙트의 예시다.

 

  • 함수에 입력된 파라미터를 변환하는 것
  • 함수 바깥의 다른 어떤 상태를 변경하는 것 (전역 변수, document.무언가, window.무언가)
  • API 호출
  • console.log()
  • Math.random()

 

API 호출이 사이드 이펙트라는 것은 의아할 수 있다. API 호출은 서버간 네트워크 연결을 형성하고 브라우저에 네트워크 로그를 남긴다는 측면에서 사이드 이펙트다.

 

아래 배열 매서드는 변형을 일으킨다.

  • push: 끝에 아이템 더하기
  • pop: 끝에 아이템을 빼내기
  • shift: 시작에 아이템 더하기
  • unshift: 시작에 아이템 빼내기
  • sort
  • reverse
  • splice

 

반대로 아래는 사이드 이펙트가 없는 함수다. 아래 함수를 한번 호출하든 백만번 호출하든 함수 바깥 세상에 영향을 끼치지 않는다. 더불어 같은 입력값에 대해 항상 같은 값을 반환하기 때문에 순수 함수 조건에 부합한다.

 

function add(a, b) {
	return a + b;
}

 

 

순수 함수는 다른 순수 함수만 호출할 수 있다

함수의 순수성은 이행되는 특징이 있다. 전부 순수하거나 순수하지 않거나 둘 중 하나다. 완전히 순수한 함수가 끝날 때는 순수하지 않은 다른 함수를 호출한다면(setState, dispatch 등) 결국 순수하지 않은 함수가 된다. 참고로 console.log는 사이드 이펙트이긴 하지만 어떤 영향을 끼치는 것이 아니라서 허용 가능하다.

 

 

순수하지 않은 함수 VS 순수 함수

아래는 person이라는 객체에 nickname 속성을 부여하는 함수다. 함수에 전달된 person 객체를 변형시키고 동일한 객체를 반환한다. 동일한 객체지만 내부가 바뀐 상태로 그 이전에는 어땠는지 전혀 알 수가 없다. 

function giveNickname(person) {
	person.nickname = "kakao";
	return person;
}

 

위 함수를 순수 함수로 다시 작성해 보자.

function giveNickname(person) {
	let newPerson = Object.assign({}, person, {
    	nickname: "kakao"
    })
	return newPerson;
}

 

함수에 입력한 person을 변형하는 대신에 Object.assign이라는 메서드로 완전히 새로운 newPerson 이라는 객체를 만든다. Object.assign은 빈 객체를 만들고 거기에 person의 모든 속성을 넣고, 마지막으로 nickname 속성을 넣는다.

 

 

리액트는 불변성을 선호한다

 

리액트에서는 state 및 props를 변형시키지 않아야 한다. 상태를 변경하려면 오직 setState 사용해야 한다.

만일 상태를 직접 변경하면 (mutable) 리액트가 state 변화를 알아채지 못해서 리렌더가 일어나지 않는다. 그러므로 상태를 변형하지 말고 항상 새로운 객체 및 배열을 만들어서 setState를 호출해야 한다.

props는 오직 컴포넌트로 들어오기만 하는 단방향이다. 만일 부모로 데이터를 보내거나 부모 컴포넌트에서 무언가를 작동시켜야 한다면 함수를 prop으로 전달하면 된다. 그리고 자식 컴포넌트에서 그 함수를 호출한다.

 

 

 

불변성은 PureComponent에서 중요하다

 

기본적으로 리액트 컴포넌트는 부모가 리렌더되거나 상태가 바뀔 때마다 리렌더한다. 성능 최적화를 위한 간단한 방법은 컴포넌트를 클래스로 만들고 React.PureComponent로 extend 하는 것이다. 이렇게 하면  컴포넌트는 자기 상태나 props가 바꼈을 때만 리렌더된다. (부모 컴포넌트가 리렌더될때는 리렌더를 스킵함)

PureComponent로 전달하는 props는 전부 immutable하게 업데이트돼야 한다. 즉 props가 객체나 배열이라면 이를 새로운 객체나 배열로 바꿔줘야 하는 것이다. 그렇지 않고 객체나 배열의 내부를 수정하면 이전의 자기 자신과 참조적으로 동일하게 된다(referentially equal). 그래서 PureComponent는 상태가 바꼈음을 알아채지 못할 것이고 리렌더도 일어나지 않을 것이다. 렌더링 버그다.

 

 

참조적으로 동일하다는 것의 의미 (Referential Equality)

객체나 배열을 === 오퍼레이터로 서로 비교할 때 자바스크립트는 실제적으로 변수가 가리키는 메모리 주소를 비교한다. 변수의 내부는 보지도 않고 오직 reference만 비교하는 것이다.

만일 객체를 복사하면 복사한 객체는 기존 객체가 가리키는 메모리 주소를 동일하게 가리킨다. 그래서 복사한 객체를 변형하면 기존 객체도 함께 바뀐다.

동일성 체크를 깊게 하는게 맞지 않나? 두 객체의 내부까지 비교하는 것이 합당하게 보일 수 있다. 그런데 객체가 엄청 크다면? 시간이 오래 걸린다.

참조값만 비교하는 것은 O(1), 즉 입력에 상관없이 항상 동일한 만큼의 시간이 걸린다(constant time). 반면에 내부까지 비교하려면 O(N), 입력에 비례하는 만큼의 시간이 걸린다(linear time).

 

 

const는 불변성을 보장해줄까?

그렇지 않다. let, const, var 로 정의된 객체 모두 내부를 변화시킬 수 있다. 다만 const로 정의된 변수를 재할당하는 것은 불가능하다.

 

 

불변성 유지하며 상태 업데이트하기

리덕스에서 reducers는 순수 함수여야 한다. 즉, 상태를 직접적으로 변경하는 것이 아니라 기존의 상태를 바탕으로 새로운 상태를 만들어야 한다. 아래는 불변성을 유지하며 상태를 변경하는 예시다. 리덕스든 리액트든 모두 해당된다.

 

  1. spread 연산자...
    • 리액트의 setState는 shallow merge(객체의 첫번째 레벨만 신경씀)를 사용한다.
    • 2단계 이상으로 중첩된 아이템을 업데이트 하려면 spread 연산자를 사용해야 한다.
  2. slice로 배열을 복사해서 복사한 배열을 변형하기
  3. map (배열)
  4. filter (배열)

 

 

Immer 라이브러리

중첩된 객체를 불변성을 유지하며 업데이트하는 것은 길고 복잡하다(위 참고). 그 번거로운 작업을 Immer가 대신 해준다. push, pop 등 mutable code를 사용해도 Immer가 불변성을 유지하여 안전하게 상태를 업데이트 해줄 것이다. Redux Toolkit의 createReducer에 자동으로 적용되어 있다.

 

 

References

https://daveceddia.com/react-redux-immutability-guide/

정재남. <코어 자바스크립트>. 위키북스, 2020