Redux 팀에서 공식적으로 만든 가장 최신 상태 관리 라이브러리이다.
Redux-toolkit (이하 RTK) 은 아래와 같은 특징을 가진다.
지금까지 배웠던 Redux, React-redux 그리고 지금 RTK 까지 Redux 관련하여 많은 상태 관리 도구들이 존재한다.
상태 관리를 더욱 효율적이고 사용하기 쉽게 한다는 목적은 동일한데, 과연 무엇이 다른걸까?
Redux 는 React 와는 직접적인 관계는 없다.
JavaScript 로 된 것이라면 어디서든지 사용할 수 있다.
반대로 이야기하자면, React 에서 사용하기엔 다소 불편한 점이 많다.
자주 사용하는 메소드는 다음과 같다.
createStore
subscribe
getState
dispatch
Redux 의 본질적인 개념을 토대로 React 에 맞춰 상태 관리를 도와주는 도구이다.
사용 방법이나 기본 원리는 모두 Redux 에 기반하기에 동일하다고 봐도 무방하다.
다만, 컴포넌트 단위에서 작업이 이뤄지는 React 의 특성에 맞게 그에 맞는 메소드를 제공해주는 차이점이 있다.
자주 사용하는 메소드는 다음과 같다.
Provider
useDispatch
useSelector
React-redux 라는 React 에 맞게 딱 맞게 나온 라이브러리임에도 불구하고 다음과 같이 여전히 많은 불편한 점이 많았다.
Action creator
)바로 이러한 문제점들을 타파하고자 나온 것이 바로 Redux-toolkit 이다.
기존의 Redux 는 거대한 하나의 store
에 모든 데이터를 담았다.
하지만 프로젝트가 커지면 기능 별로 작은 store가 필요할 것이다.
이 작은 store를 slice
라고 칭한다.
물론, Redux 가 근본적으로 추구하는 하나의 거대한 store
의 정체성은 지킨다. configureStore
라는 메소드로 작은 store인 slice
를 한데 모아 하나의 큰 store
에 취합한다.
기존 라이브러리와 차이점을 알아보기 위해 + 버튼을 눌렀을 때 값이 증가하는 예제를 만들어 본다.
기존의 Redux, React-redux로 구현한 코드는 다음과 같다.
Redux 의 createStore
를 이용하여 store
를 한 군데 모으고, React-redux 의 useSelector
, useDispatch
를 이용하여 상태를 업데이트 하는 코드이다.
import {Button, SafeAreaView, Text, View} from 'react-native';
import {createStore} from 'redux';
import {Provider, useSelector, useDispatch} from 'react-redux';
const reducer = (state, action) => {
if (action.type === 'up') {
return {...state, value: state.value + action.step};
}
return state;
};
const initialState = {
value: 0,
};
const store = createStore(reducer, initialState);
const Counter = () => {
const dispatch = useDispatch();
const count = useSelector(state => state.value);
return (
<SafeAreaView>
<View>
<Text>{count}</Text>
<Button
title="+"
onPress={() => {
dispatch({
type: 'up',
step: 2,
});
}}></Button>
</View>
</SafeAreaView>
);
};
function App(): JSX.Element {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;
이제 위 코드를 RTK 를 이용하여 만들어보자.
import {Button, SafeAreaView, Text, View} from 'react-native';
import {Provider, useSelector, useDispatch} from 'react-redux';
import {createSlice, configureStore} from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counterSlice',
initialState: {
value: 0,
},
reducers: {
up: (state, action) => {
state.value = state.value + action.payload;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
const Counter = () => {
const dispatch = useDispatch();
const count = useSelector(state => {
return state.counter.value;
});
return (
<SafeAreaView>
<View>
<Text>{count}</Text>
<Button
title="+"
onPress={() => {
dispatch(counterSlice.actions.up(2));
}}></Button>
</View>
</SafeAreaView>
);
};
function App(): JSX.Element {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;
코드를 하나씩 떼어서 보자.
createSlice
는 이름에서 알 수 있듯이 앞서 설명한 작은 store인 slice
를 만든다.
slice
안에서 initialState
와 reducer
를 함께 정의한다.
먼저, slice의 이름을 정한다. 필자는 'counterSlice' 고 정했다.
다음은 initialState
이다. 초기값을 0으로 세팅했다.
마지막으로는 reducer
이다. 여기서 reducer
는 복수형 reducers 임을 알 수 있다. 이 뜻은, 하나의 작은 기능을 담당하는 slice
하나에 여러 개의 reducer
를 설정할 수 있다는 의미를 지닌다.
현재는 버튼을 눌렀을 때 숫자가 올라가는 up
이라는 리듀서 하나이지만, 숫자가 감소되는 down
, 두 배씩 증가하는 multiply
등 여러 개의 reducer
를 정의할 수 있으며 원하는 리듀서를 골라 사용할 수 있다.
const counterSlice = createSlice({
name: 'counterSlice',
initialState: {
value: 0,
},
reducers: {
up: (state, action) => {
state.value = state.value + action.payload;
},
down: () => {
...
},
multiply: () => {
...
},
},
});
createSlice
를 통해 만든 slice
들을 한 데 모아 하나의 큰 store
로 합쳐주는 메소드이다.
즉, Redux 라이브러리의 createStore
와 동일한 역할이다.
(참고로 Redux 팀에서 를 사용하는 대신 RTK 의 createStore
configureStore
를 사용하라고 권고하는 입장이다.)
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
other: otherSlice.reducer,
...
},
});
이 객체의 키값을 보면 reducer
가 있다.
createSlice
객체의 reducers
는 복수형이라고 말했다. 복습하는 차원에서 다시 설명하자면, reducers
는 하나의 slice
가 여러 개의 기능을 담당할 수 있으므로 복수 개의 reducer
를 지원한다는 의미에서 복수형이라고 말했다.
그렇다면 configureStore
의 reducer
는 왜 단수형인가?
counterSlice와 같이 여러 작은 기능들을 담당하는 모든 slice
를 하나의 reducer
로 통합하겠다는 의미이다.
또한, 이는 Redux 라이브러리의 createStore
에 들어가는 파라미터 중 reducer
와 동일하다.
밑에 코드를 보면 첫 번째 인자로 reducer
를 받고 있음을 알 수 있다.
const reducer = (state, action) => {
if (action.type === 'up') {
return {...state, value: state.value + action.step};
}
return state;
};
const store = createStore(reducer, initialState);
reducer
객체 안의 counter: counterSlice.reducer
의미를 알아보자.
앞서 이야기 한 것처럼, counterSlice
라는 이름의 slice
는 up
이라는 기능을 하는 reducer
외에도 여러 개가 존재할 수 있다. 이를 counter
라는 이름으로 한데 모아 하나에 담겠다는 뜻이다.
이를 사용할 땐 counter
객체에 존재하는 특정 데이터를 호출하면 된다.
(밑에서 설명한다.)
useSelector
는 기존과 동일하게 React-redux 라이브러리의 메소드를 사용한다.
const count = useSelector(state => {
console.log(state);
return state.counter.value;
});
먼저 console 결과를 보자.
{"counter": {"value": 0}}
앞에 configureStore
에서 정의한 slice
의 reducer
를 한데모은 키값이 상위 객체로 잡혀있다.
이 말은 즉, 어떤 slice의 데이터를 원하는지 직접 선택해야 된다는 뜻이다.
그래서 return을 state.counter.value;
라고 한 것처럼 counter라는 slice
의 value 라는 데이터를 가져오겠다고 구체적인 명시를 한 것이다.
useDispatch
역시 기존과 동일하게 React-redux 라이브러리의 메소드를 사용한다.
다음은 기존처럼 action creator
를 만들어서 dispatch
를 하는 코드이다.
const Counter = () => {
const dispatch = useDispatch();
return (
<Button
title="+"
onPress={() => {
dispatch({
type: 'counterSlice/up',
step: 2,
});
}}/>
);
};
이렇게 해도 당연히 동작은 한다!
하지만, 매번 저렇게 type과 value를 써서 action을 create 하는 것은 개발하는 입장에서 굉장히 번거로운 단순작업 임에 틀림없다.
따라서 RTK 는 이를 아주 간편하게 구현해주도록 도와준다.
const Counter = () => {
const dispatch = useDispatch();
return (
...
<Button
title="+"
onPress={() => {
dispatch(counterSlice.actions.up(2));
}}
/>
);
};
action creator
대신 actions
라는 키워드와 어떤 reducer
이며 state
를 어떻게 바꿀 것인지를 위와 같이 한줄로 작성하면 끝난다.
그렇다면 dispatch
를 감지하고 reducer
에선 어떻게 수신할까?
아래와 같이 payload
를 통해 모든 것을 수신한다!!
즉, 아래 주석 친 것처럼 action creator
를 통해 본인이 정한 어떠한 데이터 키값을 넣어주는 것이 아니라, 모든 것을 action.payload
를 통해 수신한다.
const counterSlice = createSlice({
name: 'counterSlice',
initialState: {
value: 0,
},
reducers: {
up: (state, action) => {
// state.value = state.value + action.step;
state.value = state.value + action.payload;
},
},
});