(TIL) Redux : RTK 열어보기

김동우·2021년 10월 27일
0

Redux

목록 보기
1/2

1.

반갑습니다. 오랜만입니다.

취업 후 일주일 정도 여유를 가지고 있습니다.

매일 알고리즘 풀이도 조금씩 하며 시간을 보내느라 오랜만에 글을 적게 되네요.

11월 1일 출근 전까지 그래도 회사 스택에 대한 공부를 하며 시간을 보내볼까 합니다.

많은 것들을 공부하기에는 현재 고향에 내려와있어 조금 어려울 것 같지만, 그래도 Redux, Redux toolkit / saga, React query 정도는 한 번 사용해볼까 합니다.

오늘은 Counter를 제공하는 기본 template을 활용해 Redux를 뜯어보며 이해해보겠습니다.

2. template 뜯어보기

먼저, RTK, Redux Toolkit Quick Start 방법 중 하나인 CRA template을 생성해야 합니다.

npx create-react-app my-app --template redux-typescript

명령어를 터미널에 입력해 typescript template를 생성할 수 있습니다.

해당 프로젝트의 기본 구성은 4가지 카운터 기능입니다.

index 파일부터 한 번 열어보며 생각해보겠습니다.

Redux를 사용해본 적 없고, 기본 개념 정도만 알고 있는 상태로 RTK를 공부하는 글입니다. 따라서 깊은 분석이 아닌 유추와 적당한 검색을 통해 해결해나가는 과정을 기록하겠습니다.

2.1 index.tsx

Index 파일에는 우리가 쉽게 볼 수 있는 ReactDOM.render 메서드 호출이 있습니다.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

주목해야 할 부분은 HOC, Provider가 하나 있고, store 라는 것을 불러와 Providerprops로 사용한다는 점입니다.

대충 생각해봤을 때, RNNavigation, ReactRouter와 같이 HOC로 다른 컴포넌트를 감싸 무언가 전달하려고 하는구나 정도로 생각이 됩니다.

그렇다면 Provider 먼저 확인해봅시다.

Provider

Provider는 어떤 컴포넌트일까요?

먼저 index.d.ts 파일을 확인해보면,

export interface ProviderProps<A extends Action = AnyAction> {
    store: Store<any, A>;
    context?: Context<ReactReduxContextValue> | undefined;
    children?: ReactNode;
}

export class Provider<A extends Action = AnyAction> extends Component<ProviderProps<A>> { }

Provider라는 컴포넌트는 Context API에서의 개념과 거의 동일하게 보입니다.

ProviderProps의 interface 내부에는 store, context, children 등의 개념들이 자리하고 있습니다.

contextchildren은 이름만 봐도 React 유저라면 어라? 할 수 있을 정도의 친숙한 이름이지만, Redux만의 특별한 개념은 store에 있는 것 같습니다.

다른 2개의 프로퍼티, context, child는 옵션이지만, store는 필수로 구현해야 합니다.

그렇다면 store는 대체 뭘까요?

천천히 열어보며 어떤 개념일지 유추해봅시다.

2.2 store.ts

그럼 자연스럽게 store.ts 파일을 확인해봅시다.

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

결과적으로 index.tsx에서 사용하는 store 객체는 configureStore() 메서드의 return 값입니다.

인자로는 {reducer : {counter : counterReducer}}를 전달받고 있네요.

아래로는 AppDispatch, RootState, AppThunk 등의 타입이 정의되어 있습니다.

여기서 새로운 typescript 활용이 나오는데, ReturnType<T> 입니다.

RootState의 경우 <typeof store.getState>ReturnType 제네릭에 넣어줌으로써store.getStatereturn 값 타입으로 추론하라는 말이 됩니다.

그렇다면 store 객체는 어떻게 구성되었길래, dispatch, getState 등의 내부 메서드를 보유하고 있는걸까요?

그리고 ThunkAction은 대체 뭘까요?

하나씩 파헤쳐봅시다.

Action, Store, View

잠시 개념을 보자면, Reduxstore라는 하나의 객체에서 앱 전체의 상태를 관리한다고 합니다.

나아가 ReduxAction => Store => View 구조로 동작하고 있습니다.

단, Viewuser interaction을 담당하기 때문에 ViewAction의 Trigger가 될 수 있습니다.

그렇기에 View => Action => Store => View 의 순환구조로 이해할 수 있습니다.

그 외에도 다양한 개념이 있는데, 튜토리얼 단계에서 이해해야 할 것은 Action, Store, Viewcirculation 정도인 것 같습니다.

또한 실제 순환이라는 개념에 맞도록 Simplex, 단방향으로 통신하는 구조를 갖습니다.

