Redux부터 react-redux , redux toolkit 까지

이민석·2023년 1월 16일
2

Frontend

목록 보기
1/5

Redux란?

과거 프론트엔드는 전역 데이터를 관리하는 방법이, 상위 element에서 하위 element로 값을 보내주고, 하위 element에서 데이터의 수정이 발생하면 상위 element로 업데이트 해주는 MVC 아키텍쳐 방식이었습니다. 그러나, 2014년 페이스북의 채팅 버그로 인해 데이터가 단방향으로 흐르고, 데이터의 변화를 예측할 수 있는 방법을 고안하게 되었습니다. 그 결과 Flux 아키텍쳐가 고안되었고, redux는 flux 아키텍쳐를 구현하기 위해 탄생되었습니다.


redux를 사용하면, 위와 같이 데이터의 변화가 일어나면, store로 변화가 전달되고, store에서 모든 데이터들을 받아옴으로써, 데이터 변화의 흐름을 예측하고 제어할 수 있습니다.

STORE

리덕스가 데이터를 저장하는 저장소를 의미합니다.

REDUCER

현재 state와 action 을 참고하여, store의 저장된 데이터를 바꾸는 함수입니다.
redux는 예측불가능한 state의 변화를 허용하지 않기때문에, store에 저장된 state들은 reducer을 통해서만 업데이트 될 수 있습니다.

ACTION , DISPATCH

state를 업데이트 하고 싶을때, 우리는 reducer에게 action을 보내고, action을 전달하는 매체가 dispatch 입니다.

action은 다음과 같이 객체 형식으로 되어있습니다.

{
  type : "UPDATE",
  payload : {
    value : 20,
  }
}

action을 reducer에게 전달하기 위해서는 dispatch(action)처럼 액션객체를 dispatch 함수의 인자로 전달해야합니다.

react-redux

이러한 redux를 react에서 쉽게 사용할 수 있도록 도와주는 라이브러리가 바로 react-redux입니다!

createStore()

store을 생성해주는 함수입니다.
createStore의 인자로는 reducer을 전달해야합니다.

const store = createStore(reducer);

reducer = (currentState , action) =>{
  	if(currentState === undefined){
      return { number : 1};
    }
	const newState = {...currentState};
  	//newState에 대한 변경
  	return newState;
}

reducer은 불변성을 지켜야하기 때문에, spread operator을 사용해 기존 State를 복제하고, 여기에 변경을 가해서 newState를 return 해줍니다.
추가적으로, currentState 가 undefined 일때. 즉, 초기 state값을 지정해줄수 있습니다.

Provider

하위 element들에게 store의 접근을 허용하기 위해서, Provider의 prop으로 store을 전달해주면 됩니다.

<Provider store = {store}>
  <하위 element들>
<Provider/>

useSelector

store에서 state를 read하기 위해 사용하는 함수입니다.

const number = useSelctor(state => state.number);
위와 같이 작성하면, state에서 number 라는 요소를 받아올 수 잇습니다.

useDispatch

state를 변경하고 싶을때,
useDispatch({type = "INCREMENT" , payload : 1});
와 같이 action의 type과 payload 를 객체형식으로 useDispatch의 인자로 넣어주면, reducer에게 전달되어 state의 변화가 일어나게 됩니다.

Redux Toolkit

그러나, redux는 스토어의 환경설정이 복잡하고, 특정 일을 하기위해서 매번 작성해야하는 상용구 코드가 많다는 단점이 존재했습니다.
그러한 redux를 좀 더 쉽고 유용하게 사용할 수 있도록 도와주는 라이브러리가 redux-toolkit입니다.(이하 RTK)

이제 RTK의 에러 API를 살펴보면서, RTK를 사용할때 어떠한 이점이 있는지 알아봅시다.

configureStore()

import { configureStore } from '@reduxjs/toolkit'

// We'll use redux-logger just as an example of adding another middleware
import logger from 'redux-logger'

// And use redux-batched-subscribe as an example of adding enhancers
import { batchedSubscribe } from 'redux-batched-subscribe'

import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'

