-
리액트 리덕스 타입스크립트 적용! React-redux with typescriptFront-end/Redux 2021. 11. 21. 10:30반응형
타입스크립트를 사용 할 일이 없었다.
그런데 생겨버렸다...!
그래서 미리 좀 공부를 하려고 한다.
자바스크립트에 타입스크립트를 적용하는 건 그나마 좀 이해가 되는 부분인데, 리액트 그것도 리덕스에 타입스크립트를 적용하고 있자니 뭔가... 편한 듯 불편한 듯... 리덕스가 타입스크립트를 편하게 쓰도록 만들어놓은게 아닌가? 라는 생각이 들었다.
그렇다면... 리액트 리덕스 타입스크립트! 시작해보자!!
우선 내가 만든 것은 아주 간단하다!
input 태그에 api에 맞는 텍스트를 넣으면 단순히 호출해주는 것을 만들었다.
우선 전체적인 폴더 트리, 그리고 리덕스의 폴더트리를 보자면 아래와 같다.
자, 리덕스를 시작해보자!
우선 actions 폴더에서 타입을 만들어볼 것이다.
// src > redux > action-types > types.ts export enum ActionTypes { SEARCH_REPOSITORIES = "search_repositories", SEARCH_REPOSITORIES_SUCCESS = "search_repositories_success", SEARCH_REPOSITORIES_ERROR = "search_repositoreis_error", }
액션 타입은 enum을 이용해서 만들었다.
이는 여러가지 관련 된 상수 값들을 한 곳에 모아서 열거 할 수 있는 것을 말한다.
하지만 이렇게만 해놓으면 각각의 상수에 number가 들어가는지, array가 들어가는지, string이 들어가는지 알 수가 없다.
그래서 우리는 타입스크립트에서는 따로 타입 지정을 해주는 것이 좋다.
아래와 같이 타입 지정을 해보았다.
// src > redux > action-types > index.ts import { ActionTypes } from "./types"; interface SearchRepositoriesAction { type: ActionTypes.SEARCH_REPOSITORIES; } interface SearchRepositoriesSuccessAction { type: ActionTypes.SEARCH_REPOSITORIES_SUCCESS; payload: string[]; } interface SearchRepositoriesErrorAction { type: ActionTypes.SEARCH_REPOSITORIES_ERROR; payload: string; } export type Actions = | SearchRepositoriesAction | SearchRepositoriesSuccessAction | SearchRepositoriesErrorAction;
각각은 interface를 통해서 지정해주었다.
type과 payload를 지정했는데, payload를 이렇게 지정해줌으로써 정해진 것만 들어 갈 수 있도록 해둔 것이다.
액션 타입을 지정했으면 이제 리듀서를 만들어보자!
// src > redux > reducers > repositoriesReducer.ts import { ActionTypes } from "../action-types/types"; import { Actions } from "../action-types"; const repositoriesReducer = (state, action: Actions) => { switch (action.type) { case ActionTypes.SEARCH_REPOSITORIES: return; case ActionTypes.SEARCH_REPOSITORIES_SUCCESS: return; case ActionTypes.SEARCH_REPOSITORIES_ERROR: return; default: return state; } };
1차적으로는 일반적으로 React redux를 사용 할 때 만드는 것처럼 똑같이 switch method를 이용해서 만들고 앞서 만들었던 ActionTypes와 Actions를 import 해온다.
그리고 interface로 만들었던 Actions를 Reducer의 action의 타입으로 지정해준다.
자 이제는 state에 대한 정의와 타입이 필요하다.
// src > redux > reducers > repositoriesReducer.ts import { ActionTypes } from "../action-types/types"; import { Actions } from "../action-types"; interface RepsitoriesState { loading: boolean; error: string | null; data: string[]; } const initialState = { loading: false, error: null, data: [], }; const repositoriesReducer = ( state: RepsitoriesState = initialState, action: Actions ): RepsitoriesState => { switch (action.type) { case ActionTypes.SEARCH_REPOSITORIES: return; case ActionTypes.SEARCH_REPOSITORIES_SUCCESS: return; case ActionTypes.SEARCH_REPOSITORIES_ERROR: return; default: return state; } };
interface를 통해서 state에 들어갈 것들의 타입을 정의해주었고, state의 기본값을 설정해주었다.
이제 return 값도 추가해보자!
// src > redux > reducers > repositoriesReducer.ts import { ActionTypes } from "../action-types/types"; import { Actions } from "../action-types"; interface RepsitoriesState { loading: boolean; error: string | null; data: string[]; } const initialState = { loading: false, error: null, data: [], }; const repositoriesReducer = ( state: RepsitoriesState = initialState, action: Actions ): RepsitoriesState => { switch (action.type) { case ActionTypes.SEARCH_REPOSITORIES: return { ...state, loading: true, error: null, data: [], }; case ActionTypes.SEARCH_REPOSITORIES_SUCCESS: return { ...state, loading: false, error: null, data: action.payload, }; case ActionTypes.SEARCH_REPOSITORIES_ERROR: return { ...state, loading: false, error: action.payload, data: [], }; default: return state; } }; export default repositoriesReducer;
return 값을 추가해보았다.
search만 할 때는 당연히 loading은 true여야 할 것이고, 아직 error는 null 상태일 것이며, data는 없을 것이다.
success 상태일 때는 loading은 끝났을테니 false일 것이고 error도 없으니 null 상태이고, data만 payload를 받으면 된다.
error 일 때는 loading은 끝났을 것이다. 다만 error를 payload를 통해서 string 형태로 받을 것이고, data는 비어있을 것이다.
그리고 한 곳에서 모아서 사용하기 위해서 export default를 해주었다.
이제는 combineReducers를 통해서 한 곳에 모아보겠다.
// src > redux > reducers > index.ts import { combineReducers } from "redux"; import repositoriesReducer from "./repositoriesReducer"; const reducers = combineReducers({ allReducers: repositoriesReducer, }); export default reducers;
만든 repsitoriesReudcer를 allReducers라는 이름으로 만들었다.
이제 store를 만들어보자!
//src > redux > store.ts import { createStore, applyMiddleware } from "redux"; import { composeWithDevTools } from "redux-devtools-extension"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunk))); export default store;
store에는 store를 만들기 위한 createStore, middleware를 사용하기 위한 applyMiddleware, thunk를 사용 할 것이므로 thunk도 넣어주었고, 사용되는 것을 체크하기 위해서 redux devtools extenstion 또한 넣었다(devtools는 옵션이다).
이제 src > index.tsx로 가서 Provider를 넣어주자
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById("root") );
어떤 분들은 App.tsx에 넣어주기도 하는데, 나는 주로 index에 Provider를 넣어주는 편이다.
// src > redux > index.ts export * from "./store"; export * as ActionCreator from "./action-creators/index"; export * from "./reducers/index";
아직 action에 못다한 부분이 있다.
action-creators로 가서 actions를 정리하자.
// src > redux > action-creators > index.ts import axios from "axios"; import { Dispatch } from "redux"; import { ActionTypes } from "../action-types/types"; import { Actions } from "../action-types"; export const searchRepositories = (term: string) => { return async (dispatch: Dispatch<Actions>) => { dispatch({ type: ActionTypes.SEARCH_REPOSITORIES, }); try { const { data } = await axios.get(`https://registry.npmjs.org/-/v1/search`, { params: { text: term, }, }); const names = data.objects.map((result: any) => { return result.package.name; }); dispatch({ type: ActionTypes.SEARCH_REPOSITORIES_SUCCESS, payload: names, }); } catch (err) { dispatch({ type: ActionTypes.SEARCH_REPOSITORIES_ERROR, payload: "에러발생!!! 🙈", }); } }; };
내가 사용 할 api 주소는 아래와 같다.
https://registry.npmjs.org/-/v1/search?text=redux
그래서 api의 마지막 부분인 redux는 react 등 다른 text로 대체 되므로 search 까지만 사용을 해서 데이터의 이름만 불러오게 만들 것이다.
try~catch문을 이용해서 try문에서는 api를 불러오고 map을 이용해 이름만 뽑아내서 type이 success인 곳으로 payload를 보냈다.
ERROR가 나서 catch에서 걸리면 type이 error인 곳으로 string payload가 가도록 만들었다.
redux 폴더에 index.ts를 만들어서 정리를 좀 해주자
// src > redux > index.ts export * from "./store"; export * as ActionCreator from "./action-creators/index"; export * from "./reducers/index";
자, 여기서 조금 다른 것을 해야한다.
redux typescript를 위해서 hooks를 만들어야하는 것이다.
// src > hooks > useAction.ts import { useDispatch } from "react-redux"; import { bindActionCreators } from "redux"; import { actionCreator } from "redux"; export const useActionsHook = () => { const dispatch = useDispatch(); return bindActionCreators(actionCreator, dispatch); };
첫 번째 hook은 bindActionCreators, useDispatch를 통해서 만들었다
두 번째 hooks가 조금 문제인데, 이것이 최선일까.. 싶은 생각이 들었던 것이다.
// src > redux > reducers > index.ts import { combineReducers } from "redux"; import repositoriesReducer from "./repositoriesReducer"; const reducers = combineReducers({ allReducers: repositoriesReducer, }); export default reducers; export type RootState = ReturnType<typeof reducers>;
우선 reducers를 모아둔 combineReducers로 가서 RootState를 만들어준다.
그리고 ReturnType을 지정해주고 typeof를 만들어준다.
이걸 왜 이렇게 하는지는... Documentation이 하라고 했다... ㅠㅠㅠㅠㅠ
// src > hooks > useTypedSelector.ts import { useSelector, TypedUseSelectorHook } from "react-redux"; import { RootState } from "../redux/reducers"; export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
또한 새로운 hook을 만들어서 위와 같이 만들어준다.
이유는 없다.. 시키는대로..ㅠㅠㅠ 굳이 이렇게까지 해야하나 싶다...ㅠ
이렇게 하면 우선은 redux 쪽 정리는 끝이난다.
components 폴더에 Repositories.tsx를 만들어보자.
[ 1단계 ]
import React from "react"; const Repositories: React.FC = () => { return ( <div> <form> <input type="text" /> <button>SEARCH</button> </form> </div> ); }; export default Repositories;
[ 2단계 ]
import React, { useState } from "react"; import { useTypedSelector } from "../hooks/useTypedSelector"; import { useActionsHook } from "../hooks/useAction"; const Repositories: React.FC = () => { const [term, setTerm] = useState(""); const { searchRepositories } = useActionsHook(); const { data, error, loading } = useTypedSelector((state) => state.allReducers); return ( <div> <form> <input type="text" /> <button>SEARCH</button> </form> </div> ); }; export default Repositories;
[ 3단계 ]
import React, { useState } from "react"; import { useTypedSelector } from "../hooks/useTypedSelector"; import { useActionsHook } from "../hooks/useAction"; const Repositories: React.FC = () => { const [term, setTerm] = useState(""); const { searchRepositories } = useActionsHook(); const { data, error, loading } = useTypedSelector((state) => state.allReducers); const submitHandler = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); searchRepositories(term); }; const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => { setTerm(e.target.value); }; return ( <div> <form onSubmit={submitHandler}> <input type="text" value={term} onChange={changeHandler} /> <button>SEARCH</button> </form> {error && <h3>{error}</h3>} {loading && <h3>Loading...</h3>} {!error && !loading && data.map((item) => <div key={item}>{item}</div>)} </div> ); }; export default Repositories;
이렇게 정리가 되었으면 이제 App.tsx에 넣어볼 수 있다.
// src > App.tsx import React from "react"; import "./App.css"; import Repositories from "./components/Repositories"; const App: React.FC = () => { return ( <div className="App"> <h1>Search For a Package</h1> <Repositories /> </div> ); }; export default App;
이렇게 해주고 input창에 검색을 해보면 잠시 loading이 나온 뒤, redux의 이름이 검색 될 것이다.
생각보다 구현하는게 힘들었다.
특히 typescript가 익숙하지 않아서 고생했는데, hook과 같은 경우도 굳이 이렇게 redux를 써야하나 싶기도하고....
좀 더 연습해보고 익숙하게 만들어야할 것 같다..ㅠ
반응형'Front-end > Redux' 카테고리의 다른 글
react-redux toolkit 리덕스 툴킷 첫 경험 (0) 2021.09.27 초보 개발자의 Redux 깨우쳐보기! (thunk) (0) 2021.06.25