출처 : https://redux-toolkit.js.org/introduction/getting-started
리덕스 팀에서 만든 공식적인 Redux Tool이다. 홈페이지를 가보면 아래와 같이 설명이 되어있는데 자신감을 엿볼 수 있다.
The official, opinionated, batteries-included toolset for efficient Redux development
'Redux 개발을 위한 공식적인, 독단적인, 배터리포함 ToolSet'
-> Redux Toolkit을 사용하면 이러한 문제점을 해결하고, 유지보수가 쉽다!
configureStore(): createStore를 랩핑하여 간단하게 store를 만들어줍니다.
createReducer(): 복잡하게 switch 문을 작성하는 대신 간단하게 reducer를 만들어줍니다.
createAction(): 간단하게 액션들을 만들어줍니다.
createSlice(): 리듀서 함수의 객체, 슬라이스 이름, 초기 상태 값을 받아 해당 액션 생성자와 액션 유형을 가진 슬라이스 리듀서를 자동으로 생성합니다.
createAsyncThunk: 프로미스 기반의 액션들을 dispatch 하는 thunk를 생성합니다.
createEntityAdapter: 스토어에서 정규화된 데이터를 관리하기 위해 재사용 가능한 리듀서 및 셀렉터 세트를 생성합니다.
RTK Query는 강력한 data fetching, caching 툴입니다. 웹 애플리케이션에서 데이터를 가져오는 상황을 간단하게 만들어서 data fetching과 caching 로직을 스스로 작성할 필요가 없도록 만들어졌습니다.
데이터 패칭과 캐싱 로직은 Redux Toolkit의 createSlice와 createAsyncThunk API 위에서 동작합니다.
Redux Toolkit은 UI 독립적이기 때문에 RTK Query의 기능들은 모든 UI 계층에서 사용할 수 있습니다.
API 엔드포인트는 인자로부터 쿼리 파리미터를 생성하고 캐싱을 위해 응답을 변환하는 방법을 포함해서 미리 정의됩니다.
RTK Query는 데이터 패칭 프로세스를 캡슐화해서 data와 isLoading필드를 컴포넌트에게 제공하고, 컴포넌트가 mount, unmount시 캐시 된 데이터의 라이프타임을 관리하는 React hook을 제공합니다.
Redux : switch 기반으로 Action을 분기하여 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
}
}
Redux Toolkit : builder를 통해 좀더 간단하게 액션을 생성한다.
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)
})
})
Redux : 하나의 function에서 Action Type과 Payload가 정의된다.
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
Redux Toolkit : createAction으로 액션을 만들고 따로 매개변수를 받는다.
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
/* Reducer와 조합 */
const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = createReducer({}, (builder) => {
builder.addCase(actionCreator, (state, action) => {})
})
Redux : Reducer를 만들기 위해 액션 정의, 액션 함수 정의, switch를 통해 액션타입 별 코드 정의를 진행한다.
// 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
}
}
Redux Toolkit : createSlice를 통해 간단하게 reducers안에 액션을 생성하고 정의한다. 중복되는 변수 사용이 줄어든다.
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"}}
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
//createApi(): RTK Query 기능의 코어입니다. 데이터를 패치하고 변환하는 설정을 포함해서 엔드포인트들에서 어떻게 데이터를 패치하는지 정의할 수 있습니다.
//fetchBaseQuery(): 간단한 요청을 위한 fetch의 래퍼입니다. 대부분의 사용자에게 createApi의 baseQuery로 권장합니다.
const baseUrl = 'api_test.com'
// 이렇게 createRequest를 만들어서 사용하지 말것!
// 이미 baseQuery는 아래와 같은 형태를 지원하기에 이런식으로 만들필요가 없다.
//개발자 권고에 따라 삭제 : const createRequest = (url) => ({url})
export const someApi = createApi({
reducerPath: 'someApi',
baseQuery: fetchBaseQuery({
baseUrl,
// either you can just set `headers` here:
// headers: { "Accept": "application/vnd.api+json" }
// or you use `prepareHeaders` where you can do some calulations and have access to stuff like `getState` or the endpoint name
prepareHeaders: (headers, { getState, endpoint, type, forced }) => {
headers.set("Accept", "application/vnd.api+json")
return headers
}}),
endpoints: (builder) => ({
getSome: builder.query({
query: (count) => {url : `/some?limit=${count}`}
}),
getSomes: builder.query({
query: ({ coinUuid, timePeriod }) => {url :`/somes`}
}),
})
})
export const { useGetSomeQuery, useGetSomesQuery } = someApi;
//store 생성
export default configureStore({
reducer: {
[someApi.reducerPath]: someApi.reducer,
},
});
//root에 정의
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
// 컴포넌트에서 활용
const someComponents = ({count}) => {
const { data, isFetching } = useGetSomeQuery({count});
if(isFetching) return 'Loading...'
return (
<>
<p>Good!</p>
</>
)
}
개인적으로 느낀 가장 큰 장점은 state 변경에 따라 자동으로 다시 데이터를 불러오는 점이다!
아래와 같이 컴포넌트에서 state의 값을 RTK-Query의 훅에 전달하는 경우가 있는데, 이때 RTK-Query에서 해당 state의 변경을 자동으로 읽어서 다시 데이터를 가져온다. 굳이 useEffect를 사용하지 않아도, 필요한 부분을 다시 랜더링해오는 기능은 정말 편하고 유용했다.
물론 데이터 캐싱도 자동으로 되는 것도 정말 좋았지만, 여러모로 사용자 관점에서 편하고 효율적으로 사용할 수 있다는 생각이 들었다.
// 컴포넌트에서 활용
const someComponents = ({count}) => {
const [count, setcount] = useState(0);
const { data, isFetching } = useGetSomeQuery({count});
Hi,
I'm the author of RTK Query.
Please do not use that kind of
createRequest
helper function.That is taken from a tutorial that grossly misunderstands what
baseQuery
is made for (and spreading to other tutorials now).Essentially,
baseQuer
y is already acreateRequest
function like this - the return value of an endpoint'squery
function will be passed as first argument intobaseQuery
, which will in the case offetchBaseQuery
then callfetch
.So please use
fetchBaseQuery
correctly instead here:I would greatly appreciate it if you could change that up in this tutorial as this is essentially a bad practice and we do not want it to spread.