const reducer = {
 todos: todosReducer,
 visibility: visibilityReducer,
}

const preloadedState = {
 todos: [
   {
     text: 'Eat food',
     completed: true,
   },
   {
     text: 'Exercise',
     completed: false,
   },
 ],
 visibilityFilter: 'SHOW_COMPLETED',
}

const debounceNotify = _.debounce(notify => notify());

const store = configureStore({
 reducer,
 middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
 devTools: process.env.NODE_ENV !== 'production',
 preloadedState,
 enhancers: [batchedSubscribe(debounceNotify)],
})

configureStore는 reducer , middleware , devTools , preloadedState , enhancers를 인자로 받습니다.

reducer

단일 함수로 전달 될경우, root reducer로 바로 사용되고 / object로 전달될 경우 내부적으로 combineReducers을 사용하여 자동적으로 병합하여 root reducer로 만들어줍니다.

middleware

기본적으로는 getDefaultMiddleWare를 호출합니다. middleware들을 배열로 전달하면, getDefaultMiddleWare을 호출하지 않고, 전달한 middleware들만 사용하게 됩니다.
getDefaultMiddleware들과 함께 사용하고 싶다면, array.concat method등을 사용하여, 추가해줄 수 있습니다.
예제에서 사용된 redux-logger의 logger middleware는 console에 redux관련 정보들을 출력하게 해주는 middleware입니다.

devTools

redux 개발자 도구를 끄거나 켤 수 있는 옵션입니다. 기본적으로 true값을 가집니다.
만약에 objcet 인자를 전달할 경우, composeWithDevtools()의 인자로 objcet가 전달됩니다.

preloadedState

store의 initial state를 설정할 수 있습니다.

enhancers

배열과 콜백함수 둘 중 하나가 올 수 있는데,
배열로 정의된 경우에 리덕스의 compose 함수로 전달되어 createStore 함수로 전달된다.
콜백 함수로 정의된 경우에 applyMiddleware 보다 앞에 추가할 수 있다.
원하는 enhancer를, middleware들보다 앞에 추가하고 싶은 경우 다음과 같이 할 수 있다.
enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers]
그러면 다음과 같은 순서로 적용된다.
[offline, applyMiddleware, devToolsExtension]

createReducer()

리듀서 함수를 생성하는 함수이다. 내부적으로 immer 라이브러리가 사용되어서, 자동적으로 작성한 코드를 mutative하게 바꿔준다. 따라서, createReducer()을 사용하면, 기존에 mutate함을 위해 노력했던 것들을 하지 않아도 된다.

RTK 를 사용하지 않았을 때는, reducer를 작성할때 보통 switch문을 사용했다.

RTK에서 reducer을 작성하는 방법은 두가지가 있다.

Builder Callback Notation

Builder Callback Notation은 typescript와 호환성이 좋기 때문에 선호되는 방법이다.

import {
  createAction,
  createReducer,
  AnyAction,
  PayloadAction,
} from '@reduxjs/toolkit'

const increment = createAction<number>('increment')
const decrement = createAction<number>('decrement')

function isActionWithNumberPayload(
  action: AnyAction
): action is PayloadAction<number> {
  return typeof action.payload === 'number'
}

const reducer = createReducer(
  {
    counter: 0,
    sumOfNumberPayloads: 0,
    unhandledActions: 0,
  },
  (builder) => {
    builder
      .addCase(increment, (state, action) => {
        // action is inferred correctly here
        state.counter += action.payload
      })
      // You can chain calls, or have separate `builder.addCase()` lines each time
      .addCase(decrement, (state, action) => {
        state.counter -= action.payload
      })
      // You can apply a "matcher function" to incoming actions
      .addMatcher(isActionWithNumberPayload, (state, action) => {})
      // and provide a default case if no other handlers matched
      .addDefaultCase((state, action) => {})
  }
)

createReducer 함수는 첫번째 인자로 initialState를 받는다. 이는 State | (()=>State) 즉, state나 state를 return하는 함수가 올 수 있다.
두번째 인자는 builderCallback이다. builder객체를 인자로 받는 함수이다.