"영향을 끼칠 수 있는 존재는 정해져있다." 가 대부분 순환 시스템의 핵심이라는 생각이 강해서인지 미리 생각해두면 확 와닿는 것 같습니다.

이제 개념은 대충 정리되어 있으니, 관계별로 필요한 내용들을 조금씩 끼얹으며 코드 분석을 해봅시다.

configureStore

앞서, StoreView에 영향을 끼치는 존재인 것을 확인했습니다.

우선은 RTK의 configureStore() 메서드의 선언을 보며 store 객체가 어떤 항목들을 포함하고 있는지 확인할 수 있기는 한데...

export declare function configureStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = 
    [ThunkMiddlewareFor<S>]>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M>;

그런데 사실 선언을 봐도 잘 모르겠습니다.

간단하게 console.log()를 찍어보면,

store는 이런 구성으로 이루어져 있고, configureStorereturn 결과는 해당 메서드들로 구성된 객체라는 것을 알 수 있습니다.

type 선언으로 유추하고자 한다면 EnhancedStore => Store 의 방법으로도 확인할 수 있습니다.

또한 store 내부에는 reducer를 포함하는 등의 내용은 존재하지 않습니다.

이유는 간단합니다. 다른 개념이니까요.

Reducer의 개념은 잠깐 아래 항목에서 다루겠습니다.

그러나 store를 생성하는 시점에 store가 프로젝트의 Reducer를 인식할 수 있는 이유는

configureStore 메서드는 기존 createStore() 메서드의 결과와 달리 parameter로 넘겨준 객체의 reducer {Object}에 대해 자동으로combineReducer() 메서드를 실행하기 때문입니다.

원리는 store를 정의하는 시점에서 root reducer에 해당 reducer를 할당하는 구조인 것 같은데, 기존 Redux에 비해 얼마나 편해진것인지 체감하기엔 전례가 없어 모르겠습니다.

parameter를 관찰하는 방법은

RTK 공식문서 configureStore

또는 type 선언 중 ConfigureStoreOptions를 참고하는 방법이 있습니다.

요약하자면,

  1. configureStore 는 dispatch, subscribe, getState 등의 메서드를 포함하는 객체를 반환한다.

아직은 어디다 쓰는지 모른다.

  1. store를 configureStore로 정의하는 시점에 전달되는 reducer 객체는 combinedReducer 메서드를 통해 rootReducer에 할당되는 것처럼 보인다.

rootReducer는 또 뭘까.

정도로 요약할 수 있습니다.

이어서,

Reducer

돌아돌아 결국 Action => Store => View의 관계에 필요한 Reducer라는 키워드를 알 수 있었습니다.

rootReducer 단어를 먼저 접하긴 했지만, 사실 ReduxStoreAction 사이에 Reducer라는 특별한 레이어가 존재합니다.

Reducer 는 다시 특정 ActionStoreState로 구성됩니다.

다시 생각하면

View => Action => Reducer => Store => View 의 구조로 순환하는게 Redux라고 볼 수 있습니다.

그럼 이 Reducer의 예시 코드를 한 번 볼까요?

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = "loading";
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = "idle";
        state.value += action.payload;
      });
  },
});

이것저것 잔뜩 정의되어 있습니다.

이 중, reducers property를 보면, 함수 3개가 보입니다.

그 중 action을 가지고 있는 함수는 incrementByAmount() 라는 이름의 함수입니다.

그렇다면 나머지 둘은 action이 존재하지 않는 걸까요?

엄연히 얘기하면 그건 아닙니다.

그 전에 사실 타고 들어가자면 selector, dispatch 개념도 먼저 알아야 하는데, Storestate를 직접 변경하지 않도록 하는 하나의 장치 정도로 얕게 이해하고 넘어갑시다.

실제 dispatch가 사용되는 위치는 Button onClick() 시점에 사용되기에 action 객체의 type이 존재하지 않는 것이지, 일종의 action 개념은 존재한다고 생각해도 되는게 아닐까 합니다.

actiontype이라는 분류 키워드가 존재하고, 하나의 action 객체에 type과 커스텀 프로퍼티가 함께 있습니다. 원래의 Redux에서는 이 dispatchaction 객체를 전달받는 콜백함수를 전달해 storestate를 변경합니다.

라고 유추하는 것이지, 명확한 사실은 아닙니다.

Redux가 아닌 RTK니까요.

요약하자면,

  1. Action과 Store 사이에는 [action, state] 구조로 이루어진 Reducer가 존재한다.

  2. Store state는 dispatch 메서드로 변경해야 한다.

  3. Reducer는 연산 레이어로 볼 수 있다.

정도가 되겠습니다.

오늘은 여기까지 하고, 다음 글에서 좀 더 깊게 이어서 다뤄보도록 하겠습니다.

0개의 댓글