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
를 작성할 필요가 없습니다. 사용자 앱에서 데이터 가져오기 코드를 간소화하는 데 도움이 되는지 한 번 찍먹해 보세요!