리덕스 - Redux tool kit (RTK)

hwisaac·2023년 2월 8일
0

Redux

목록 보기
2/2

Redux Toolkit

  • 매번 많은 양의 Boilerplate code 를 써야 하는 것은 불편함을 야기합니다. 때때로 우리는 기본적으로 사용할 수 있는 가장 간단한 방법으로 시작하기를 원합니다. 리덕스 툴킷을 사용하면 해결할 수 있습니다!
  • 리덕스 툴킷은 shortcut을 모아놓은 패키지입니다 : configureStore, createReducer, createSlice
  • Redux Toolkit의 목표는 일반적인 Redux 사용 방식을 단순화하는 것입니다. 이것은 여러분이 Redux로 하고 싶어하는 모든 것을 해결해 주지는 않지만, 여러분이 훨씬 더 간결하게 Redux 관련 코드를 작성할 수 있게 해줍니다.

설치

  • npm install @reduxjs/toolkit 이미 앱이 있을 경우
  • npx create-react-app my-app --template redux-typescript 프로젝트를 시작할경우

Redux 코어 라이브러리

  • npm install redux redux 도 같이 설치해줘야 합니다.

Store Setup

모든 Redux 앱은 Redux 스토어를 구성하고 생성해야 합니다. 그 단계는 다음과 같습니다.

  1. root reducer 함수 가져오기 또는 생성하기
  2. middleware 설정하기 : 비동기 로직을 처리할 미들웨어를 하나 이상 포함합니다.
  3. Redux DevTools Extension 구성하기
  4. 애플리케이션이 개발용으로 구축되는지 또는 프로덕션용으로 구축되는지 여부에 따라 일부 로직 수정

Manual Store Setup

전형적인 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
}
  • 기본 Redux 의 createStore 함수는 (rootReducer, preloadedState, enhancer) 의 인자들을 취하는데, 뭐가 뭔지 까먹기 쉽다.
  • 특히 여러 구성 요소를 추가하려는 경우, middleware 및 enhancers 를 설정하는 절차가 복잡할 수 있습니다.
  • 많은 유저들이 스니펫을 복사-붙여넣기 하는데 이것은 코드를 읽기 어렵게 만듭니다.

configureStore

configureStore 를 쓰면 좋은 점.

  • 가독성 좋은, "named" 매개 변수를 가진 옵션 객체를 가집니다.
  • store에 추가할 middlewares 및 enhancers 배열을 제공하고 applyMiddleware를 호출하고 자동으로 compose 할 수 있습니다.
  • 자동으로 Redux DevTools Extension 을 활성화합니다.
  • configureStore 는 middleware 를 자동으로 추가합니다. 각각은 다음 목표를 가집니다.
    • redux-thunk 는 컴포넌트의 바깥에서 동기적인 로직와 비동기적 로직을 모두 사용하는 데 가장 일반적으로 사용되는 미들웨어입니다
    • 개발과정에서, 미들웨어는 statemutation하거나 non-serialziable values를 사용하는 것과 같은 일반적인 실수를 체크합니다.
  • store 셋업 코드는 짧아지고, 가독성이 높아지고, 디폴트로 좋은 기능을 탑재합니다.

이를 사용하는 가장 간단한 방법은 root reducer 함수를 reducer라는 매개 변수로 전달하는 것입니다

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
  reducer: rootReducer,
})

export default store

또한 "slice reducer"를 담은 객체를 전달할 수 있으며, configureStorecombineReducers 를 호출해줍니다

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는 제공한 미들웨어만 사용합니다.
  • 만약 custom middleware와 defaults 를 같이 쓰고 싶으면, callback notation 을 사용하면 됩니다:getDefaultMiddleware를 호출하고 당신이 리턴한 middleware 배열에 result 를 포함하세요

createReducer

  • 기존 리덕스에서는 데이터를 mutate 하는게 금기였지만 툴킷에서는 배열에 push 같은 걸 해도 괜찮습니다(immer 라이브러리를 이용해서 안보이는 영역에서 원본을 복사해서 처리하므로 원본을 지키는 작업이 있기 때문)
  • 일반적으로 Redux reducer에서 사용하는 switch문은 createRedcuer 를 이용해서 대체할 수 있다.
    • 각각의 casecreateReducer 에 넘기는 object 의 key 가 된다.
    • 객체를 펼치거나 배열을 복사하는 것과 같은 immutable 업데이트 로직은 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 함수에서만 작동합니다.)

createAction

  • 일일이 action creator를 작성하는 것은 지루할 수 있습니다. Redux Toolkit은 createAction이라는 함수를 제공하며, 이 함수는 단순히 주어진 action type을 사용하는 action creator를 생성하고 해당 인수를 payload 필드로 변환합니다:
// 기존코드
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 레퍼런스를 참조하십시오.

