Redux는 액션을 이용한 효율적인 상태관리 라이브러리 특히 전역으로 관리하기 편하다.
Redux-Toolkit은 Redux를 만든 곳에서 공식적으로 효율적인 Redux 개발을 위해 만들어진 툴킷
Redux 작성하는 코드를 간단하게 작성, Redux 보다 훨씬 효율적이므로 사용을 권장
아래는 "(state, action) => newState" 형태의 리듀서입니다.
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
if (action.type === 'counter/increment') {
// state 복사
return {
...state,
// 새로운 밸류와 합께 업데이트
value: state.value + 1
}
}
// 별도의 액션이 없다면 그대로 반환
return state
}
이를 이용하려면 먼저 store라는 객체를 불러와야합니다.
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
react에서 데이터의 단반향 흐름을 생각해봅시다.
1. 상태는 특정 시점의 앱 상태를 설명합니다.
2. UI는 해당 상태를 기반으로 렌더링됩니다.
3. 어떤 일이 발생하면(예: 사용자가 버튼을 클릭하는 경우) 발생한 상황에 따라 상태가 업데이트됩니다.
4. 새로운 상태에 따라 UI가 다시 렌더링됩니다.
Redux에서는 이를 더 자세히 구별합니다.
Redux 공식 홈페이지 데이터흐름 gif
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
export const selectCount = (state) => state.counter.value
export default counterSlice.reducer
위의 예제를 통해 React와 Redux가 소통하는 법을 알아보도록 합시다.
위에서 store에는 counter: {value:0} 가 있는 것을 알 수 있습니다.
또 리듀서를 통해 increment, decrement, incrementByAmount 3가지 작업이 있다는 것을 알 수 있습니다.
이는 DevTools을 통해 전부 확인 할 수 있고 incrementByAmount작업은 payload또한 확인할 수 있습니다.
또 DevTools을 통해 무엇이 디스페치에 영향을 주었는지 Diff를 통해 value가 변화한 모습도 볼 수 있습니다.
/src
index.js: 앱의 시작점
App.js: 최상위 React 구성 요소
/app
store.js: Redux 스토어 인스턴스 생성
/features/counter
Counter.js: 카운터 기능에 대한 UI를 보여주는 React 컴포넌트
counterSlice.js: 카운터 기능을 위한 Redux 로직
먼저 store는 "configureStore" ReduxToolkit의 기능을 사용하여 생성됩니다.
아래처럼 reducer를 인수로 념겨줘야 합니다.
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
만약 더많은 리듀서를 가지는 블로그라면 아래와 같은 것 입니다.
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
그럼 가져오는 리듀서에 대해 알아볼면
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
위에서 처음 알아볼 것은 이전 처럼 {type: "counter/increment"} 가져오지 않는다는 것인데 위처럼 createSlice사용해 name을 통해서 반복적인 작업을 줄일 수 있습니다.
리듀서의 규칙을 적용했습니다.
규칙 1. state및 action인수를 기반으로 새 상태 값만 계산해야 합니다.
규칙 2. 기존 값을 복사 하고 복사된 값을 변경하여 불변 업데이트를 수행해야 합니다.
규칙 3. 비동기 로직 또는 기타 "side effects"을 수행해서는 안 됩니다.
위의 규칙을 지겨야하는 이유는 이후에 테스트, 직관적인 로직, 버그, Redux DevTools를 올바르게 사용할 수 있도록 합니다.
또 비동기 사용을 하고싶다면 redux-thunk을 사용해야합니다. 다행히도 Redux Toolkit의 configureStore기능은 이미 자동으로 설정되어 있으므로 바로 사용하면 됩니다.
예를 들면 아래처럼 외부의 값을 받아올 수 있습니다.
const fetchUserById = userId => {
return async (dispatch, getState) => {
try {
const user = await userAPI.fetchById(userId)
dispatch(userLoaded(user))
} catch (err) {
}
}
}
이제 이렇게 만들어진 리듀서를 컴포넌트에서 어떻게 이용할까요?
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
먼저 useSelector의 쓰임을 알아 보겠습니다.
useSelector의 역할은 store에서 특정 데이터조각을 추출하는데 있습니다.
다음 dispatch는 말 그래도 dispatch하는데 있습니다.
이쯤에서 생각하면 굳이 store에 넣어야하나라고 생각할 수 있습니다. 꼭 그런건 아닙니다. 본인 생각에 이 데이터가 전역에서 사용한다면 넣어야하지만 그것이 아니라면 꼭 넣는것 보다는 state로 이용하는 편이 좋겠습니다. 위에 const "[incrementAmount, setIncrementAmount] = useState('2')"처럼 말이죠
이를 위한 가이드도 존재합니다.
ㅁ 애플리케이션의 다른 부분에서 이 데이터를 관리합니까?
ㅁ 이 원본 데이터를 기반으로 추가 파생 데이터를 만들 수 있어야 합니까?
ㅁ 여러 구성 요소를 구동하는 데 동일한 데이터가 사용되고 있습니까?
ㅁ 이 상태를 주어진 시점으로 복원할 수 있다는 점에서 가치가 있습니까?
ㅁ 데이터를 캐시하시겠습니까(굳이 데이터를 다시 요청할 것입니까?)?
ㅁ UI 구성 요소를 리로드하는 동안 이 데이터를 일관되게 유지하시겠습니까?