리덕스 자체도 굉장히 어려운 편에 속한다고 생각하는데, TS까지 섞여버리면 머리가 아프다..
그치만, 크게 문제될 것은 없다.
타입만 지정해주면 되기 때문에!
이번에는 Typescript + React에서 기초적인 Redux 사용법을 기록하고자 한다.
thunk나 promise, redux-toolkit 등은 활용하지 않고, 딱 기본만 사용해봤으므로 혹시나 이게 궁금하셨던 분들은...뒤로가기!
리덕스를 사용할 때 어떤 순서로 작성을 했었나 생각해보자(물론 사람마다 다 다르다..)
1. 액션(action) 타입(type) 정의
2. 액션 정의
3. 리듀서(Reducer) 정의
4. 루트 리듀서(Root Reducer) 정의
5. 스토어 생성
6. useDispatch로 액션 디스패치하기
7. useSelector로 스토어에 접근하기
대략 이정도 큰 줄기를 따라서 간다. 물론 코딩하다보면 왔다갔다 하면서 정의하게 된다.
자, 여기서 Typescript를 끼얹으면 어떻게 될까?
타입만 지정해주면 된다! 시작해보자!
리덕스에 주요하게 사용되는 것은 action, reducer 폴더이다. 참고해주세요!
필자는 action과 reducer를 구분해서 만들었고, export로 가져오냐 같은 파일에 작성하냐의 차이밖에 없을 것이므로, 큰 문제없이 이해하실 수 있으리라 생각합니다.(리덕스 파일 구조는 정말 다양한 것 같아서 뭐가 맞다고는 할 수 없다고 한다)
//* src/action/types.ts *//
export const SAVE_BIG_LEAGUE = "save_big_league" as const;
일반적인 리덕스와 다른 것은 뒤에 "as const"가 붙은 것 뿐이다.
나중에 액션 객체를 만들 때 action.type을 추론하는 과정에서 string으로 추론되지 않고
'save_big_league'와 같이 추론된다.
//* src/action/league_action.ts *//
import { SAVE_BIG_LEAGUE } from "./types";
interface saveBigLeaguePropsType {
test: any;
}
export function saveBigLeague(test: saveBigLeaguePropsType) {
return {
type: SAVE_BIG_LEAGUE,
payload: test,
};
}
액션 정의도 크게 다를 것이 없다.
TS에서 props를 받아오면 항상 그 타입을 명시해줘야하는 것을 그대로 실천해주었다.
//* src/reducer/league_reducer.ts *//
import { SAVE_BIG_LEAGUE } from "../action/types";
import { saveBigLeague } from "../action/league_action";
// 리듀서 파라미터 중 initialState의 타입 정의
type LeagueStateType = {
leagueData: Array<any>;
};
const initialState = {
leagueData: [],
};
// 리듀서 파라미터 중 action의 타입 정의
type LeagueActionType =
| ReturnType<typeof saveBigLeague>;
export default function leagueReducer(
state: LeagueStateType = initialState,
action: LeagueActionType
) {
switch (action.type) {
case SAVE_BIG_LEAGUE:
return { ...state, leagueData: state.leagueData.concat(action.payload) };
default:
return state;
}
}
리듀서를 정의하는 부분도 타입를 명시해주는 부분 외에는 전혀 다를게 없다.
reducer의 파라미터로 들어가는 state에 initialState를 지정해놓을 때가 있는데, 이 initialState의 타입을 정의해준다.
추가로, 또 다른 파라미터인 action에도 타입을 지정해준다.
필자는 any를 많이 썼는데 TS를 사용하는 의미가 없어지므로, 다른 분들은 데이터에 알맞는 타입을 잘 정리해서 사용하시기를!
//* src/reducer/index.ts *//
import { combineReducers } from "redux";
import leagueReducer from "./league_reducer";
const rootReducer = combineReducers({
leagueReducer,
});
export default rootReducer;
// useSelector로 스토어에 접근할 때 필요하다!
export type RootState = ReturnType<typeof rootReducer>;
이 부분도 전혀 다른게 없다.
딱 하나, RootState라는 type을 하나 정의해줬다.
이는 useSelector를 통해 리덕스 스토어에 접근할 때 사용된다.
//* src/index.tsx *//
// ... //
import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./reducer";
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<Provider store={store}>
<App />
</Provider>
);
Redux Store를 생성해주고, Provider로 감싸주면 사용 준비 끝!
createStore를 사용하면 빨리 Redux-toolkit을 사용해달라는 경고 문구가 보일테지만...지금 당장은 외면..
//* src/pages/LandingPage/ChoiceLeague.tsx *//
// ... //
import { useDispatch } from "react-redux";
function ChoiceLeague({ country, pathname }: PropsType) {
// ... //
const dispatch = useDispatch();
useEffect(() => {
const options = {
method: "GET",
url: "https://api-football-v1.p.rapidapi.com/v3/leagues",
params: { country: country },
headers: {
"X-RapidAPI-Key": `${API_KEY}`,
"X-RapidAPI-Host": "api-football-v1.p.rapidapi.com",
},
};
axios
.request(options)
.then(function (response) {
setData(response.data.response[0]);
})
.catch(function (error) {
console.error(error);
});
}, []);
// axios 통신으로 받아온 데이터가 존재한다면 액션을 dispatch한다!
useEffect(() => {
if (data !== undefined) {
dispatch(saveBigLeague(data));
}
}, [data]);
return {
// ... //
};
}
export default ChoiceLeague;
여기도 똑같다. useDispatch를 활용하여 정의해둔 액션을 사용한다.
이 부분은 일반적인 리덕스 활용과 다를 것이 없다.
dispatch된 액션의 인자로 들어가는 것의 타입을 2번(액션 정의)에서 선언해줬다.
필자는 any라는 타입을 사용했지만, 데이터 구조가 명확하거나, 가공해서 사용할 때는 정확한 타입을 명시해주도록 하자.
//* src/pages/LeaguePages/LeagueTitle.tsx *//
// ... //
import { useSelector } from "react-redux";
import { RootState } from "../../reducer/index";
function LeagueTitle() {
// ... //
const leagueData = useSelector(
(state: RootState) => state.leagueReducer.leagueData
);
return (
// ... //
);
}
export default LeagueTitle;
앞서 dispatch한 액션을 통해 data를 리덕스 스토어에 저장했다.(액션 이름도 일부러 save라는 단어를 넣어놨으니 이해가 편하시리라 생각합니다..ㅎㅎ..)
이제 스토어에 저장을 해놨으니 꺼내서 활용하는 것이 useSelector이다.
4번(루트 리듀서 정의)에서 Rootstate라는 타입을 정의했었다.
이는 useSelecotor에서 파라미터로 사용되는 state의 타입을 정의해줄 때 사용된다.
Redux를 한번 사용해신 분이라면 '아, 정말 타입 선언 말고는 다른게 거의 없네'라는 생각을 하셨을거다.
물론 리덕스 자체만으로도 사용법이 천차만별이라 이해가 안되실 수도 있고, 필자가 잘못 사용하고 있는 걸 수도 있다..
Redux-toolkit이라는 공식으로 지원하는 일반 Redux의 업그레이드 버전이 나와있기 때문에, 그에 맞춰 공부를 진행해야할 것 같다.(얘는 그냥 js로 할 때도 머리 아팠는데 ㅠㅠ)