다음 토이프로젝트의 개발 기록이다.
Redux Toolkit은 Redux를 쉽게 사용할 수 있도록 도와주는 공식적인 패키지다. Redux의 복잡한 설정 과정과 불필요한 코드를 줄이고, 상태 관리 로직을 작성할 때 보다 간결하고 효율적인 방법을 제공한다. Redux Toolkit은 store 설정, 리듀서 작성, 미들웨어 추가 등을 편리하게 처리하며, 특히 createSlice, configureStore 같은 유틸리티 함수로 반복적인 패턴을 줄인다.
기존 Redux는 액션 타입, 액션 생성자, 리듀서 등을 일일이 작성해야 하지만 Redux Toolkit에서는 createSlice를 사용해 이 과정을 간소화할 수 있다.
기존 Redux에서 createStore로 직접 스토어를 구성했지만, Redux Toolkit의 configureStore는 기본 미들웨어 설정과 개발 환경에서의 디버깅 도구 통합이 자동으로 포함된다.
비동기 처리를 위한 createAsyncThunk: Redux에서 비동기 처리를 하려면 미들웨어를 추가하거나 직접 설정해야 했지만, Redux Toolkit에서는 이 과정을 createAsyncThunk를 사용해 단순하게 처리할 수 있다.
createSlice로 slice를 생성할 수 있다.
initialState로 초기값을 지정해주고, reducers 안에 slice의 상태값을 수정할 수 있는 reducer를 설정한다.
해당 reducer를 호출할 때 담은 인자를 action으로 받아올 수 있다.
import { createSlice } from '@reduxjs/toolkit';
export const locationDataSlice = createSlice({
name: 'locationData',
initialState: {
province: '',
city: '',
},
reducers: {
setProvince: (state, action) => {
state.province = action.payload;
},
setCity: (state, action) => {
state.city = action.payload;
},
initLocation: state => {
state.province = localStorage.getItem('province') as string;
state.city = localStorage.getItem('city') as string;
},
},
});
export const { setProvince, setCity, initLocation } = locationDataSlice.actions;
export default locationDataSlice.reducer;
configureStore를 사용해 각 슬라이스 리듀서를 결합 할 수 있다. 이 예제에서는 shortData와 longData라는 두 개의 상태 슬라이스를 관리하고 있으며, Redux Toolkit을 통해 간결한 설정이 가능하다.
import { configureStore } from '@reduxjs/toolkit';
import locationDataSliceReducer from '@src/Store/locationDataSlice';
export const store = configureStore({
reducer: {
locationDataSliceReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
만들어준 store를 프로젝트의 root 경로로 가서 프로젝트를 감싼 Provider에 인자로 넣어주어야 각 컴포넌트에서 접근이 가능해진다.
import { useEffect } from 'react';
import { Provider } from 'react-redux';
import { RouterProvider } from 'react-router-dom';
import router from '@src/router';
import store from '@src/Store';
function App() {
return (
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
}
export default App;
useSelector로 store의 상태에 접근할 수 있고, useDispatch로 state의 상태를 주어진 reducer를 통해 변경할 수 있다.
다음은 위에서 생성한 locationDataSlice를 실제로 사용한 코드이다.
reducer인 setCity, setProvince, initLocation에 action 값을 넣어줌으로써 slice의 상태값을 변경시키고 있다.
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useGeolocation } from '@src/Hook';
import { locationType } from '@src/Hook/useGeolocation';
import { ICoord } from '@src/API/getWeatherShort';
import { RootState } from '@src/Store/store';
import { open } from '@src/Store/kakaoModalSlice';
import { errorAccured } from '@src/Store/requestStatusSlice';
import { setCity, setProvince, initLocation } from '@src/Store/locationDataSlice';
import { transLocaleToCoord, setLocalCoordInfo } from '@src/Util';
import { Form, Button } from 'react-bootstrap';
import styled from 'styled-components';
import _short_local from '@src/Assets/short_api_locals.json';
const short_local = _short_local as ICoordJson;
interface ICoordJson {
[depth1: string]: {
[depth2: string]: {
x: number;
y: number;
};
};
}
interface Props {
handleChangeCoord: (coord: ICoord) => void;
}
const = ({ handleChangeCoord }: Props) => {
const dispatch = useDispatch();
const location: locationType = useGeolocation();
const { province, city } = useSelector((state: RootState) => state.locationDataSliceReducer);
const onChangeProvince = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectProvince = e.target.value;
dispatch(setProvince(selectProvince));
dispatch(setCity(''));
};
const onChangeCity = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectCity = e.target.value;
if (selectCity === '') return;
const { x: nx, y: ny } = short_local[province][selectCity];
if (setLocalCoordInfo({ nx, ny, province, city: selectCity })) {
localStorage.setItem('Geolng', '');
localStorage.setItem('Geolat', '');
handleChangeCoord({ nx, ny });
dispatch(setCity(selectCity));
localStorage.setItem('province', province);
localStorage.setItem('city', selectCity);
}
};
const currentLocation = async () => {
if (location.loaded && location.coordinates) {
let { lng, lat } = location.coordinates;
const prevLng = Number(localStorage.getItem('Geolng'));
const prevLat = Number(localStorage.getItem('Geolat'));
lng = Number(lng.toFixed(7));
lat = Number(lat.toFixed(7));
if (prevLng === lng && prevLat === lat) {
dispatch(errorAccured('이미 현재 위치 정보입니다.'));
return;
}
localStorage.setItem('Geolng', lng.toString());
localStorage.setItem('Geolat', lat.toString());
const result = await transLocaleToCoord({ lng, lat });
if (result) {
const { nx, ny, province, city } = result;
dispatch(setProvince(province));
dispatch(setCity(city));
handleChangeCoord({ nx, ny });
}
} else {
alert('현재 위치 정보를 가져올 수 없습니다.');
dispatch(errorAccured('현재 위치 정보를 가져올 수 없습니다.'));
}
};
useEffect(() => {
dispatch(initLocation());
}, []);
return (
<LocationHeaderContainer>
<LocationHeaderSelector>
<Form.Select onChange={onChangeProvince} value={province}>
<option value=''>선택</option>
{Object.keys(short_local).map(key => {
return (
<option value={key} key={key}>
{key}
</option>
);
})}
</Form.Select>
<Form.Select onChange={onChangeCity} value={city}>
<option value=''>선택</option>
{province &&
Object.keys(short_local[province]).map(key => {
return (
<option value={key} key={key}>
{key}
</option>
);
})}
</Form.Select>
<div>의 날씨는?</div>
</LocationHeaderSelector>
<LocationHeaderButtons>
<Button onClick={() => currentLocation()}>현재 위치로 설정</Button>
<Button onClick={() => dispatch(open())}>지도에서 선택하기</Button>
</LocationHeaderButtons>
</LocationHeaderContainer>
);
};
export default LocationHeader;