Builder Methods

builder객체의 메소드는 총 3가지가 존재한다.

  1. builder.addCase(actionCreator , reducer)
    주어진 action이 actionCreator와 일치하면, reducer를 작동시킨다.

  2. builder.addMatcher(matcher , reducer)
    matcher는 타입스크립트에서 type prediction function이다.
    주어진 action의 type이 matcher에 부합하면, reducer를 작동시킨다.

  3. addDefaultCase(reducer)
    위에서 작성된 어떠한 addCase와 addMatcher에 부합하지 않으면, 작동될 기본 reducer을 추가하는 메소드이다.

Builder Method의 순서는 반드시 addCase -> addMatcher -> addDefaultCase가 되어야한다.

addCase vs addMatcher

처음 Builder Methods를 접했을때, 둘의 차이가 무엇인지 궁금했다.
전자는 action이 정확히 일치하면 reducer가 실행되는 것이고, 후자는 action의 type이 조건을 만족하면 reducer가 실행되는 것인데, addMatcher을 addCase로 대체할 수 있지 않을지 의문이 들었다.

이는 아래의 예제 코드를 살펴보면 이해할 수 있을 것이라 생각한다.

import {
  createAction,
  createReducer,
  AsyncThunk,
  AnyAction,
} from '@reduxjs/toolkit'

type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>

type PendingAction = ReturnType<GenericAsyncThunk['pending']>
type RejectedAction = ReturnType<GenericAsyncThunk['rejected']>
type FulfilledAction = ReturnType<GenericAsyncThunk['fulfilled']>

const initialState: Record<string, string> = {}
const resetAction = createAction('reset-tracked-loading-state')

function isPendingAction(action: AnyAction): action is PendingAction {
  return action.type.endsWith('/pending')
}

const reducer = createReducer(initialState, (builder) => {
  builder
    .addCase(resetAction, () => initialState)
    // matcher can be defined outside as a type predicate function
    .addMatcher(isPendingAction, (state, action) => {
      state[action.meta.requestId] = 'pending'
    })
    .addMatcher(
      // matcher can be defined inline as a type predicate function
      (action): action is RejectedAction => action.type.endsWith('/rejected'),
      (state, action) => {
        state[action.meta.requestId] = 'rejected'
      }
    )
    // matcher can just return boolean and the matcher can receive a generic argument
    .addMatcher<FulfilledAction>(
      (action) => action.type.endsWith('/fulfilled'),
      (state, action) => {
        state[action.meta.requestId] = 'fulfilled'
      }
    )
})

RTK에서 비동기를 처리할때는 AsyncThunk를 사용한다. 위의 예제는 Asyncthunk를 사용했을때, resposne가 'pending'인지 'rejected'인지, 'fulfilled'인지에 따라서, state를 바꾸는 코드이다.
CreateAsyncThunk()메소드는 첫번째 인자로 typePrefix를 받는데, 이에 따라 비동기 요청의 결과로 typePrefix + '/pending' , 'fulfilled' , 'rejected' 액션타입을 생성시킨다.
비동기 요청의 typePrefix는 매번 다르기 때문에, 여러 비동기 요청에 대해서, 모두 통용될 수 있는 reducer을 작성하기 위해서는 위와같이 action의 type이 끝나는 패턴을 분석해야 하는 것이다.
결론적으로, typePrefix는 다르지만, pending, fulfilled, rejected가 동일하면 같은 reducer을 작동시키고 싶다면, addMacther을 사용해야한다.

Map Object Notation

builder callback 표기법보다 단순하고, JS를 사용하는 프로젝트에 유효한 방법이다.

const increment = createAction('increment')
const decrement = createAction('decrement')

const counterReducer = createReducer(0, {
  [increment]: (state, action) => state + action.payload,
  [decrement.type]: (state, action) => state - action.payload
})

Matcher와 Default Case를 사용하고 싶은 경우 아래와 같이 작성하면 된다.

const isStringPayloadAction = (action) => typeof action.payload === 'string'

