타입스크립트 환경에서 리덕스 미들웨어를 어떻게 사용할지 알아보자!
깃허브 api를 사용해서 특정 사용자의 프로필을 조회해보는 기능을 구현해 볼 것이다.
이전 글에서 만들었던 프로젝트에
yarn add axios redux-thunk
axios와 redux-thunk를 설치해보자.
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import rootReducer from "./modules";
import Thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(Thunk));
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
라이브러리를 설치했으면 Thunk를 import 해주고 applyMiddleware로 적용시켜 준다.
아래 api를 사용해볼 것이다.
GET https://api.github.com/users/:username
다음 api를 가지고 본인의 계정닉네임을 브라우저에 쳐보면
다음과같이 정보들이 나열된다.
허나 나온 이 정보들을 typescript로 변환하는거는 번거로운 일이다.
위 링크를 이용하면 손쉽게 JSON파일을 타입스크립트로 변환할 수 있다.
왼쪽에 코드를 작성하면 쉽게 typescript로 바꿀수 있다.
이제 src파일에 github.ts파일을 만들고 해당 내용을 복사해서 넣어주자.
//github.ts
import axios from "axios";
export async function getUserProfile(username: string) {
const response = await axios.get<GithubProfile>(
`https://api.github.com/users/${username}`
);
}
export type GithubProfile = {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
name: null;
company: null;
blog: string;
location: null;
email: null;
hireable: null;
bio: null;
twitter_username: null;
public_repos: number;
public_gists: number;
followers: number;
following: number;
created_at: Date;
updated_at: Date;
};
즉, 타입을 지정해둔후, 그 타입에 맞게 Profile을 받아오게 하는 방식이다.
//github/action.js
import { GithubProfile } from "../../api/github";
import { deprecated } from "typesafe-actions";
import { AxiosError } from "axios";
const { createStandardAction } = deprecated;
/* 액션 */
export const GET_USER_PROFILE = "github/GET_USER_PROFILE";
export const GET_USER_PROFILE_SUCCESS = "github/GET_USER_PROFILE_SUCCESS";
export const GET_USER_PROFILE_ERROR = "github/GET_USER_PROFILE_ERROR";
/* 액션 생성 */
export const getUserProfile = createStandardAction(GET_USER_PROFILE)();
export const getUserProfileSuccess = createStandardAction(
GET_USER_PROFILE_SUCCESS
)<GithubProfile>();
export const getUserProfileError = createStandardAction(
GET_USER_PROFILE_ERROR
)<AxiosError>();
이전에 배운형식대로 위처럼 액션함수들을 만들어도 되지만, 조금 다른 방식으로 만들어 보겠다.
//github/action.js
import { GithubProfile } from "../../api/github";
import { createAsyncAction } from "typesafe-actions";
import { AxiosError } from "axios";
/* 액션 */
export const GET_USER_PROFILE = "github/GET_USER_PROFILE";
export const GET_USER_PROFILE_SUCCESS = "github/GET_USER_PROFILE_SUCCESS";
export const GET_USER_PROFILE_ERROR = "github/GET_USER_PROFILE_ERROR";
/* 액션 생성 */
export const getUserProfileAsync = createAsyncAction(
GET_USER_PROFILE,
GET_USER_PROFILE_ERROR,
GET_USER_PROFILE_SUCCESS
)<undefined, GithubProfile, AxiosError>();
createAsyncAction을 사용하면 위 코드처럼 조금더 깔끔하게 작성이 가능하다.
전 글에서 미들웨어를 만들었던 것처럼
위와같은 파일구조로 리덕스 모듈을 분할해서 만들어볼 것이다.
//github/action.js
import { GithubProfile } from "../../api/github";
import { createAsyncAction } from "typesafe-actions";
import { AxiosError } from "axios";
/* 액션 */
export const GET_USER_PROFILE = "github/GET_USER_PROFILE";
export const GET_USER_PROFILE_SUCCESS = "github/GET_USER_PROFILE_SUCCESS";
export const GET_USER_PROFILE_ERROR = "github/GET_USER_PROFILE_ERROR";
/* 액션 생성 */
export const getUserProfileAsync = createAsyncAction(
GET_USER_PROFILE,
GET_USER_PROFILE_SUCCESS,
GET_USER_PROFILE_ERROR
)<undefined, GithubProfile, AxiosError>();
//github/types.ts
import * as actions from "./actions";
import { ActionType } from "typesafe-actions";
import { GithubProfile } from "../../api/github";
export type GithubAction = ActionType<typeof actions>;
export type GithubState = {
userProfile: {
loading: boolean;
data: GithubProfile | null;
error: Error | null;
};
};
//github/thunks.ts
import { ThunkAction } from "redux-thunk";
import { RootState } from "..";
import { getUserProfile } from "../../api/github";
import { getUserProfileAsync } from "./actions";
import { GithubAction } from "./types";
export function getUserProfileThunk(
username: string
): ThunkAction<void, RootState, null, GithubAction> {
//리턴타입, Root상태, ExtraArgument타입, action의 타입 순서대로 넣음
return async (dispatch) => {
const { request, success, failure } = getUserProfileAsync;
dispatch(request());
try {
const userProfile = await getUserProfile(username);
dispatch(success(userProfile));
} catch (e: any) {
//??
dispatch(failure(e));
}
};
}
//github/reducer.ts
import { action, createReducer } from "typesafe-actions";
import {
GET_USER_PROFILE,
GET_USER_PROFILE_ERROR,
GET_USER_PROFILE_SUCCESS,
} from "./actions";
import { GithubAction, GithubState } from "./types";
const initialState: GithubState = {
userProfile: {
loading: false,
error: null,
data: null,
},
};
const github = createReducer<GithubState, GithubAction>(initialState, {
[GET_USER_PROFILE]: (state) => ({
...state,
userProfile: {
loading: true,
error: null,
data: null,
},
}),
[GET_USER_PROFILE_SUCCESS]: (state, action) => ({
...state,
userProfile: {
loading: false,
error: null,
data: action.payload,
},
}),
[GET_USER_PROFILE_ERROR]: (state, action) => ({
...state,
userProfile: {
loading: false,
error: action.payload,
data: null,
},
}),
});
export default github;
이제 작성한 파일들을 github/index.ts안에
//modules/github/index.ts
export { default } from "./reducer";
export * from "./actions";
export * from "./types";
export * from "./thunks";
다음과 같이 모아서 내보내주기만 하면 된다.
//modules/index.ts
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
import github from "./github";
const rootReducer = combineReducers({
counter,
todos,
github,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
그 후에 만들었던 rootReducer에 넣어주기만 하면 된다.
이제 프리젠테이셔널 컴포넌트를 만들어 보자.
사용자 계정을 입력해서 api를 호출하는 기능과,
불러온 정보를 보여주는 기능 2개의 컴포넌트를 만들것이다.
이번에는 css파일도 작성해서 간단히 꾸며보겠다.
먼저 user정보를 입력받아서 조회하는 컴포넌트이다.
//GithubUsernameForm.tsx
import React, { useState } from "react";
import "./GithubUsernameForm.css";
type GithubUsernameFormProps = {
onSubmitUsername: (username: string) => void;
};
function GithubUsernameForm({ onSubmitUsername }: GithubUsernameFormProps) {
const [input, setInput] = useState("");
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmitUsername(input);
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
};
return (
<form onSubmit={onSubmit} className="GithubUsernameForm">
<input
onChange={onChange}
value={input}
placeholder="Github 계정 입력!!!"
/>
<button type="submit">조회</button>
</form>
);
}
export default GithubUsernameForm;
/*GithubUsernameForm.css*/
.GithubUsernameForm {
width: 400px;
display: flex;
align-items: center;
height: 32px;
margin: 0 auto;
margin-top: 16px;
margin-bottom: 48px;
}
.GithubUsernameForm input {
flex: 1;
border: none;
outline: none;
border-bottom: 1px solid black;
font-size: 21px;
height: 100%;
margin-right: 1rem;
}
.GithubUsernameForm button {
background: black;
color: white;
cursor: pointer;
outline: none;
border: none;
border-radius: 4px;
font-size: 16px;
padding-left: 16px;
padding-right: 16px;
height: 100%;
font-weight: bold;
}
.GithubUsernameForm button:hover {
background: #495057;
}
이제 받은 정보를 출력해주는 컴포넌트를 구현해 보자.
//GithubProfileInfo.tsx
import React from "react";
import "./GithubProfileInfo.css";
type GithubProfileInfoProps = {
name: string;
thumbnail: string;
bio: string;
blog: string;
};
function GithubProfileInfo({
name,
thumbnail,
bio,
blog,
}: GithubProfileInfoProps) {
return (
<div className="GithubProfileInfo">
<div className="profile-head">
<img src={thumbnail} alt="user thumbnail" />
<div>{name}</div>
</div>
<p>{bio}</p>
<div>{blog !== "" && <a href="blog">블로그</a>}</div>
</div>
);
}
export default GithubProfileInfo;
/* GithubProfilesInfo.css */
.GithubProfileInfo {
width: 400px;
margin: 0 auto;
}
.GithubProfileInfo .profile-head {
display: flex;
align-items: center;
}
.GithubProfileInfo .profile-head img {
display: block;
width: 64px;
height: 64px;
border-radius: 32px;
margin-right: 1rem;
}
.GithubProfileInfo .profile-head .name {
font-weight: bold;
}
이젠 컨테이너를 작성해주자.
//containers/GithubProfileLoader.tsx
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import GithubProfileInfo from "../components/GithubProfilesInfo";
import GithubUsernameForm from "../components/GithubUsernameForm";
import { RootState } from "../modules";
import { getUserProfileThunk } from "../modules/github";
function GithubProfileLoader() {
const { data, loading, error } = useSelector(
(state: RootState) => state.github.userProfile
);
const dispatch = useDispatch();
const onSubmitUsername = (username: string) => {
dispatch(getUserProfileThunk(username));
};
return (
<>
<GithubUsernameForm onSubmitUsername={onSubmitUsername} />
{loading && <p style={{ textAlign: "center" }}>로딩중..</p>}
{error && <p style={{ textAlign: "center" }}>에러발생!..</p>}
{data && (
<GithubProfileInfo
bio={data.bio}
blog={data.blog}
name={data.name}
thumbnail={data.avatar_url}
/>
)}
</>
);
}
export default GithubProfileLoader;
상태를 조회한다음에 각 상태에 따라 data가 있으면 출력되게 하였다.
git의 정보가 잘 출력됨을 알 수 있다.
그럼이제 작성한 위 코드들을 리팩토링하여 좀더 멋있는 코드로 다듬어보자.
'FrontEnd > React' 카테고리의 다른 글
[TypeScript] File 타입과 FileList 타입 (0) | 2022.10.06 |
---|---|
[React] .env 사용하기 (0) | 2022.09.30 |
26_타입스크립트 & 리덕스 (0) | 2022.01.07 |
25_타입스크립트 & 리액트 (0) | 2022.01.07 |
23_리액트 리덕스 미들웨어(2) (0) | 2022.01.05 |