상태에 어떠한 변화가 필요하게 될 때, 우리는 '액션'이라는 것을 발생시킨다.
이는 하나의 객체로 표현되는데, 액션 객체는 다음과 같은 형식으로 이루어져 있다.
{
type: 'TOGGLE_VALUE'
}
액션 객체는 type 필드를 필수적으로 가지고 있어야 하고, 그 외의 값들은 개발자 마음대로 넣어줄 수 있다.
{
type: "ADD_TODO",
data: {
id: 0,
text: "리덕스 배우기",
}
}
{
type: "CHANGE_INPUT",
text: "안녕하세요"
}
액션 생성 함수는 액션을 만드는 함수이다.
파라미터를 받아와서 액션 객체 형태로 만들어주는 역할을 한다.
export const addTodo = (data) => ({
type: "ADD_TODO",
data,
});
export const changeInput = (text) => ({
type: "CHANGE_INPUT",
text,
});
이러한 액션 생성 함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다. 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.
리듀서는 변화를 일으키는 함수이다. 리듀서는 두 가지의 파라미터, 즉 '현재의 상태'와 '전달받은 액션'을 참고하여 새로운 상태를 만들어서 반환하는 함수이다.
만약 카운터를 위한 리듀서를 작성한다면 다음과 같이 작성할 수 있다.
export default const counterState = (state, action) => {
switch(action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
default:
return state;
}
}
Redux를 사용할 때에는 여러 개의 리듀서를 만들고, 이를 합쳐서 루트 리듀서(rootReducer)를 만들 수 있다. (루트 리듀서 안의 작은 리듀서들은 서브 리듀서라고 부른다.)
Redux에서는 한 어플리케이션 당 단 하나의 스토어를 만들게 된다.
스토어 안에는 현재의 앱 상태와 리듀서가 들어가 있고, 추가적으로 몇 가지 내장 함수들이 있다.
dispatch라는 함수에는 액션을 파라미터로 전달한다. ex) dispatch(action)dispatch를 호출하면 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태로 만들어준다.Redux를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하는데, 이 코드들을 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 파일 하나에 작성하는 방법도 있다.
counter.jstodos.jsActionTypes.jscounter.jstodos.js위의 일반적인 구조는 코드를 종류에 다라 다른 파일에 작성하여 정리할 수 있어서 보기엔 깔끔하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 하기 때문에 번거롭기도 하다.
counter.jstodos.js위의 패턴에서는 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 싹 다 몰아서 작성하는 방식이다. 이러한 방식을 Ducks 패턴이라고 부르며, 앞서 설명한 일반적인 구조로 리덕스를 사용하다가 불편함을 느낀 개발자들이 자주 사용하는 패턴이라고 한다.
이렇게 Ducks 패턴을 이용하여 액션 타입, 액션 생성 함수, 리듀서를 몰아서 쓴 파일을 '모듈(module)'이라고 한다.
/src/store 디렉토리에 /modules 폴더를 만들고 거기에 우리가 연습해볼 counter 모듈 counter.js를 만들자.
가장 먼저 해야 할 일은 바로 '액션 타입(Action type)'을 정의하는 것이다.
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export 키워드를 붙여줘서 다른 파일에서 import해서 사용할 수 있도록 한다.// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 액션 생성 함수
export const increase = () => ({ type: 'INCREASE'});
export const decrease = () => ({ type: 'DECREASE'});
이제 counter 모듈의 초기 상태와 리듀서 함수를 만들어주자.
이 모듈의 초기 상태에는 number 값을 설정해 주었으며, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성하여 반환하는 코드를 작성해준다. 마지막으로, export default 키워드를 사용하여 함수를 내보내준다.
// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// 초기 상태 (initialState)
const initialState = { number: 0 };
// 리듀서 (reducer)
export default const counterState = (state = initialState, action) => {
switch(action.type) {
case 'INCREASE':
return {
number: state.number + 1,
};
case 'DECREASE':
return {
number: state.number - 1,
};
default:
return state;
}
}
그런데, 여기에서 1씩 증가하고 1씩 감소하는 것이 아니라, 지정해준 값만큼 증가하고 감소하는 기능도 추가하고 싶다면 어떻게 해야 할까? 다음과 같이 액션을 추가한다.
// 추가된 액션
const INCREASE_BY_VALUE = 'counter/INCREASE_BY_VALUE';
const DECREASE_BY_VALUE = 'counter/DECREASE_BY_VALUE';
// 추가된 액션 생성 함수
export const increase_by = (num) => ({
type: INCREASE_BY_VALUE,
value: num,
});
export const decrease_by = (num) => ({
type: DECREASE_BY_VALUE,
value: num,
})
// 초기 상태는 그대로
const initialState = { number : 0 };
// 리듀서에서 추가된 코드
export default const counterState = (state = initialState, action) => {
switch(action.type) {
case 'INCREASE_BY_VALUE':
return {
...state,
number: state.number + action.value,
};
case 'DECREASE_BY_VALUE':
return {
...state,
number: state.number - action.value,
};
}
}
완성된 전체 counter.js 모듈의 코드는 다음과 같다.
// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
const INCREASE_BY_VALUE = 'counter/INCREASE_BY_VALUE';
const DECREASE_BY_VALUE = 'counter/DECREASE_BY_VALUE';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increase_by = (num) => ({
type: INCREASE_BY_VALUE,
value: num,
});
export const decrease_by = (num) => ({
type: DECREASE_BY_VALUE,
value: num,
})
// 초기 상태 (initialState)
const initialState = { number: 0 };
// 리듀서 (reducer)
export default const counterState = (state = initialState, action) => {
switch(action.type) {
case 'INCREASE':
return {
number: state.number + 1,
};
case 'DECREASE':
return {
number: state.number - 1,
};
case 'INCREASE_BY_VALUE':
return {
...state,
number: state.number + action.value,
};
case 'DECREASE_BY_VALUE':
return {
...state,
number: state.number - action.value,
};
default:
return state;
}
}
Redux를 사용할 때 위처럼 모듈을 딱 하나만 사용하진 않을 것이다. /src/store/modules 디렉토리에 counter.js를 비롯한 많은 모듈들이 들어가게 될 것인데, 해당 모듈들에는 각각 리듀서들이 존재하게 되므로 전체적으로 봤을 때는 리듀서가 여러 개 존재하게 된다.
그런데 나중에 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 단 하나만 파라미터로 넘겨줄 수 있다. 그렇기 때문에, 기존에 만들었던 리듀서들을 하나로 합쳐주어야 하는데, 이 작업은 Redux에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수 있다.
/src/store 디렉토리에 index.js를 만들고 다음과 같이 작성하자.
import { combineReducers, createStore } from 'redux';
import counterState from './modules/counter.js`;
const rootReducer = combineReducers({
counterState,
});
export const store = createStore(rootReducer);