Using Action Creator as Action Types

  • redux reducer 는 state를 업데이트하는 방법을 결정하기 위해 특정 action type을 찾아야 합니다. 일반적으로 이 작업은 action type string과 action creator 함수들을 별도로 정의하여 수행됩니다.
  • createAction 함수는 몇가지 트릭으로 이것들을 쉽게 만듭니다.
  1. createAction은 생성하는 action creator의 toString() 메서드를 override합니다. 즉, builder.addCase 또는 createReducer object notation에 제공된 키값들과 같은 일부 위치에서 action creator 자신을 "action type" 참조로 사용할 수 있습니다.
  2. action type 은 또한 action creatortype 필드로서 정의됩니다.
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를 반복할 필요가 없습니다.

  • 안타깝게도 switch문에 대해 문자열로의 암시적 변환이 발생하지 않습니다.
  • switch문에서 다음 action creators 중 하나를 사용하려면 직접 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
    }
  }
}
  • 만약 당신이 TypeScript와 함께 Redux Toolkit을 사용한다면, action creator가 객체 키로 사용될 때 TypeScript 컴파일러가 암묵적인 문자열() 변환을 받아들이지 않을 수 있습니다. 이 경우 수동으로 문자열(actionCreator as string)에 캐스트하거나 .type 필드를 키로 사용해야 할 수 있습니다.

createSlice

  • Redux state는 일반적으로 "slices"로 구성되며, combineReducers에 전달되는 reducer에 의해 정의됩니다:
import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer,
})
  • 이 예제에서 userspostsslices로 간주될수 있습니다. 두 reducers 모두:

    • 초기 값이 무엇인지를 포함하여 state의 a piece를 "소유"합니다
    • 해당 state가 어떻게 업데이트 되는지 정의합니다.
    • 어떤 actions 가 어떤 state update 를 발생시키는지 정의합니다.
  • 일반적인 접근은 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
  }
}

Defining functions in objects

  • 모던한 자바스크립트에서는 객체에 키와 함수를 모두 정의하는 몇 가지 좋은 방법이 있으며(Redux에만 국한되지 않음), 다양한 키 정의와 함수 정의를 혼합하고 일치시킬 수 있습니다. 예를 들어, 다음은 객체 내부에서 함수를 정의하는 방법입니다:
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) => { }
}

createSlice 사용하기

이 프로세스를 단순화하기 위해 Redux Toolkit에는 당신이 제공하는 reducer 함수의 이름을 기반으로 action typeaction 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"}}
  • createSlicereducer 필드에 정의된 모든 함수를 살펴보았으며 제공된 모든 "case reducer" 함수에 대해 리듀서의 이름을 action type 자체로 사용하는 action creator를 생성합니다. 따라서 createPost reducer는 "posts/createPost"의 action type이 되었고, createPost() action creator는 해당 type의 action을 반환합니다.

Exporting and Using slice

대부분의 경우 당신은 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 creatorreducer를 정의하고 export를 위한 "Redux Ducks" 패턴과 개념이 매우 유사합니다. 그러나 슬라이스를 import하거나 export할 때 몇 가지 단점을 가질 수 있습니다.

  1. Redux action typesinlge slice에만 국한되지 않습니다. 개념적으로 각 슬라이스 reducer는 Redux 상태의 자체 slice를 "소유"하지만, 모든 동작 유형을 listen 하고 상태를 적절하게 업데이트할 수 있어야 합니다. 예를 들어, 서로 다른 슬라이스들이 데이터를 지우거나 초기 상태 값으로 재설정함으로써 "사용자 로그아웃" action에 응답하려고 할 수 있습니다. state 모양을 설계하고 slice를 만들 때는 이 점을 명심하십시오.
  2. JS 모듈은 두 모듈이 서로 import를 시도할 경우 "circular reference" 문제가 발생할 수 있습니다. 이로 인해 import가 정의되지 않아 해당 가져오기가 필요한 코드를 손상시킬 수 있습니다. 특히 "ducks" 또는 slice의 경우, 두 개의 서로 다른 파일에 정의된 slice가 모두 다른 파일에 정의된 작업에 응답하려는 경우에 발생할 수 있습니다.

이 문제에 대한 CodeSandbox 예제

  • 이 문제가 발생하면 circular reference를 방지하는 방식으로 코드를 다시 작성해야 할 수 있습니다. 일반적으로 공통된 코드를 별도의 파일로 따로 추출하여 두 모듈을 모두 가져와서 사용해야 합니다.
    • 이 경우 createAction을 사용하여 일반적인 action type를 별도의 파일에 정의하고 각 슬라이스 파일로 action creator를 가져온 다음 extraReducers 인수를 사용하여 작업을 처리할 수 있습니다.
  • [How to fix curcular dependency issues in JS] 문서 참고

Asynchronous Logic and Data Fetching

Using Middleware to Enable Async Logic

  • Redux store 그 자체로는 비동기 로직에 대해 아무런 정보가 없습니다.
  • 이것은 action을 동기적으로 발송하고 root reducer 함수를 호출하여 state를 업데이트하며 UI에 무언가가 변경되었음을 알리는 방법만 알고 있습니다.
  • 모든 비동기 로직은 store 밖에서 발생해야 합니다.
  • 그러나 현재 store 상태를 dispatch하거나 체크하여 비동기 로직이 store와 상호 작용하도록 하려면 어떻게 해야 할까요? 이게 Redux 미들웨어가 등장하는 이유입니다. store를 확장하여 다음을 수행할 수 있습니다:
  • actiondispatch될 때 추가 로직 실행(예: action 및 state 기록)
  • dispatched action의 일시 중지, 수정, 연기, 교체 또는 중지 등
  • dispatchgetState에 액세스할 수 있는 코드를 작성
  • function 및 promise 와 같은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를 작성할 필요가 없습니다. 사용자 앱에서 데이터 가져오기 코드를 간소화하는 데 도움이 되는지 한 번 찍먹해 보세요!

0개의 댓글