Redux Toolkit 알아보기
들어가며
출처: 코딩애플
프론트엔드에서 중요한 문제 중 하나가 전역상태를 어떻게 관리할 것인가 라고 생각한다. 특히나 React로 프로젝트를 구성했다면 여러 컴포넌트들 사이에서 props를 주고 받는 것이 프로젝트 규모가 커질수록 부담스러워질 수 있다. 컴포넌트 깊이가 깊어질수록 상위 컴포넌트의 state를 갱신하는 과정이 복잡해지고, 이는 컴포넌트 간의 결합도를 강화시킨다. 컴포넌트 간의 독립성을 추구하기 위해, 그리고 state 갱신에 관한 에러를 방지하기 위해 redux가 등장하였다. Redux에 대한 자세한 설명은 생략하기로 하고, 이 글에서는 Redux Toolkit을 이용해서 프로젝트를 어떻게 구성했는지를 설명해보려고 한다. 그냥 바닐라(?) 리덕스 대신 리덕스 툴킷을 이용한 이유는 보일러플레이트 코드가 훨씬 적고 공식문서에서도 이를 권장하기 때문이다. 리덕스의 개념은 벨로퍼트 문서를 통해 익혔는데, redux Toolkit은 공식문서와 여러 구글링을 통해서 익혔다.
사실 프로젝트의 전역상태관리를 할 수 있는 방법은 리덕스만 있는 것은 아니다. 대표적으로 리액트 내의 context API를 활용할 수 있고(useReducer와 혼합하여 쓰면 바닐라 리덕스와 코드 흐름이 어느정도 비슷해진다), Recoil같은 라이브러리를 이용할 수도 있다. CEOS에서 진행했던 이전 프로젝트에서 context + useReducer로 상태관리를 해봤던 경험이 있는데, 여기에는 두 가지 문제점이 있었다.
하나는 context + useReducer의 조합은 전역상태의 조합이 늘어날수록 손이 더 많이 간다는 점이다. 우선 context의 경우 관심사의 분리를 해주지 않으면 전체가 한 번에 리렌더링되는 이슈를 야기할 수 있기 때문에, 관심사마다 컨텍스트를 따로 두어 성능 최적화를 하려면 손이 많이 간다. 또, useReducer는 결국 바닐라 Redux와 비슷한 코드라고 할 수 있는데, 액션과 리듀서를 분리해서 정의하는 과정이 보일러플레이트코드를 많이 많들기 때문에 손이 많이 간다.
다른 한 가지 문제는 전역상태의 명시적 관리에 관한 것인데, 액션이 일어나고 상태가 개선됐는지 확인하려면 이 조합에서는 액션이 일어났는지, 지금 현재 전역 상태가 무엇인지를 계속 콘솔에 찍어보면서 체크를 해야했다.
이 두 가지 문제 모두 리덕스 툴킷을 이용하면 해결할 수 있는데, 독립적인 slice를 생성해서 전역상태를 종류별로 관리하고, chrome Extension의 Redux devtools를 이용하여 전역상태를 자동으로 가시화할 수 있다. 물론 리덕스를 이용하는 것 자체로 redux devtools를 이용할 수 있지만, 툴킷을 이용하면 연동을 좀 더 간편하게 할 수 있는 이점이 있다.
이와 같은 과정을 통해서 이전 프로젝트와는 다른 방법으로 전역상태관리를 하게 되었다.
Redux Toolkit은 어떻게 돌아가는가
Redux Toolkit의 개념적인 부분을 알고자 이 글을 검색하신 분이 있다면 화해블로그의 글을 탐독하시기를 권장드린다. 이 글에서는 간략한 설명만 해보려고 한다.
우선 리덕스 툴킷에서는 createSlice가 가장 중요하다. 바닐라 리덕스에서는 액션 타입을 정의하고 액션 객체를 정의하고 리듀서를 또 따로 정의하고 그것들을 조립해서 썼다면, RTK에서 가장 차별화된 점은 액션을 정의하는 코드가 Slice 안에 녹아들어있다는 것이다.
아래와 같은 코드가 있다고 하자. 실제 프로젝트의 코드의 일부를 가져왔다.
const initialState: User = {
id: -1,
nickname: '',
password: '',
name: '',
phoneNumber: '',
isLoggedIn: false,
isConsultant: false,
consultantRegisterStatus: '신청 전',
};
const userSlice = createSlice({
// createAction 과 createReducer의 결합임.
// 슬라이스이름. 이것을 기반으로 액션이름이 자동 생성됨.
// 슬라이스이름/리듀서이름으로 액션이름이 결정됨.
name: 'user',
// 초기상태
initialState,
reducers: {
SET_USER: (state, action) => {
return { ...state, ...action.payload };
},
LOG_OUT: (state, action) => {
setCookie('token', undefined);
alert('로그아웃되었습니다.');
return { ...initialState };
},
},
extraReducers: (builder) => {
// builder는 Case Reducer로 액션별로 나눠서 액션을 처리할 수 있음.
// extraReducer를 사용한 이유는 맵핑된 내부 액션 타입이 아니라 외부 액션을 참조하려는 것임.
// 회원가입
builder.addCase(signUpAsync.fulfilled, (state, action) => {
alert('회원가입에 성공하였습니다.');
setCookie('token', action.payload.token);
return { ...state, nickname: action.payload.nickname, isLoggedIn: true };
});
builder.addCase(signUpAsync.rejected, (state, action) => {
alert('회원가입에 실패했습니다.');
console.log(action.payload);
return { ...state };
});
});
우선 initialState에서는 관리할 전역상태의 객체를 정의한다. 이것은 서비스 로직에 따라 필요한 정보를 가지고 정의하면 된다.
Slice는 Action과 Reducer의 결합이다. 즉, createSlice는 createAction과 createReducer의 결합이다. 인자 값으로 name, initialState, reducers, extraReducers가 들어간다.
- name은 이 전역상태를 지칭하는 이름으로, user라고 정했다.
- reducers에는 액션의 세부이름과 state를 어떻게 개선할 건지의 로직이 들어간다. 바닐라 리덕스의 reducer와 같다. 그렇게 되면, devTools에서 리덕스를 확인할 때 user/SET_USER 와 같은 형식으로 액션로그가 찍힌다.
- extraReducers 에는 Slice 내부에서 정의된 액션이 아닌, 외부의 액션을 참조하는 로직을 넣는다. 나의 경우 회원가입, 로그인 등 axios를 끼고 비동기 통신을 하는 부분에 대해서는 createAsyncThunk로 로직을 만들어주고, 이에 대한 결과를 extraReducers에서 처리해주는 방식으로 구현을 했다.
아래는 같은 파일에 있는, 회원가입 로직이다.
export const signUpAsync = createAsyncThunk(
'user/SIGN_UP',
async (signUpInfo: SignUpInfo, { rejectWithValue }) => {
try {
const response = await axios.post(
'https://api.ownroom.link/api/users/signup',
{
nickname: signUpInfo.id,
password: signUpInfo.password,
name: signUpInfo.userName,
phoneNumber: signUpInfo.phoneNumber,
}
);
return { ...response.data, nickname: `${signUpInfo.id}` };
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
여기서 정의된 액션은 Slice내의 액션이 아니기 때문에 외부 액션으로 간주되어 위에서 extraReducer로 처리를 해줘야 하는 것이다. 그리고, 한 가지 더 언급하고 싶은 점은 rejectWithValue를 꼭 써줘야 프로미스가 reject됐을 때의 로직을 처리할 수 있다. reject됐을 때도 역시 처리는 위의 slice에서 extraReducer 로직을 통해 처리한다.
이렇게 Slice가 만들어지고 나면, 슬라이스의 reducer부분을 export해주고, 또 RootState로부터 해당 slice로 정의된 전역상태를 가져올 수 있는 함수를 export해준다.
export default userSlice.reducer;
export const getUserInfo = (state:RootState) => state.user;
이쯤에서 나의 디렉토리 구조를 살펴보자면, 아래와 같은 식으로 이루어져 있다.
//configureStore.ts
import { configureStore } from '@reduxjs/toolkit';
import user from './modules/user';
import portfolio from './modules/portfolio';
import modal from './modules/modal';
const store = configureStore({
reducer: {
user,
portfolio,
modal,
},
devTools: process.env.NODE_ENV !== 'production',
});
export type RootState = ReturnType<typeof store.getState>; // state의 기본 타입
export type AppDispatch = typeof store.dispatch; // dispatch의 기본타입
export default store;
//configureStore.hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from './configureStore';
// 타입 없이 쓰기 위한!
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelect: TypedUseSelectorHook<RootState> = useSelector;
Slice는 modules 폴더 안의 ts파일들에서 정의하고, 거기서 default로 export한 reducer를 모아 configureStore로 store를 만든다. 그리고 configureStore.ts에서 RootState와 AppDispatch의 타이핑을 해주고, hooks.ts에서 이를 받아올 수 있는 훅을 만든다. 그러면 말단 컴포넌트에서 간단하게 사용할 수 있다.
무엇을 전역상태로 관리했는가
그렇다면 나는 무엇을 전역상태로 관리했는가? 눈치가 빠른 독자라면 configureStore의 reducer 코드를 통해 추측하셨을 것이다. 나는 아래와 같이 세 가지 전역상태를 설정하였다.
- user : 사용자의 정보관리
- portfolio : 현재 렌더링된 포트폴리오의 concept가 무엇인지 관리
- modal : 모달에 띄울 수 있는 메세지, 모달에 띄우는 이미지 관리
세 가지를 설정한 기준은 위의 세 상태는 말단 컴포넌트에서 호출될 일이 비일비재하다고 생각했기 때문이다. 이것들을 수직적인 컴포넌트 계층을 통과하면서 주고받는 것은 상당히 비효율 적이라고 생각했다. 위의 세 상태는 언제 어디서든 불려지고 개선될 여지가 있기에, 전역상태로 채택하였고, 관심사의 분리를 수행하기 위해 다른 파일에 각각 slice를 만들어서 관리하였다.
Redux devTools 사용하기
결과적으로 Redux Devtools를 연동한 후에(configureStore의 devTools 옵션을 통해 간편하게 연동이 가능하다.) chrome Extension 상으로 관리되는 모습을 촬영해봤다. 아래 gif는 회원가입 이후 어떤 액션이 일어나는 지를 순차적으로 보여준다.
위의 portfolio 관련 액션은 처음 홈 화면이 렌더링 될 때 포트폴리오 목록을 렌더링해오면서 생기는 액션이고, 회원가입 수행 시 user관련 액션이 순차적으로 이루어지면서 전역상태 user의 정보가 바뀌는 것을 볼 수 있다. 회원가입 이후 유저가 로그인을 하지 않아도 토큰을 받아서 바로 로그인이 되도록 구현했기 때문이다.
이렇게 전역상태를 명시적으로 관리하면 유지/보수를 할 때 코드의 어느 부분에서 문제가 생겼는지 알 수 있어서 매우 좋다.
'Study Log' 카테고리의 다른 글
리액트 쿼리 공식문서 번역 및 요약 (0) | 2022.07.20 |
---|---|
git branch 공부하기 (0) | 2022.02.26 |
Redux 공부하기 (0) | 2022.01.01 |
브라우저 공부 (0) | 2021.11.20 |
React.js 공식문서 정리 - Hook (0) | 2021.11.18 |
댓글
이 글 공유하기
다른 글
-
리액트 쿼리 공식문서 번역 및 요약
리액트 쿼리 공식문서 번역 및 요약
2022.07.20 -
git branch 공부하기
git branch 공부하기
2022.02.26 -
Redux 공부하기
Redux 공부하기
2022.01.01 -
브라우저 공부
브라우저 공부
2021.11.20