npm install @reduxjs/toolkit 이미 앱이 있을 경우npx create-react-app my-app --template redux-typescript 프로젝트를 시작할경우npm install redux redux 도 같이 설치해줘야 합니다.모든 Redux 앱은 Redux 스토어를 구성하고 생성해야 합니다. 그 단계는 다음과 같습니다.
전형적인 store 셋업의 예시 : 가독성은 좋은데 직관적이지 않다 😂
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
createStore 함수는 (rootReducer, preloadedState, enhancer) 의 인자들을 취하는데, 뭐가 뭔지 까먹기 쉽다.configureStore 를 쓰면 좋은 점.
store에 추가할 middlewares 및 enhancers 배열을 제공하고 applyMiddleware를 호출하고 자동으로 compose 할 수 있습니다.Redux DevTools Extension 을 활성화합니다.middleware 를 자동으로 추가합니다. 각각은 다음 목표를 가집니다.redux-thunk 는 컴포넌트의 바깥에서 동기적인 로직와 비동기적 로직을 모두 사용하는 데 가장 일반적으로 사용되는 미들웨어입니다state를 mutation하거나 non-serialziable values를 사용하는 것과 같은 일반적인 실수를 체크합니다.이를 사용하는 가장 간단한 방법은 root reducer 함수를
reducer라는 매개 변수로 전달하는 것입니다
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
또한 "slice reducer"를 담은 객체를 전달할 수 있으며,
configureStore는combineReducers를 호출해줍니다
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
이 방법은 one level 의 reducer에만 적용됩니다. reducers 를 중첩하고 싶으면 combineReducers를 직접 호출하여 중첩을 처리해야 합니다.
store setup을 커스터마이징 하고 싶은 경우 추가 옵션을 전달할 수 있습니다.
Redux Toolkit을 사용한 hot reloading example은 다음과 같습니다:
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: [monitorReducersEnhancer],
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
configureStore는 제공한 미들웨어만 사용합니다. getDefaultMiddleware를 호출하고 당신이 리턴한 middleware 배열에 result 를 포함하세요createRedcuer 를 이용해서 대체할 수 있다. case 는 createReducer 에 넘기는 object 의 key 가 된다.mutation으로 전환될 수 있다. (변경되지 않은 업데이트를 그대로 유지하고 업데이트된 복사본을 반환하는 것도 여전히 가능)(기존 방식) "todo list" reducer
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
createReducer 를 사용한 방식
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index)
})
})
state 를 "mutate" 하는 것은 nested state 를 업데이트 할 때 매우 도움이 된다.
// 기존의 복잡한 코드
case "UPDATE_VALUE":
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
// 개선된 코드
updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}
주의: 기존 state 를 "mutate" 하는 것과, 새 state 값을 만들어서 리턴하는 방식을 함께 섞어서 사용하지 마세요! ( + mutate는
createReducer함수에서만 작동합니다.)
// 기존코드
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
// createAction 적용
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
reateAction은 또한 payload 필드의 결과를 커스터마이징하고 meta 필드를 추가할 수 있는 prepare callback 인수를 허용합니다. prepare callback을 사용하여 작업 작성자를 정의하는 방법에 대한 자세한 내용은 createAction API 레퍼런스를 참조하십시오.createAction 함수는 몇가지 트릭으로 이것들을 쉽게 만듭니다.createAction은 생성하는 action creator의 toString() 메서드를 override합니다. 즉, builder.addCase 또는 createReducer object notation에 제공된 키값들과 같은 일부 위치에서 action creator 자신을 "action type" 참조로 사용할 수 있습니다.action type 은 또한 action creator의 type 필드로서 정의됩니다.const actionCreator = createAction('SOME_ACTION_TYPE')
console.log(actionCreator.toString())
// "SOME_ACTION_TYPE"
console.log(actionCreator.type)
// "SOME_ACTION_TYPE"
const reducer = createReducer({}, (builder) => {
// actionCreator.toString() will automatically be called here
// also, if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})
// Or, you can reference the .type field:
// if using TypeScript, the action type cannot be inferred that way
builder.addCase(actionCreator.type, (state, action) => {})
})
즉, 별도의 action type variable를 쓰거나 사용하거나, const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"과 같은 action type의 name과 value를 반복할 필요가 없습니다.
actionCreator.toString()을 호출해야 합니다:const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = (state = {}, action) => {
switch (action.type) {
// ERROR: this won't work correctly!
case actionCreator: {
break
}
// CORRECT: this will work as expected
case actionCreator.toString(): {
break
}
// CORRECT: this will also work right
case actionCreator.type: {
break
}
}
}
actionCreator as string)에 캐스트하거나 .type 필드를 키로 사용해야 할 수 있습니다.combineReducers에 전달되는 reducer에 의해 정의됩니다:import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
이 예제에서 users와 posts 는 slices로 간주될수 있습니다. 두 reducers 모두:
일반적인 접근은 slice의 reducer 함수를 자신의 파일에 정의하고 action creator를 두 번째 파일에 정의하는 것d입니다. 두 함수 모두 동일한 action type을 참조해야 하기 때문에 일반적으로 세 번째 파일에 정의되고 두 위치에서 모두 imported 해옵니다:
// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// omit implementation
}
default:
return state
}
}
"ducks" 파일 구조는 주어진 슬라이스에 대한 모든 Redux 관련 로직을 다음과 같이 단일 파일에 넣을 것을 제안합니다:
// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// Omit actual code
break
}
default:
return state
}
}
const keyName = "ADD_TODO4";
const reducerObject = {
// Explicit quotes for the key name, arrow function for the reducer
"ADD_TODO1" : (state, action) => { }
// Bare key with no quotes, function keyword
ADD_TODO2 : function(state, action){ }
// Object literal function shorthand
ADD_TODO3(state, action) { }
// Computed property
[keyName] : (state, action) => { }
}
이 프로세스를 단순화하기 위해 Redux Toolkit에는 당신이 제공하는 reducer 함수의 이름을 기반으로 action type과 action creators를 자동으로 생성하는 createSlice 함수가 포함되어 있습니다.
다음은 createSlice를 사용한 posts 예제입니다:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
console.log(postsSlice)
/*
{
name: 'posts',
actions : {
createPost,
updatePost,
deletePost,
},
reducer
}
*/
const { createPost } = postsSlice.actions
console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
createSlice는 reducer 필드에 정의된 모든 함수를 살펴보았으며 제공된 모든 "case reducer" 함수에 대해 리듀서의 이름을 action type 자체로 사용하는 action creator를 생성합니다. 따라서 createPost reducer는 "posts/createPost"의 action type이 되었고, createPost() action creator는 해당 type의 action을 반환합니다.대부분의 경우 당신은 slice를 정의하고 action creator 및 reducers를 export할 것입니다. 권장되는 방법은 ES6 destructuring 및 export 구문을 사용하는 것입니다:
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer
원하는 경우 슬라이스 object 자체를 직접 export할 수도 있습니다.
이렇게 정의된 슬라이스는 action creator 및 reducer를 정의하고 export를 위한 "Redux Ducks" 패턴과 개념이 매우 유사합니다. 그러나 슬라이스를 import하거나 export할 때 몇 가지 단점을 가질 수 있습니다.
action type은 sinlge slice에만 국한되지 않습니다. 개념적으로 각 슬라이스 reducer는 Redux 상태의 자체 slice를 "소유"하지만, 모든 동작 유형을 listen 하고 상태를 적절하게 업데이트할 수 있어야 합니다. 예를 들어, 서로 다른 슬라이스들이 데이터를 지우거나 초기 상태 값으로 재설정함으로써 "사용자 로그아웃" action에 응답하려고 할 수 있습니다. state 모양을 설계하고 slice를 만들 때는 이 점을 명심하십시오.import를 시도할 경우 "circular reference" 문제가 발생할 수 있습니다. 이로 인해 import가 정의되지 않아 해당 가져오기가 필요한 코드를 손상시킬 수 있습니다. 특히 "ducks" 또는 slice의 경우, 두 개의 서로 다른 파일에 정의된 slice가 모두 다른 파일에 정의된 작업에 응답하려는 경우에 발생할 수 있습니다.이 문제에 대한 CodeSandbox 예제
circular reference를 방지하는 방식으로 코드를 다시 작성해야 할 수 있습니다. 일반적으로 공통된 코드를 별도의 파일로 따로 추출하여 두 모듈을 모두 가져와서 사용해야 합니다. createAction을 사용하여 일반적인 action type를 별도의 파일에 정의하고 각 슬라이스 파일로 action creator를 가져온 다음 extraReducers 인수를 사용하여 작업을 처리할 수 있습니다.store 그 자체로는 비동기 로직에 대해 아무런 정보가 없습니다. action을 동기적으로 발송하고 root reducer 함수를 호출하여 state를 업데이트하며 UI에 무언가가 변경되었음을 알리는 방법만 알고 있습니다. store 밖에서 발생해야 합니다.store 상태를 dispatch하거나 체크하여 비동기 로직이 store와 상호 작용하도록 하려면 어떻게 해야 할까요? 이게 Redux 미들웨어가 등장하는 이유입니다. store를 확장하여 다음을 수행할 수 있습니다:action이 dispatch될 때 추가 로직 실행(예: action 및 state 기록)dispatched action의 일시 중지, 수정, 연기, 교체 또는 중지 등dispatch 및 getState에 액세스할 수 있는 코드를 작성plain action object 대신 다른 value 들을 accpt 하는 방법을 dispatch 에게 알려줍니다. (도중에 가로채서 real action object 를 dispatch 함)store와 상호 작용할 수 있도록 하기 위해서입니다. 이렇게 하면 UI와 별도로 로직를 유지하면서 action을 발송하고 store 상태를 확인할 수 있는 코드를 작성할 수 있습니다.Redux 에 관한 여러 종류의 비동기 미들웨어가 있는데, 각자 다른 문법으로 로직을 작성해야 합니다. 가장 보편적으로 쓰이는 비동기 미들웨어는 다음과 같습니다.
redux-thunk 비동기 로직을 직접 포함하는 plain function 을 작성하게 해줍니다redux-saga 이것은 미들웨어에 의해 실행될 수 있도록 descriptions of behavior를 리턴하는 generator 함수를 사용합니다.redux-observable RxJS observable 라이브러리를 사용해서 actions 를 처리하는 함수의 체인을 생성합니다.TIP
Redux Toolkit의 RTK Query data fetching API는 Redux 앱을 위한 목적에 맞게 구축된 데이터 패칭 및 캐싱 솔루션이며, 데이터 가져오기를 관리하기 위해thunk또는reducer를 작성할 필요가 없습니다. 사용자 앱에서 데이터 가져오기 코드를 간소화하는 데 도움이 되는지 한 번 찍먹해 보세요!