const lengthOfAllStringsReducer = createReducer(
  // initial state
  { strLen: 0, nonStringActions: 0 },
  // normal reducers
  {
    /*...*/
  },
  //  array of matcher reducers
  [
    {
      matcher: isStringPayloadAction,
      reducer(state, action) {
        state.strLen += action.payload.length
      },
    },
  ],
  // default reducer
  (state) => {
    state.nonStringActions++
  }
)

createAction()

기존에 Redux에서 action을 정의하기 위해서는
다음과 같이 action type 상수와 action 생성자 함수를 분리해서 선언해야 했다.

const INCREMENT = 'counter/increment'

function increment(amount: number) {
  return {
    type: INCREMENT,
    payload: amount,
  }
}

const action = increment(3)
// { type: 'counter/increment', payload: 3 }

createAction은 이를 하나로 합친 메소드이다.

import { createAction } from '@reduxjs/toolkit'

const increment = createAction<number | undefined>('counter/increment')

const action = increment(3)
// returns { type: 'counter/increment', payload: 3 }

또한, toString() 메서드를 오버라이딩 해서, 위의 예제에서 increment.toString()의 결과는 'counter/increment'가 되고, 따라서 아래처럼, map object notation에서
액션 생성자를 키로 사용하는 것이 가능한 것이다.

const increment = createAction<number>('counter/increment')

const counterReducer = createReducer(0, {
  [increment]: (state, action) => state + action.payload,
  [decrement.type]: (state, action) => state - action.payload
})

Customize Action Contents

일반적으로 createAction은 단일 인자를 받아서 action.payload를 생성한다.
payload뿐만 아니라, action이 생성되는 시점, action의 id 등을 만들고 싶은 경우에는 prepare 함수를 사용하면 된다.
prepare은 createAction의 두번째 인자의 callback으로 제공된다.
prepare은 payload를 전처리하는 미들웨어 느낌으로 이해하면 될 것 같다. 아래에서 소개되지만, prepare을 통해서 기본 payload값도 설정할 수 있다.

import { createAction, nanoid } from '@reduxjs/toolkit'

const addTodo = createAction('todos/add', function prepare(text: string) {
  return {
    payload: {
      text,
      id: nanoid(),
      createdAt: new Date().toISOString(),
    },
  }
})

console.log(addTodo('Write more docs'))
/**
 * {
 *   type: 'todos/add',
 *   payload: {
 *     text: 'Write more docs',
 *     id: '4AJvwMSWEHCchcWYga3dj',
 *     createdAt: '2019-10-03T07:53:36.581Z'
 *   }
 * }
 **/

createSlice()

내부적으로 createAction()과 createReducer()을 사용해서, slice즉, 작은 state 단위마다 action과 reducer을 만들어주는 메소드이다.
createSlice를 통해서, state에 속해있는 name, id 등의 속성마다 reducer을 따로따로 붙여줄 수 있다.

function createSlice({
    // A name, used in action types
    name: string,
    // The initial state for the reducer
    initialState: any,
    // An object of "case reducers". Key names will be used to generate actions.
    reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
    // A "builder callback" function used to add more reducers, or
    // an additional object of "case reducers", where the keys should be other
    // action types
    extraReducers?:
    | Object<string, ReducerFunction>
    | ((builder: ActionReducerMapBuilder<State>) => void)
})

첫번째 parameter는 slice의 name이다.
두번째 parameter는 initialState이다.
세번째 parameter는 reducers로, action의 이름과 reducer함수가 key와 value 쌍으로 object 형태로 제공되어야 한다.
네번째 parameter는 extraReducers로 필요한 경우에만 작성하면 된다.

reducers

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
  },
})
// Will handle the action type `'counter/increment'`

위와 같은 경우에는 action의 type이 increment인데, slice에서 action type은 slice의 이름과 합쳐서 정해진다. 따라서, 최종 action type은 counter/increment이다.

추가적으로, action을 customize하고 싶은 경우 prepare을 사용할수도 있다.

