리덕스는 자바스크립트를 위한 상태 관리 프레임워크다. 리액트를 사용하는 많은 프로젝트에서 리덕스도 같이 사용하는데 이유는 왜일까?
상태 관리 코드를 분리
할 수 있다.서버 렌더링
시 데이터 전달이 간편
하다.로컬 스토리지
에 데이터를 저장하고 불러
오는 코드를 쉽게 작성할 수 있다.부모 컴포넌트
에서 깊은 곳에 있는 자식 컴포넌트에 상탯값을 전달
할 때 좋다.
- 전체 상탯값을 하나의 객체에 저장한다.
- 상탯값은 불변객체다.
- 상탯값은 순수 함수에 의해서만 변경되어야 한다.
하나의 객체에 프로그램의 전체 상탯값을 저장한다
전체 상탯값이 하나의 자바스크립트 객체로 표현되기에 활용도가 높아진다. 리덕스를 사용하면 하나의 객체를 직렬화해서 서버와 클라이언트가 프로그램의 전체 상탯값을 서로 주고받을 수 있다.
최근의 상탯값을 버리지 않고 저장해 놓으면 실행취소
와 다시 실행 기능
을 쉽게 구현할 수 있다.
하지만, 프로그램의 전체 상탯값을 리덕스로 관리하는 것은 쉬운 일이 아니다
. 애니메이션을 위한 데이터나 문자열 입력창의 현재 상탯값은 컴포넌트에서 관리하는게 더 나을 수도 있다. 프로그램의 일부 상태만 리덕스를 활용해도 된다.
상탯값을 불변객체로 관리한다
상탯값은 오직 액션객체
에 의해서만 변경된다.
const incrementAction={
type:"INCREMENT",//1
amount:123,//2
};
const conditionalIncrementAction={
type:"CONDITIONAL_INCREMENT",//1
amount:2. //2
gt:10, //2
lt:100, //2
};
store.dispatch(incrementAction); //3
store.dispatch(conditionalIncrementAction);//3
1.액션 객체는 type 속성 값
이 존재해야 한다. type속성 값으로 액션 객체를 구분한다.
2.type속성 값을 제외한 나머지는 상탯값을 수정하기 위해 사용되는 정보
다.
3. 액션 객체와 함께 dispath메서드
를 호출하면 상탯값이 변경
된다.
리덕스의 상탯값을 수정하는 유일한 방법은 액션 객체와 함께 dispatch메서드를 호출하는 것이다.
상탯값은 dispatch메서드가 호출된 순서대로 리덕스 내부에서 변경되기에 상탯값이 변화되는 과정을 쉽게 이해할 수 있다.
상탯값 수정이라는 목적만 놓고 보면 불변객체를 사용하는 것보다는 상탯값을 직접 수정하는 게 더 빠르다. But,
이전 상탯값과 이후 상탯값을 비교해서 변경 여부를 파악
할 때는불변 객체
가 훨씬 유리하다. 리액트 렌더링 성능을 올리는 데도 유리하다.
리액트에서 엄청 깊은 구조로 되어있는 객체를 업데이트를 할 때 기존의 객체는 건들이지 않고
Object.assign
을 사용하거나spread 연산자
를 사용하여 업데이트하곤 한다. 리덕스에서도 마찬가지!리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위하여shallow equality검사
를 하기 때문이다. 이를 통하여 객체의 변화를 감지할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있는 것이다.
오직 순수 함수에 의해서만 상탯값을 변경해야 한다
리덕스에서 상탯값을 변경하는 함수를 리듀서
라고 부른다.
(state,action)=>nextState
리듀서는 이전 상탯값과 액션 객체를 입력으로 받아서 새로운 상탯값을 만드는 순수 함수다. 순수 함수는 부수효과(side effect: 전역 변수값을 수정한다거나 API 요청을 보내는 등 함수 외부의 상태를 변경시키는 것을 말한다
)를 발생시키지 않아야 한다. 또한 순수함수는 같은 인수에 대해 항상 같은 값을 반환해야 한다.
리듀서는 순수 함수이기에 같은 상탯값과 액션 객체를 입력하면 항상 똑같은 다음 상탯값을 반환한다. 따라서 실행된 액션객체를 순서대로 저장했다가 나중에 똑같은 순서로 dispatch메서드를 호출하면 쉽게 리플레이 기능 구현할 수 있다.
액션은 type속성값을 가진 자바스크립트 객체다.
액션을 발생시키는 예제 코드를 봐보자.
store.dispatch({type:'ADD',title:'영화 보기',priority:'high'});
store.dispatch({type:'REMOVE',id:123});
store.dispatch({type:'REMOVE_ALL'});
각 액션은 고유한 type속성 값을 사용해야 하는데 ADD라는 단어 하나만으로는 중복 가능성이 높다. type이름의 중복을 피하기 위해 다음과 같이 접두사를 붙이는 방법이 많이 사용된다.
store.dispatch({type:'todo/ADD',title:'영화 보기',priority:'high'});
store.dispatch({type:'todo/REMOVE',id:123});
store.dispatch({type:'todo/REMOVE_ALL'});
type속성 값은 리듀서에서 액션 객체를 구분할 때도 사용되기에 상수 변수로 만드는게 좋다.
export const ADD='todo/ADD';
export const REMOVE='todo/REMOVE';
export const REMOVE_ALL='todo/REMOVE_ALL';
// 1
export function addTodo({title,priority}){
return{type:ADD,title,priority};
}
export function removeTodo({id}){
return{type:REMOVE,id};
}
export function removeAllTodo(){
return{type:REMOVE_ALL};
}
//2
1에서 type 이름을 상수 변수로 만들었다. 이 변수는 리듀서에서도 필요하기에 export 키워드를 이용해서 외부에 노출한다. 2의 액션 생성자 함수도 외부에서 노출해야 하므로 외부로 노출한다.
미들웨어는 리듀서가 액션을 처리하기 전에 실행되는 함수다. 디버깅 목적으로 상탯값 변경 시 로그를 출력하거나, 리듀서에서 발생한 예외를 서버로 전송하는 등의 목적으로 미들웨어를 활용할 수 있다. 리덕스 사용 시 특별히 미들웨어를 설정하지 않았다면 발생한 액션은 곧바로 리듀서로 보내진다
.
const myMiddleware=store=>next=>action=>next(action);
미들웨어는 함수 세 개가 중첩된 구조로 되어 있다. 화살표 함수가 연속으로 표현된 코드가 익숙하지 않는다면 헷갈릴 수 있다.
리듀서는 액션이 발생했을 때 새로운 상탯값을 만드는 함수다.
function reducer(state=INITIAL_STATE,action){//1
switch(action.type){
//...
case REMOVE_ALL://2
return{
...state,
todos:[],
}; //3
case REMOVE:
return{
...state,
todos: state.todos.filter(todo=>todo.id!==action.id),
};
default;
return state;//4
}
}
const INITIAL_STATE={todos:[]};
리덕스는 스토어를 생성할 때 상탯값이 없는 상태로 리듀서를 호출하므로 1 매개변수의 기본값을 사용해서 초기 상탯값을 정의
한다. 2 각 액션 타입별로 case문을 만들어서 처리한다
. 3 상탯값은 불변 객체로 관리해야 하므로 수정할 때마다 새로운 객체를 생성한다.
전개 연산자를 사용하면 상탯값을 불변 객체로 관리할 수 있다. 4 처리할 액션이 없다면 상탯값을 변경하지 않는다.
스토어는 리덕스의 상탯값을 가지는 객체다. 액션의 발생은 스토어의 dispatch메서드로 시작된다. 스토어는 액션이 발생하면 미들웨어 함수를 실행하고, 리듀서를 실행해서 상탯값을 새로운 값으로 변경한다.
리덕스의 첫 번째 원칙에서 애플리케이션의 전체 상탯값을 하나의 스토어에 저장하라고 했다.
단순히 데이터의 종류에 따라 구분하기 위한 용도라면 나중에 설명할 combineReducer함수를 이용하면 된다. 그러나 특별한 이유가 없다면 스토어는 하나만 만드는 것이 좋다.
참고: 실전리액트프로그래밍