목차
· 리덕스(Redux)
· 리덕스(Redux)를 왜 사용하고 있을까?
· 리덕스(Redux)의 단점
· 리덕스(Redux)의 장점
· 너무 복잡한 Redux, RTK(React Tool Kit)의 등장?
· 레퍼런스
리덕스(Redux)
먼저 리덕스는 Flux 아키텍쳐의 구현체로, 대형 MVC 어플리케이션에서 종종 나타나는 데이터 간 의존성 이슈, 즉 연쇄적인 갱신이 뒤얽혀 데이터의 흐름을 예측할 수 없게 만들었던 문제를 해결하기 위해서 고안이 되었다. 잘 알려진 사례 중 하나인 2014년 컨퍼런스에서 소개된 페이스북의 채팅 버그에서 읽지 않은 메세지 상태를 나타내는 카운트를 확인하고 사용자가 메세지를 확인해도 어느새 좀비처럼 카운트 숫자가 되살아나면서 사용자를 괴롭히던 버그가 있었다.
개발자가 버그를 수정해도 잠시 동안은 괜찮아보여도, 다시 버그는 재발생하였고 이러한 경험을 거치면서 페이스북 팀은 설계에 기반한 근본적인 문제가 있다고 판단했다. 그리고 그 해결책으로 어플리케이션의 데이터가 단방향으로 흐르는 방법을 고안하게 된다. 그것은 FLUX 아키텍쳐의 핵셈 멘탈 모델, 다시 말해서 사고 과정, 동기, 철학적 배경 등에 대해 깊이 이해할 수 있게 하는 하나의 모델이 되었고, 그 구현체인 리덕스는 어플리케이션을 위한 상태 컨테이너로써 단방향 데이터 흐름을 활용하여 시스템을 예측 가능하게 만들어 시스템을 보완하는 역할을 해준다.
리덕스를 사용하는 구조에서 전역 상태를 전부 하나의 저장소(store) 안에 있는 객체 트리에 저장하며, 상태를 변경하는 것은 어떤 일이 일어날지를 서술하는 객체인 액션(action)을 내보내는(dispatch) 것이 유일한 방법이다. 그리고 액션이 전체 어플리케이션의 상태를 어떻게 변경할지 명시하기 위해서는 리듀서(Reducer) 작성이 필요하다.
리듀서는 변화를 일으키는 함수로써 전달받은 액션을 가지고 새로운 상태를 만들어서 스토어에 전달한다. 이 모든 설계는 데이터가 단방향으로 흐른다는 전제하에 데이터의 일관성을 향상시키고 버그 발생 원인을 더 쉽게 파악하려는 의도에서 출발했다.
리덕스(Redux)를 왜 사용하고 있을까?
React를 사용하면 state 끌어올리기에 부담을 느끼거나, 불필요한 컴포넌트 드릴링(Component Drilling) 등이 이슈가 되면서 전역적인 state 관리법이 필요해졌고, 그에 대한 솔루션으로 나온 것이 Redux라고 할 수 있다. React에 Flux 패턴이 더 해진 것, 하지만 요즘은 전역적인 state 관리를 꼭 Redux 만으로 할 수 있는 것은 아니다. context와 useContext 등의 조합 혹은 recoil, zustand, jotai 등의 새로운 state 관리 라이브러리 등이 이미 많고, 충분히 효용성이 입증된 상태이다. 이런 상황에 우리는 Redux를 왜 계속 사용하고 있을까?
리덕스(Redux)의 단점
· 리덕스 스토어 환경 설정이 복잡하다. 일례로 extension도 default로 세팅되는 것이 아니라 직접 해줘야한다.
· 리덕스를 유용하게 사용하려면 많은 패키지를 추가해야한다.
· 리덕스는 보일러 플레이트, 즉 어떤 일을 하기 위해 꼭 작성해야하는 (상용구) 코드를 너무 많이 요구한다. 항상 action types, action 생성 함수, reducer 등을 정의해주면서 여러 파일을 돌아다니던 개발 과정을 생각해보면 그렇다.
리덕스(Redux)의 장점
· 리덕스를 사용한 개발 스타일이 너무 마음에 들 때
모든 상태 업데이트를 액션으로 정의하고, 액션 정보에 기반하여 리듀서에서 상태를 업데이트하는 이 간단 명료한 발상 덕분에 상태를 더욱 쉽게 예측 가능하게하며 유지보수 측면에 긍정적인 효과가 있다. 이게 흔히 말하는 Reducer + FLUX 패턴이며, 마음에 드는 사람들은 계속 리덕스를 사용한다.
· 미들웨어
Redux-saga, Redux-thunk와 같은 미들웨어를 통해 비동기 작업에 대한 좀 더 디테일 하고, 편한 컨트롤을 할 수 있다. 예를 들어 redux-saga를 쓰는 이유를 생각해보면, 아래와 같은 부분에서 좀 더 편하고, 명확한 개발을 할 수 있게 해준다.
1. 요청을 연달아서 여러번 하게 될 때 이전 요청은 무시하고, 가장 마지막의 요청만 처리하도록 하고 싶을 때 (예를 들어, input에서 keyCode === "ender"로 뭔가를 해놨다고 했을 때 repeat()을 안쓰고, 마지막 엔터에만 api 요청을 하고 싶다고 했을 때) : takeLatest를 통해 가능하다.
2. 특정 조건이 만족됐을 때 이전에 시작한 요청을 취소하는 상황
3. 특정 콜백함수를 원하는 액션이 디스패치 됐을 때 호출하도록 등록을 하는 상황
4. 컴포넌트 밖에서 어떤 작업을 수행할 때
이외에도 API 요청과 관련하여 이전에는 API 요청을 위해 리덕스와 미들웨어를 사용하는 것이 당연시 됐었다. 그러나 이제는 SWR과 React-Query 같은 라이브러리가 있기 때문에 단순 API 요청을 위하여 미들웨어를 사용 할 필요는 없긴하다. 하지만 그럼에도 비동기 작업에 대한 플로우에 대하여 더 많은 컨트롤이 필요할 때 (위에서 redux-saga가 하는 일과 같이) 미들웨어는 여전히 유용하게 쓰이고, Redux에는 이러한 미들웨어가 있다.
· 서버
API 요청 결과를 사용하여 서버 사이드 렌더링을 할 때 유용하다. 이 과정에서 미들웨어가 정말 유용하게 사용된다. 리덕스가 없어도 충분히 구현 할 수는 있긴 하지만 레퍼런스도 부족하고 번거로운 편이다. 물론 NextJS를 사용하면 좀 더 편리하다. Recoil과 Zustand 등의 state 관리 라이브러리는 아직 SSR을 제대로 지원하지 않기 때문에 Redux가 아직은 SSR과 관련하여 좀 더 매력적인 선택지 일 수 있다.
· 규모가 크다.
규모가 크고, 커뮤니티가 방대하기 때문에 사용을 한다. 많이 사용된다는 것은 그만큼 레퍼런스도 많고, 믿을만하다는 것이기 때문이다. 물론 개발자로서 이런 자세가 정답이다라고 할 수는 없지만, 대부분의 회사 제품이 Redux를 따르기도 하고, 많은 개발 팀에서 Redux를 채택했기 때문에 이유가 있을 법하다.
사실, 다른 신흥 전역 상태 라이브러리를 사용해도 되지만 그럼에도 Redux를 사용하는 이유는 비동기 작업에 대한 컨트롤을 위한 미들웨어를 사용하기 때문에 리덕스가 권장되는 상황인 것이다.
너무 복잡한 Redux, RTK(React Tool Kit)의 등장?
RTK란, 쉽게 말하자면 리덕스를 더 쉽게 사용하기 위해서 만들어진 도구 모음과 같은 키트이다. 사실 완벽할 것만 같았던 리덕스에도 문제가 있었는데, 대표적으로 언급되는 리덕스의 3가지 문제는 아래와 같다.
· 리덕스 스토어 환경 설정은 너무 복잡하다.
· 리덕스를 유용하게 사용하려면 많은 패키지를 추가해야한다.
· 리덕스는 보일러 플레이트, 즉 어떤 일을 하기 위해 꼭 작성해야하는 (상용구) 코드를 너무 많이 요구한다.
이러한 이슈를 해결하기 위해 툴킷이 등장했다. 공식 문서에 따르면 RTK는 리덕스 로직을 작성하는 표준 방식이 되기 위한 의도로 만들어졌다고 한다. 핵심은 기존 리덕스의 복잡함을 낮추고, 사용성을 높이는 것이다. 물론 툴킷이 리덕스가 가진 모든 문제를 해결할 수는 없겠지만 리액트가 CRA를 통해 개발 접근성을 높였듯이 RTK도 복잡한 리덕스 설정 과정을 포함해서 유스케이스 전반에 걸쳐 추상화를 시도했다는 것이다.
툴킷에서 제공하는 함수는 사용자에게 어플리케이션 코드를 간단히 작성할 수 있도록 지원하는데 집중하고 있다. 그리고 이 모든 도구는 이미 리덕스를 경험해본 사용자나 처음으로 RTK를 활용해 프로젝트를 구성하는 신규 사용자 모두에게 동일한 혜택으로 제공되어야 한다는 기본적인 철학을 바탕으로 하고 있다.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer
},
});
· 스토어 구성은 `configureStore`을 통해 구성 할 수 있다.
· 리덕스 스토어 상태에서 데이터 추출을 할 때 useSelector의 결점인 메모제이션을 하기 위해서 createSelector을 사용할 수 있다.
· 리덕스 로직을 작성할 때는 덕-패턴(ducks pattern) 형태로 작성을 돕는 createSlice를 사용하면 된다. (createSlice를 사용하면, createAction, createReducer 함수가 내부적으로 사용되며 리듀서와 액션 생성자와 액션 타입을 자동으로 생성해준다 그렇기 때문에 따로 작성할 필요가 없어진다.)
· 비동기 로직은 createAsyncThunk를 사용해 작성을 할 수 있다.
import { createAsyncThunk, createSlice, isFulfilled } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchIncrement = createAsyncThunk(
"counter/fetchIncrement",
async (value) => {
const response = await axios.put('/counter/increment', { value: value })
return response.data;
}
);
export const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: {
[fetchIncrement.fulfilled]: (state, action) => {
state.value = action.payload.value;
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
타입 스크립트를 지원하는 빌더 콜백(builder callback) 표기법을 사용하는 것이 바람직하다.
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
extraReducers: (builder) => {
// 'users/fetchUserById' 액션 타입과 상응하는 리듀서가 정의되어 있지 않지만
// 아래처럼 슬라이스 외부에서 액션 타입을 참조하여 상태를 변화시킬 수 있습니다.
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
프로미스 생명 주기를 따르는 액션 타입으로 비동기 로직을 관리해보자.
· 데이터 패칭과 캐싱은 RTK Query를 사용해보자.
레퍼런스
댓글