interface Item {
  id: string
  text: string
}

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Item[],
  reducers: {
    addTodo: {
      reducer: (state, action: PayloadAction<Item>) => {
        state.push(action.payload)
      },
      prepare: (text: string) => {
        const id = nanoid()
        return { payload: { id, text } }
      },
    },
  },
})

action.type의 value로 reducer와 prepare가 담긴 객체를 제공하면 되는데, prepare는 reducer 함수 전에 제공되는 전처리 미들웨어 같은 느낌이다.

extraReducers

createSlice메소드는 reducers의 인자로 전달한 reducer와 action을 생성한다. extraReducers는 현재 slice에서 생성된 reducer가 아닌, 외부에서 생성된 reducer에 접근하려는 의도를 가지고 있다.

다른 slice에서 생성한 reducer에 현재 slice의 state가 영향을 받는 경우

상점에서 보유한 상품을 state로 모델링한다고 가정해보자.
상점에서는 공책을 판매하는데, 공책을 주문한 모든 사람에게 서비스로 연필을 1개씩 준다고 해보자.
그러면, 공책이 팔리는 action이 호출되면, 공책의 개수가 줄어듬과 동시에, 연필의 개수도 1개 줄어야 할 것이다.
위와 같은 상황에서 연필의 state를 관리하는 slice에 extraReducers로 다음과 같이 쓸 수 있다.

extraReducers: {
    ['note/ordered']: (state) => {
      state.numOfpencils -= 1;
    },

여기서 ‘note/ordered’는 공책에 주문되는 action이다.

비동기 요청과 함께 사용될때

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)

    return response.data
  }
)

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {},
  // extraReducers에 케이스 리듀서를 추가하면 
  // 프로미스의 진행 상태에 따라서 리듀서를 실행할 수 있습니다.
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {})
      .addCase(fetchUserById.fulfilled, (state, action) => {
	      state.entities.push(action.payload)
      })
      .addCase(fetchUserById.rejected, (state) => {})
  },
})

createAsyncThunk에서 비동기 요청을 통해서 생성된 action에 따라서, 처리를 하고 싶은경우 위와 같이 사용하면 된다.

예제

import { createSlice, createAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createStore, combineReducers } from 'redux'

const incrementBy = createAction<number>('incrementBy')
const decrementBy = createAction<number>('decrementBy')

const counter = createSlice({
  name: 'counter',
  initialState: 0 as number,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
    multiply: {
      reducer: (state, action: PayloadAction<number>) => state * action.payload,
      prepare: (value?: number) => ({ payload: value || 2 }), // fallback if the payload is a falsy value
    },
  },
  // "builder callback API", recommended for TypeScript users
  extraReducers: (builder) => {
    builder.addCase(incrementBy, (state, action) => {
      return state + action.payload
    })
    builder.addCase(decrementBy, (state, action) => {
      return state - action.payload
    })
  },
})

const user = createSlice({
  name: 'user',
  initialState: { name: '', age: 20 },
  reducers: {
    setUserName: (state, action) => {
      state.name = action.payload // mutate the state all you want with immer
    },
  },
  // "map object API"
  extraReducers: {
    // @ts-expect-error in TypeScript, this would need to be [counter.actions.increment.type]
    [counter.actions.increment]: (
      state,
      action /* action will be inferred as "any", as the map notation does not contain type information */
    ) => {
      state.age += 1
    },
  },
})

const reducer = combineReducers({
  counter: counter.reducer,
  user: user.reducer,
})

const store = createStore(reducer)

store.dispatch(counter.actions.increment())
// -> { counter: 1, user: {name : '', age: 21} }
store.dispatch(counter.actions.increment())
// -> { counter: 2, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply(3))
// -> { counter: 6, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply())
// -> { counter: 12, user: {name: '', age: 22} }
console.log(`${counter.actions.decrement}`)
// -> "counter/decrement"
store.dispatch(user.actions.setUserName('eric'))
// -> { counter: 12, user: { name: 'eric', age: 22} }

위의 예제에서는 prepare이 action의 payload값이 주어지지 않았을때의 case에 적용되도록 사용하였다.
createSlice를 이해했다면, 위의 예제를 이해할 수 있을 것이다.

