반갑습니다. 오랜만입니다.
취업 후 일주일 정도 여유를 가지고 있습니다.
매일 알고리즘 풀이도 조금씩 하며 시간을 보내느라 오랜만에 글을 적게 되네요.
11월 1일 출근 전까지 그래도 회사 스택에 대한 공부를 하며 시간을 보내볼까 합니다.
많은 것들을 공부하기에는 현재 고향에 내려와있어 조금 어려울 것 같지만, 그래도 Redux, Redux toolkit / saga, React query 정도는 한 번 사용해볼까 합니다.
오늘은 Counter
를 제공하는 기본 template을 활용해 Redux를 뜯어보며 이해해보겠습니다.
먼저, RTK, Redux Toolkit Quick Start 방법 중 하나인 CRA template을 생성해야 합니다.
npx create-react-app my-app --template redux-typescript
명령어를 터미널에 입력해 typescript template를 생성할 수 있습니다.
해당 프로젝트의 기본 구성은 4가지 카운터 기능입니다.
index 파일부터 한 번 열어보며 생각해보겠습니다.
Redux를 사용해본 적 없고, 기본 개념 정도만 알고 있는 상태로 RTK를 공부하는 글입니다. 따라서 깊은 분석이 아닌 유추와 적당한 검색을 통해 해결해나가는 과정을 기록하겠습니다.
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
라는 것을 불러와 Provider
의 props
로 사용한다는 점입니다.
대충 생각해봤을 때, RN
의 Navigation
, React
의 Router
와 같이 HOC
로 다른 컴포넌트를 감싸 무언가 전달하려고 하는구나 정도로 생각이 됩니다.
그렇다면 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
등의 개념들이 자리하고 있습니다.
context
와 children
은 이름만 봐도 React 유저라면 어라? 할 수 있을 정도의 친숙한 이름이지만, Redux만의 특별한 개념은 store
에 있는 것 같습니다.
다른 2개의 프로퍼티, context, child
는 옵션이지만, store
는 필수로 구현해야 합니다.
그렇다면 store
는 대체 뭘까요?
천천히 열어보며 어떤 개념일지 유추해봅시다.
그럼 자연스럽게 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.getState
의 return
값 타입으로 추론하라는 말이 됩니다.
그렇다면 store
객체는 어떻게 구성되었길래, dispatch, getState
등의 내부 메서드를 보유하고 있는걸까요?
그리고 ThunkAction
은 대체 뭘까요?
하나씩 파헤쳐봅시다.
잠시 개념을 보자면, Redux
는 store
라는 하나의 객체에서 앱 전체의 상태를 관리한다고 합니다.
나아가 Redux
는 Action => Store => View
구조로 동작하고 있습니다.
단, View
는 user interaction
을 담당하기 때문에 View
는 Action
의 Trigger가 될 수 있습니다.
그렇기에 View => Action => Store => View
의 순환구조로 이해할 수 있습니다.
그 외에도 다양한 개념이 있는데, 튜토리얼 단계에서 이해해야 할 것은 Action, Store, View
의 circulation
정도인 것 같습니다.
또한 실제 순환이라는 개념에 맞도록 Simplex
, 단방향으로 통신하는 구조를 갖습니다.
"영향을 끼칠 수 있는 존재는 정해져있다." 가 대부분 순환 시스템의 핵심이라는 생각이 강해서인지 미리 생각해두면 확 와닿는 것 같습니다.
이제 개념은 대충 정리되어 있으니, 관계별로 필요한 내용들을 조금씩 끼얹으며 코드 분석을 해봅시다.
앞서, Store
는 View
에 영향을 끼치는 존재인 것을 확인했습니다.
우선은 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
는 이런 구성으로 이루어져 있고, configureStore
의 return
결과는 해당 메서드들로 구성된 객체라는 것을 알 수 있습니다.
type 선언으로 유추하고자 한다면
EnhancedStore
=>Store
의 방법으로도 확인할 수 있습니다.
또한 store
내부에는 reducer
를 포함하는 등의 내용은 존재하지 않습니다.
이유는 간단합니다. 다른 개념이니까요.
Reducer의 개념은 잠깐 아래 항목에서 다루겠습니다.
그러나 store를 생성하는 시점에 store가 프로젝트의 Reducer를 인식할 수 있는 이유는
configureStore
메서드는 기존 createStore()
메서드의 결과와 달리 parameter
로 넘겨준 객체의 reducer {Object}
에 대해 자동으로combineReducer()
메서드를 실행하기 때문입니다.
원리는 store
를 정의하는 시점에서 root reducer
에 해당 reducer
를 할당하는 구조인 것 같은데, 기존 Redux
에 비해 얼마나 편해진것인지 체감하기엔 전례가 없어 모르겠습니다.
parameter를 관찰하는 방법은
또는 type 선언 중 ConfigureStoreOptions
를 참고하는 방법이 있습니다.
요약하자면,
아직은 어디다 쓰는지 모른다.
rootReducer는 또 뭘까.
정도로 요약할 수 있습니다.
이어서,
돌아돌아 결국 Action => Store => View
의 관계에 필요한 Reducer
라는 키워드를 알 수 있었습니다.
rootReducer
단어를 먼저 접하긴 했지만, 사실 Redux
는 Store
와 Action
사이에 Reducer
라는 특별한 레이어가 존재합니다.
Reducer
는 다시 특정 Action
과 Store
의 State
로 구성됩니다.
다시 생각하면
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
개념도 먼저 알아야 하는데,Store
의state
를 직접 변경하지 않도록 하는 하나의 장치 정도로 얕게 이해하고 넘어갑시다.
실제 dispatch
가 사용되는 위치는 Button onClick()
시점에 사용되기에 action
객체의 type
이 존재하지 않는 것이지, 일종의 action
개념은 존재한다고 생각해도 되는게 아닐까 합니다.
action
은type
이라는 분류 키워드가 존재하고, 하나의action
객체에type
과 커스텀 프로퍼티가 함께 있습니다. 원래의Redux
에서는 이dispatch
에action
객체를 전달받는 콜백함수를 전달해store
의state
를 변경합니다.
라고 유추하는 것이지, 명확한 사실은 아닙니다.
Redux가 아닌 RTK니까요.
요약하자면,
Action과 Store 사이에는 [action, state] 구조로 이루어진 Reducer가 존재한다.
Store state는 dispatch 메서드로 변경해야 한다.
Reducer는 연산 레이어로 볼 수 있다.
정도가 되겠습니다.
오늘은 여기까지 하고, 다음 글에서 좀 더 깊게 이어서 다뤄보도록 하겠습니다.