리덕스의 원리

리덕스는 크게 (1)중앙저장소, (2)액션, (3)디스패치, (4)리듀서로 나뉘며 흐름은 중앙저장소 → 액션 → 디스패치 → 리듀서 → 중앙저장소로 흘러간다.

(2)액션에서는 발생하는 액션에 대한 정의를 해주고, (4)리듀서에서는 발생한 액션에 따라 데이터를 어떻게 immutable하게 바꾸는지 결정해주는 방식이다. 서비스가 커질 수록 정의해야하는 액션과 리듀서가 많아지므로 코드량이 방대해진다.

이러한 과정이 번거롭다고 생각할 수 있지만, 실제 리덕스를 구현해보면 상당히 효율적이다. 액션 하나하나가 모두 리덕스에 기록이 되므로 (데이터 변경 사항이 추적이 된다.) 유지보수면에서 매우 좋기 때문이다. (타임머신처럼 데이터 변화 히스토리를 redux-devtool로 모두 확인할 수 있다.)

예를 들어 로그인, 로그아웃 등의 상태를 화면으로 테스트해볼 때에도 기존에는 웹사이트 내에서 로그인, 로그아웃 등의 동작을 모두 다시 해야 테스트가 가능했다면 리덕스를 사용하면 redux-devtool에서 히스토리를 거슬러 올라가는 것으로 모든 컴포넌트 테스트가 가능하다.

리덕스의 불변성

리덕스에서는 불변성(Immutable)이 중요하므로, 항상 다른 객체를 리턴한다. (아래 코드 참조)

{} === {} // false
const a = [];
const b = a;
a === b; // true - 참조 관계가 있으면 true이다. 

왜 객체를 새로 만들어야 할까? 아래의 prev, next 변수를 보면 알 수 있듯 변경 사항을 추적할 수 있기 때문이다. 우리는 변경사항을 추적하여 관리하는 것을 목적으로 하므로 반드시 데이터의 불변성을 유지해줘야 한다.

const prev = { name: 'vicky', posts: [{}, {}, {}] }

// 아래와 같이 직접 객체를 수정하면 원 데이터도 변경된다. (즉, 변경사항을 추적할 수 없다.)
const next = prev;
next.name = 'wonny';
prev.name; // wonny

// 아래와 같이 변경사항을 새로운 객체로 정의해주면 원 데이터가 유지되어 변경사항을 추적할 수 있다.
const next = { name: 'wonny', ...prev }

그렇다면 데이터 업데이트 시 기존 데이터를 왜 전개 구문(Spread Operator)을 사용하여 만들어줄까?(...prev) 그 이유는 코드의 양이 줄어드는 것도 있지만 메모리를 절약해주기 때문이다. 만약 전개 구문없이 직접 적어주면 새로운 객체를 생성하여 추가 메모리가 소요된다. 따라서 전개 구문을 사용해 참조관계를 유지시켜 메모리를 절약하는 것이다.

또한 { a: 'b' } === { a: 'b' } // false 이므로 데이터를 직접 적어주면 리덕스는 데이터가 변경되었다고 판단하여 컴포넌트에서 불필요한 리렌더링이 발생할 가능성이 있다. 따라서 참조를 적절히 유지하는 것은 Redux에서 매우 효율적인 방식이다.

또한 development 버전에서는 리덕스가 히스토리를 모두 가지고 있으나 실제 production에서는 히스토리를 보는 기능이 별도로 필요하지 않으므로 중간중간 히스토리를 정리하여 메모리 문제가 발생하지 않는다.

참조 - 얕은 복사

const next = { b: 'c' };
const prev = { a: next };

const next = { ...prev };

prev.a === next.a // true - 객체 안의 참조를 그대로 유지한다.
prev === next // false - 객체 자체는 모두