상태에 어떠한 변화가 필요하게 될 때, 우리는 '액션'이라는 것을 발생시킨다.
이는 하나의 객체로 표현되는데, 액션 객체는 다음과 같은 형식으로 이루어져 있다.
{
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.js
todos.js
ActionTypes.js
counter.js
todos.js
위의 일반적인 구조는 코드를 종류에 다라 다른 파일에 작성하여 정리할 수 있어서 보기엔 깔끔하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 하기 때문에 번거롭기도 하다.
counter.js
todos.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);