createAsyncThunk

액션 타입 문자열과 프로미스를 반환하는 콜백을 인자로 받아서, 프로미스 생명 주기 기반의 액션을 생성한다.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId: number, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

interface UsersState {
  entities: []
  loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState = {
  entities: [],
  loading: 'idle',
} as UsersState

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    // Add reducers for additional action types here, and handle loading state as needed
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
      state.entities.push(action.payload)
    })
  },
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

세번째 parameter로 options를 받을 수 있는데, 오류를 처리하거나 thunk를 취소하는 등의 작업이 가능하다.
꼭 서버에서 비동기 요청을 처리할때만 사용하는 것이 아니라, 비즈니스 로직을 비동기형태로 구현할때도 사용할 수 있다. (modal을 띄워서 사용자에게 정보제공 동의 받기 등)

createSelector()

store에서 가져온 현재 state를 가공하는 것을 도와주는 메소드이다. Reselect 라이브러리에서 제공하는 함수를 그대로 따왔다고 한다.

redux는 useSelector함수를 지원했지만, 컴포넌트들이 불필요하게 리렌더링되는 현상을 막기위해서 createSelector을 도입했다.

예를 들어서
const users = useSelector( (state) => state.users.filter(user => user.subscribed) );
useSelector을 사용한 경우, 컴포넌트가 리렌더링 될때마다 users를 구하기 위한 연산이 새로 이루어진다. 만약 이 로직이 복잡하다면, 충분히 성능저하가 될만한 이슈일 것이다.

createSelector는 메모제이션을 기반으로, parameter의 변경여부를 검사해서, parameter가 변경되었을때만 새롭게 로직을 실행한다.

state에는 user.name과 user.age라는 property가 존재한다고 생각해보자.

  
const nameSelector = (state): string =>
  state.user.name || initialState.name;

const ageSelector = (state): number =>
  state.user.age || initialState.age;

export const greetingSelector = createSelector(
  nameSelector,
  ageSelector,
  (name, age) => {
    return `My name is ${name}. I'm ${age} years old.`;
  }
);

먼저, createSelector에 사용될 state를 선언해야한다.
그리고 그 state들을 createSelecotr의 parameter로 전달하면 된다.
createSelector는 n개의 parameter을 받는데, n-1번째 까지는 새로운 값을 계산하기 위해 필요한 state를 받고, 마지막에는 지금까지 받았던 state를 사용해서 새로운 return 값을 계산하는 로직을 넣으면 된다.
greeting Selector 는 nameSelector와 ageSelector가 변경되었을때만 리렌더링을 한다.

const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent

// subtotal 값을 메모이제이션 합니다.
const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((subtotal, item) => subtotal + item.value, 0)
)

// 메모이제이션된 subtotal 값과 taxPercentSelector를 합성하여
// 새로운 값을 메모이제이션 합니다.
const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

const exampleState = {
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
}

console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState))      // 0.172
console.log(totalSelector(exampleState))    // { total: 2.322 }

위의 예시는 createSelector를 계층적으로 적용함으로써, state가 변경되었을때 필수적인 컴포넌트들만 리렌더링하게 된다.
만약 useSelector을 사용했다면, state 중 하나만 변경되었어도 모든 component들이 리 렌더링 되었을 것이다.

마무리

이번기회를 통해서 RTK를 자세히 파헤쳐보고, RTK의 숨은 기능에 대해서 많이 알게된 것 같다.
사실 실무에 어떻게 적용할 수 있을지는 잘 감이 안온다....
그래도 다음 프로젝트는 꼭 RTK를 사용해서 효율적인 상태관리를 해보고 싶다.

출처 : https://yamoo9.github.io/react-master/lecture/rd-redux.html
https://blog.hwahae.co.kr/all/tech/tech-tech/6946
https://www.youtube.com/watch?v=yjuwpf7VH74
https://redux-toolkit/js.org
https://velog.io/@hyunn/Redux-Toolkit-reducers-vs-extraReducers-비동기-처리
https://velog.io/@seongkyun/useSelector-제대로-사용하기

0개의 댓글