반갑습니다.
어제는 Redux -> RTK
의 순서가 아닌 RTK -> Redux
순서로 공부하는 글이었습니다.
그렇기에 오늘은 약간의 코드와 다량의 개념들로 글이 이루어질 것 같습니다.
지난 글에서는 코드를 보며
등에 대한 개념을 RTK
를 통해 알아보았습니다.
그런데 해당 방식은 서순이 맞지 않기도 하고, 놓치는 부분이 약간씩 생기는 것 같아 Redux
의 개념을 RTK
로 이해해보는 식으로 변경해볼까 합니다.
이번 글은 기존의 Redux
에서 왜 RTK
를 사용하는지? 그리고 어떤 개념들이 RTK
를 사용하게 만들었는지 체크해보겠습니다.
그럼 시작해보겠습니다.
보통 가장 처음 생각하고 넘어갔어야 할 3가지 원칙을 어제 글에서는 적지 않았었습니다.
그런데 공부하다보니 대부분의 포스팅, 도서, 강의에서 강조하는 내용이 3가지가 있어 적고 시작하겠습니다.
약간 복무신조 느낌이네요.
하나씩 생각해보죠.
Redux
는 단 하나의 상태 객체, Store
를 구성할 것을 강조합니다.
먼저 앱의 확장성을 고려했을 때, 하나의 자바스크립트 객체를 활용하는 것이 Client - Server
요청 처리를 단순화하기 때문입니다.
그리고 이전의 객체가 저장되어 있다면 redo - undo
를 쉽게 구현할 수 있기 때문입니다.
디버깅에 용이하기 때문입니다.
자, 보통의 개발자가 좋아하는 키워드 하나로 표현할 수 있겠죠?
분명 "유지보수"에 용이할 수 있다. 가 포인트입니다.
변경에 있어 하나의 가능성만을 남겨두고, 단방향 플로우를 구현해야 하는 이유.
디버깅에 용이하다. 유지보수에 도움이 된다. 프로젝트 안정성을 보장한다.
하나의 컨벤션을 준수하는 것으로 수많은 오류를 회피할 수 있습니다.
그런데 이런 이유 말고 왜 불변이라는 표현을 사용해야만 할까요?
슬슬 감이 오시듯,
상태는 Redux
뿐 아니라, React
도 강조하고 있는 개념인 불변성을 가진 객체로 관리되어야 합니다.
그건 바로 변경 탐지의 이유때문입니다.
React
가 상태, props의 변경을 어떻게 체크하는지 생각해보면 왜 불변성을 유지해야 하는지도 알 수 있습니다.
React
는Shallow compare
를 통해Rendering 최적화
를 이뤄냈으니, 한 번 해당 키워드 글을 읽어보면 좋습니다.
순수함수? 쉽게 볼 수 있는 키워드는 아닙니다.
그런데 크게 신경써야 할 부분은 바로 이 부분입니다.
하나의 스토어, 불변객체인 State
관리, 그보다 난해한 것은 내가 작성한 Reducer
가 순수함수가 맞을까? 하는 고민이 가장 어려울 것 같습니다.
순수함수는 I-O(Input - Output)
이 명확해야 합니다.
어떠한 Side-Effect
도 존재하지 않는 함수를 순수함수라 명합니다.
예를 들어보면,
지난 프로젝트에서 다루었던 dayjs()
의 유틸함수 중 일부는 순수함수가 아닙니다.
현재 시점의 Date()
객체의 data
를 다루는 함수의 경우 다양한 상황에서 버그가 발생할 수 있습니다.
대표적 예시로 Github Actions
에서의 test
환경은 KST
환경과 다른 UTC
환경입니다.
그럴 경우 Date()
객체의 data
는 한국 시각이 아니니, I-O
자체가 틀어져버리는 이슈가 발생하게 됩니다.
그러나 반대로
특정 date String
을 입력으로 받아 milliSec
로 변경해주는 로직의 경우 Side-Effect
가 존재하지 않습니다.
변경("2021-10-28"); // return 대충 밀리초(12321313213)
이런 함수가 바로 순수함수입니다.
즉, 앞으로의 상태변경 로직은 순수함수로만 작성되어야 한다. 정도만 이해하면 될 것 같습니다.
Redux
를 알아보았을 때, Store
하나로 프로젝트 전체의 상태를 관리한다는 것은 때로 매우 까다로울 수 있으며, 때로는 프로젝트 내부의 Cost
가 높아질 수도 있습니다.
거기다 불변객체가 비교에는 용이하지만, re-Render
가 적은 경우 객체 단순 변경이 어쩌면 더 좋은 퍼포먼스를 자랑할수도 있습니다.
순수함수를 고려하는 것 또한 개발자의 입장으로써 쉬운 일은 아닐 수 있죠.
그럼에도 Redux
는 확장을 고려할 때 사용해야 하는 하나의 솔루션으로 인정받았습니다.
실제로 인턴십 중에도 Redux
를 실무에 사용하고 있다는 것을 듣기도 했고, 지금의 회사도 Redux
를 사용하고 있습니다.
그래서인지 Redux
를 사용할 때는 모두 입을 모아 "더 고민해보고, 다시 고민해라." 라는 말을 하기도 합니다.
하지만 상태관리 이슈에 부딪혀본 저와 같은 사람들이라면 이제는 도전해볼 때가 되었습니다.
가보죠.
먼저, Redux의 동작흐름은 어제 말씀드린것처럼
이런 식으로 구성되어 있습니다.
이를 하나씩 생각해보면
Action
은 특정 상태에 대한 하나의 정의입니다.
통상적으로 객체 형태이며, type
프로퍼티를 필수적으로 갖는 JS Plain Object
입니다.
Reducer
실행 전, 거쳐가는 하나의 레이어 개념입니다.
Middleware
에는 보통 Reducer
의 예외처리를 하거나, 상태값을 디버깅하는 로직을 추가할 수 있습니다.
그 외에도 Action
에 대한 비동기 처리를 가능하게 한다던가(redux-thunk
), 특정 Action
을 기반으로 동작하는 하나의 chaining(redux-saga
)을 구현할수도 있습니다.
라이브러리를 주로 활용하는데, 위 두 예시가 자주 사용되는 라이브러리입니다.
Middleware의 경우 function compose로 구현되는데, 이는 클로저 개념을 응용하는 방식입니다.
주로
const middleware = store => next => action => next(action)
의 형태를 가집니다.
어제 까보았던 RTK의 store API를 적용해서 생각하면,
const middleware = ({getState, dispatch}) => next => action => next(action)
으로 디테일하게 표현할 수 있습니다.
(prevState, action)
둘을 전달받아 newState
를 반환하는 하나의 function입니다.
const reducer = (prev, action) => new
의 구조로 이루어져 있다고 생각할 수 있습니다.
Redux는 Reducer를 위한 라이브러리입니다.
그러니, Reducer를 얼마나 잘 작성하고, 잘 다룰 수 있느냐가 사실상 관건이 아닐까 생각합니다.
단일 JS object
입니다.
프로젝트 전체의 상태를 보유하기도 하고, 경우에 따라 일부만 보유할 수도 있습니다.
어제 보았던 store
의 내부에는 다양한 property
가 존재하는데, 이를 store API
라고 표현하기도 합니다.
모든 상태의 집합체라고 생각할 수 있습니다.
또한 값을 직접 수정할 수 없기에 dispatch
메서드를 지원합니다.
dispatch
를 reducer
내부 등에서 호출할 경우 store
는 쌓인 순서, queue
에 담아두는 것과 같은 순서로 처리합니다.
UI단, interaction을 접수받는 유일무이한 창구입니다.
저는 항상 View도 단방향 통신을 한다고 말하는데, 유저의 눈과 손을 분리해서 생각하기에 View도 하나의 순환구조가 형성된다고 생각합니다.
눈에 영향을 끼치고, 손에게 영향을 받으며, 흐름에 맞게 다른 시스템 구조의 누군가에게 영향을 끼치게 됩니다.
특정 Action의 Trigger이기도 합니다.
이제 Redux
를 어느정도 알아봤으니, RTK에 대해 얘기해봅시다.
Redux Toolkit
은 Redux
의 다양한 기능을 보다 쉽게 사용할 수 있는 메서드를 지원합니다.
공식문서에도 잔뜩 있기도 하고, 구체적인 내용은 적지 않겠습니다.
그런데 새로운 개념 하나 둘 정도는 확실히 짚고 넘어가봅시다.
분명 template을 하나 만들면 이상한 키워드들이 좀 있습니다.
AsyncThunk
라던가 혹은 Slice
가 그 예시가 될겁니다.
어쩌면 어제 적었던 configureStore()
도 그 예시중에 포함됩니다.
그런데 AsyncThunk
나 configureStore
정도는 이름만 봐도 느낌이 있는데, Slice
? 이건 정말 모르겠습니다.
그러니 한 번 짚고 넘어가봅시다.
Slice
는 Reducer + Action
입니다.
?
왜 이렇게 되어있는지 의문이시겠지만, 분명 그런 개념이라고 명시되어 있습니다.
이 Slice
를 통해 어제 Redux
개념을 공부하려다보니 분명 구멍이 생겼을 정도로 가히 파격적인 개념입니다.
다시 어제의 코드를 가져와봅시다.
const initialState: CounterState = {
value: 0,
status: "idle",
};
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;
});
},
});
이제 눈에 확 들어오는 친구들이 몇 있습니다.
initialState
: 초기 상태값입니다. 따로 할당된 값은 블럭 위에 있습니다.
reducers
: Action type을 key로 갖는 reducer function의 집합체입니다.
extraReducers
: pending, fulfilled, reject 등의 비동기 Promise 상태에 따라 변경되는 새로운 State를 반환하는 reducer 입니다. 예외처리기입니다.
이정도면 Slice는 참 간단히 이해할 수 있겠습니다.
근데 incrementAsync
는 어디에 있을까요?
여기 있습니다.
// counterAPI.ts
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
...
// counterSlice.ts
export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount: number) => {
const response = await fetchCount(amount);
return response.data;
});
이게 뭐시여... 싶으시겠지만, RTK
를 활용한 비동기 처리 로직 사용법의 예시입니다.
axios
를 쓰면 fetch
가 어색하듯, createAsyncThunk
도 어색합니다.
그러나 해당 요청결과를 extraReducer
내부에서 당연히 필터할 수 있고, 기존 custom hooks
를 활용한 로직 결과 반환을 Slice
파일 내에서 처리할 수 있다는 소소한 장점도 생깁니다.
그러니 thunkAction도 유용하게 사용해봅시다.
Middleware layer의 예시를 RTK식으로 표현할 수도 있습니다.
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
홀수일 때 더하는 로직인데, 익숙한 표현이 하나 보입니다.
그 전에 AppThunk
는 custom type
입니다.
실제로는 TunkAction type
을 대입한 것과 같은 구조기에 return type
은 (dispatch, getState) => {}
와 같습니다.
(dispatch, getState) => {}
확실히 Redux middleware
와 유사한 구성이죠?
또한 currentValue
의 경우 prevState
에 해당합니다.
dispatch(action())
도 보이네요.
생각보다 이제 보이는게 좀 있는 것 같습니다.
Redux -> RTK 구조로 공부하는게 어제보다 효율도 잘 나오고, 개념 이해도 쉬웠던 것 같습니다.
둘 다 사용할거니까 어차피 둘 다 하긴 했어야 합니다.
그런데 로드맵이 있다면 Redux 개념 -> RTK 순서가 훨씬 도움이 되었던 것 같네요.
그럼 오늘은 이정도로 마치겠습니다. 읽어주셔서 감사합니다.