개인 공부를 위해 작성했습니다
자바스크립트를 위한 상태 관리 라이브러리, 리덕스👍
리액트를 사용하는 많은 프로젝트에서 리덕스도 같이 사용하는데, 그 이유는
이런 이유로 공부한 react-redux에 대해 정리해본다.
store
객체에 저장한다전체 상태값이 하나의 자바스크립트 객체로 표현되기 때문에 활용도가 높아진다. 리덕스를 사용하면
상태값은 오직 액션 객체에 의해서만 변경되어야 한다
const incrementAction = {
type: 'INCREMENT', // --------------(1)
amount: 123, // --------------(2)
};
store.dispatch(incrementAction); // --(3)
incrementAction
라는 이름의 액션 객체를 살펴보면,
(1) 액션 객체는 type
속성값이 존재해야 한다. type
속성값으로 액션 객체를 구분한다.
(2) type
속성값을 제외하면 나머지는 상태값을 수정하기 위해 사용되는 정보다
(3) 액션 객체와 함께 dispatch
메서드를 호출하면 상태값이 변경된다.
리덕스의 상태값을 수정하는 유일한 방법은 액션 객체
와 dispatch
메서드를 호출하는 것이다. 다른 방법으로 상태값을 수정하면 안 된다.
상태값은 dispatch
메서드가 호출된 순서대로 리덕스 내부에서 변경된다
불변 객체를 사용하는 이유는, 이전 상태값과 이후 상태값을 비교해서 변경 여부를 파악할 때 유리하기 때문이다. 불변 객체를 사용하지 않고 직접 상태값을 수정하게 되면 이전 상태값 비교가 어렵다.
리듀서(reducer): 리덕스에서 상태값을 변경하는 함수
(state, action) => nextState
리듀서는 이전 상태값과 액션 객체를 입력받아 새로운 상태값을 만드는 순수 함수이다.
순수 함수는
side effect
(전역 변수의 값을 수정하거나 API 요청을 보내는 등 함수 외부의 상태를 변경시키는 것)를 발생시키지 않아야 하고,이러한 특성 덕분에 순수 함수는 테스트 코드를 작성하기 쉽다.
뷰(컴포넌트)
가 상태값을 변경하고 싶을 때는 액션을 발생시킨다.
액션
은 미들웨어
가 처리하고 (미들웨어에 원하는 기능 추가도 가능하다)
리듀서
는 액션에 의해서 상태값이 어떻게 변경되는지 로직을 담고 있다. 리듀서는 새로은 상태값을 출력하는데, 그 새로운 상태값을
스토어
에게 알려주면 스토어는 상태값을 저장한다. 변경된 상태값을 다시 뷰에게 알려준다.
dispatch
는 액션이 발생했다는 것을 리덕스에게 알려주는 함수action creator
를 만들어서 사용하는 이유는, 각 액션 객체의 구조를 일관성 있게 작성하기 위함type
을 상수 변수로 관리하는 이유는,action creator
에서도 사용하지만reducer
에서도 사용하기 때문// (1) dispatch()를 호출할 때 액션 객체를 직접 입력한 case
store.dispatch({
type: 'todo/ADD' // type: 액션을 구분하는 속성값이므로 고유해야 한다.
title: '리덕스 공부하기'
});
// (2) action creator 함수를 만들어서 호출한 case
const ADD = 'todo/ADD' // (3) 액션 type을 상수 변수로 관리
function addTodo(title) {
return { type: ADD, title }
}
store.dispatch(addTodo({ title: '리덕스 공부하기' }))
🚩 미들웨어
미들웨어(middleware)는 리듀서가 액션을 처리하기 전에 실행하는 함수
// 미들웨어의 기본 구조: 화살표 사용
const myMiddleware = store => next => action => next(action);
// 미들웨어의 기본 구조: 화살표 사용X
const myMiddleware = function(store){
return function(next){
return function(action){
return next(action);
};
};
};
🌱 미들웨어 작성 예제
const middleware1 = store => next => action => {
console.log('middleware1 start'); // (2)
const result = next(action); // (3)
console.log('middleware1 end'); // (8)
return result;
}
const middleware2 = store => next => action => {
console.log('middleware2 start'); // (4)
const result = next(action); // (5)
console.log('middleware2 end'); // (7)
return result;
}
const myReducer = (state, action) => {
console.log('myReducer'); // (6)
return state;
};
const store = createStore(myReducer, applyMiddleware(middleware1, middleware2));
store.dispatch({ type: 'someAction' }); // (1)
(1) 액션이 발생했을 때 미들웨어부터 처리가 된다.
첫 번째 미들웨어 middleware1
가 실행되고 (2)가 출력된다. 그 다음 줄의 (3)next
를 호출했을 때 (4)이 출력된다.
(3)next
는 middleware2
를 의미한다.
(5)next
는 없기 때문에 (5)next
는 reducer
를 호출한다.
그래서 (6)를 출력하게 되고 (5)next
인 myReducer()
가 종료되면서 (7)이 출력되고 (8)이 출력된다.
즉, 미들웨어의 순서대로 호출되고 next를 호출하면서 다음 미들웨어를 호출하게 되고 마지막 미들웨어에서는 리듀서를 호출한다.
상태값 변경을 검사하는 코드는 각 이벤트 처리 함수에서 구현해야 하는데, react-redux
패키지의 connect
함수에서는 자체적으로 상태값 변경을 검사한다.
🚩 리듀서
리듀서(reducer)는 액션에 발생했을 때 새로운 상태값을 만드는 함수다
(state, action) => nextState
🌱 리듀서 작성 예제
function reducer(state = INITIAL_STATE, action) { // (1) state = INITIAL_STATE
switch (action.type){ // (2) 액션 타입별 case로 처리
case REMOVE_ALL :
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) 각 액션 타입별로 switch case
문을 만들어서 처리한다.
(3) 상태값은 불변 객체로 관리해야 하므로 수정할 때 마다 새로운 객체를 생성한다.
(4) 처리할 액션이 없다면 상태값을 변경하지 않는다.
불변 객체를 관리할 목적으로 이머(immer) 패키지를 사용한다
🌱 이머(immer)를 사용해서 불변 객체를 관리하는 예제
import produce from 'immer';
const person = { name: 'yurim', age: 20 };
const newPerson = produce(person, draft => { // (1)
draft.age = 30;
});
(1) produce()
의 첫 번째 인자는 변경하고자 하는 객체,
두 번째 인자는 첫 번째 인자로 받은 객체를 수정하는 함수
draft
객체를 수정하면 produce()
가 새로운 객체를 반환한다.
🌱 이머(immer)를 사용해서 예제 코드 리펙토링 하기
function reducer(state = INITIAL_STATE, action) {
return produce(state, draft => {
switch (action.type){
case ADD :
return { draft.todos.push(action.todo) } // (1)
case REMOVE_ALL :
return { draft.todos = [] }
case REMOVE :
return { draft.todos = draft.todos.filter(todo => todo.id !== action.id) }
default :
return state;
}
})
}
const INITIAL_STATE = { todos: [] }
(1) draft
가 새로운 객체를 반환하므로, push
메서드를 사용해도 기존 상태값은 직접 수정되지 않는다.
🌱 createReducer()
로 리듀서 작성하기
switch
문 보다 더 간결하게 리듀서 함수를 작성할 수 있다
function reducer(state = INITIAL_STATE, { // (1)
[ADD]: (state, action) => state.todos.push(action.todo),
[REMOVE_ALL]: state => ( state.todos = [] ),
[REMOVE]: (state, action) => (state.todos = state.todos.filter(todo => todo.id !== action.id))
})
첫 번째 인자로 초기값을 받고,
(1) 두 번째 인자로 액션 처리 함수를 담고 있는 객체를 받는다.
🚩 스토어
스토어(store)는 리덕스의 상태값을 가지는 객체.
액션의 발생은 스토어의 dispatch()로 시작된다
리덕스의 첫 번째 원칙에 따라 전체 상태값을 하나의 store
객체에 저장한다.
하지만 여러개의 store
객체를 사용해도 문제가 되지 않는다.
단순히 데이터 종류에 따라 구분하기 위한 용도라면 combineReducer()
를 사용하면 된다.
특별한 이유가 없다면 스토어는 하나만 만드는 게 좋다.
리덕스는
store
객체에 저장한다.type
속성값으로 액션 객체를 구분한다. dispatch
메서드로 상태값을 변경한다dispatch
메서드를 호출하는 것이다.이머(immer)
패키지를 사용한다