1) /modules/input.js 작성
: input 리듀서를 만들고 그 안에 username이라는 state 생성import { createAction, handleActions } from 'redux-actions'; const CHANGE_INPUT = 'input/CHANGE_INPUT'; export const changeInput = createAction(CHANGE_INPUT, (username)=>({ username, })); const initState = { username: "", }; const input = handleActions( { [CHANGE_INPUT]: (state, { payload: {username}}) => ({ ...state, username, }), }, initState ) export default input;
2) rootReducer에 추가
import { combineReducers } from "redux"; import { all } from "redux-saga/effects"; import input from './input'; const rootReducer = combineReducers({ input, }); export default rootReducer;
3) InputContainer.js 에서 사용
: input 리듀서에 포함된 username이라는 state를 사용하기 위해
useDispatch()와 useSelector()를 사용import React, {useRef} from 'react'; import InputComponent from '../components/InputComponent'; import {useDispatch, useSelector} from "react-redux"; import {changeInput} from '../modules/input'; function InputContainer() { const dispatch = useDispatch(); /* 입력받는 값을 실시간으로 username에 업데이트 */ const onHandleInputChange = (e)=>{ const value = e.target.value; dispatch(changeInput(value)); }; return ( <> <InputComponent onHandleInputChange={onHandleInputChange}> </InputComponent> </> ) } export default InputContainer
- user Reducer에 필요한 state
1) userInfo (유저의 정보)
: https://api.github.com/users/[계정이름] 의 response 정보를 저장
2) reposInfo (유저의 repo 정보)
: https://api.github.com/users/[계정이름]/repos 의 response 정보를 저장
1) /modules/user.js 작성
import { createAction, handleActions } from 'redux-actions'; import createRequestSaga from "../lib/createRequestSaga"; import { createRequestActionTypes } from "../lib/createRequestSaga"; import * as userAPI from "../lib/api/user"; import { takeLatest } from "redux-saga/effects"; /* GET_USER에 대한 성공,실패 액션을 생성 */ const [GET_USER, GET_USER_SUCCESS, GET_USER_FAILURE] = createRequestActionTypes( "user/GET_USER" ); const [GET_REPOS, GET_REPOS_SUCCESS, GET_REPOS_FAILURE] = createRequestActionTypes( "user/GET_REPOS" ); /* 액션 호출 함수 생성 */ export const getUser = createAction(GET_USER, (username) => username); export const getRepo = createAction(GET_REPOS, (username) => username); /* 해당하는 액션 호출시 Saga실행 */ const getUserSaga = createRequestSaga(GET_USER, userAPI.userInfo); const getRepoSaga = createRequestSaga(GET_REPOS, userAPI.userRepo); /* 요청된 것들 중 가장 마지막 요청만 처리 (여러번 클릭시 모두 처리되면 매우 비효율적!) */ export function* userSaga(){ yield takeLatest(GET_USER, getUserSaga); yield takeLatest(GET_REPOS, getRepoSaga); } /* State 초기값 */ const initState = { userInfo: null, error: null, reposInfo: null, } /* 액션을 store에 저장하는 리듀서를 handleActions로 쉽게 처리! */ const user = handleActions( { [GET_USER_SUCCESS]: (state, { payload: userInfo }) => ({ ...state, userInfo, }), [GET_USER_FAILURE]: (state, { payload: error }) => ({ ...state, error, }), [GET_REPOS_SUCCESS]: (state, { payload: reposInfo }) => ({ ...state, reposInfo, }), [GET_REPOS_FAILURE]: (state, { payload: error }) => ({ ...state, error, }), }, initState ); export default user;
2) rootReducer에 추가
import { combineReducers } from "redux"; import { all } from "redux-saga/effects"; import input from './input'; import user, {userSaga} from './user'; const rootReducer = combineReducers({ input, user }); /* Saga사용을 위해 생성한 userSaga()를 추가! */ export function* rootSaga() { yield all([userSaga()]); } export default rootReducer;
3) InputContainer.js 수정
: Form 작성 후 Submit 될때 getUser() / getRepo() 추가function InputContainer() { const dispatch = useDispatch(); //const nameInput = useRef(); /* dispatch로 값 넘기기 위해 useSelector로 값 가져옴 */ const {username} = useSelector(({input})=>({ username: input.username, })); const onHandleInputChange = (e)=>{ const value = e.target.value; dispatch(changeInput(value)); }; /* 추가된 부분의 핵심 */ const onHandleFormSubmit = (e) => { e.preventDefault(); dispatch(getUser(username)); dispatch(getRepo(username)); } return ( <> <InputComponent onHandleInputChange={onHandleInputChange} onHandleFormSubmit={onHandleFormSubmit} username={username} </InputComponent> </> ) }
4) ResultContainer.js수정
: 저장된 userInfo / reposInfo state를 Component에게 전달import React from 'react' import ResultComponent from '../components/ResultComponent'; import { useSelector } from "react-redux"; function ResultContainer() { /* 필요한 state들을 useSelector로 store에서 가져온다 */ const {userInfo, reposInfo, loading} = useSelector(({user, loading})=>({ userInfo: user.userInfo, reposInfo: user.reposInfo, loading: loading["user/GET_REPOS"], })); return ( <> <ResultComponent loading={loading} userInfo={userInfo} reposInfo={reposInfo}> </ResultComponent> </> ) } export default ResultContainer
5) ResultComponent.js 수정
: Container에서 보낸 userInfo와 reposInfo를 적절하게 출력 (조건부 렌더링)function SearchResult({userInfo, reposInfo, loading}) { return ( <> { userInfo && <CardTemplate> <Avatar src={userInfo.avatar_url}/> <UserInfo> <h2>{userInfo.name}</h2> <p>{userInfo.bio}</p> <FollowTemplate> <Follow><strong>Followers</strong> {userInfo.followers}</Follow> <Follow><strong>Following</strong> {userInfo.following}</Follow> <Follow><strong>Repos</strong> {userInfo.public_repos}</Follow> </FollowTemplate> <div> { reposInfo ? reposInfo.slice(0,10).map((repo, idx)=>( <Repo key={idx} href={repo.html_url} target="_blank">{repo.name}</Repo> )) : (loading == true? <Loading>LOADING</Loading>:null) } </div> </UserInfo> </CardTemplate> } </> ); } export default SearchResult
- 원리 : loading 리듀서를 만든 뒤 Saga 사용에 삽입
- startLoading() / finishLoading() 을 본 요청 전/후에 수행하여 state관리!
1) /modules/loading.js
: startLoading() / finishLoading()와 적절한 리듀서 함수 제작import { createAction, handleActions } from "redux-actions"; const START_LOADING = "loading/START_LOADING"; const FINISH_LOADING = "loading/FINISH_LOADING"; export const startLoading = createAction( START_LOADING, (requestType) => requestType ); export const finishLoading = createAction( FINISH_LOADING, (requestType) => requestType ); const initState = {}; const loading = handleActions( { [START_LOADING]: (state, action) => ({ ...state, [action.payload]: true, }), [FINISH_LOADING]: (state, action) => ({ ...state, [action.payload]: false, }), }, initState ); export default loading;
2) /lib/createRequestSaga.js 작성
: 앞서 getUserSaga / getRepoSaga를 만들 때 처럼 어떤 요청을 할 때에도
createRequestSaga를 사용하게 되기 때문에, 요청의 앞/뒤로
startLoading() / finishLoading() 수행!import { put, call } from "redux-saga/effects"; import { startLoading, finishLoading } from "../modules/loading"; export const createRequestActionTypes = (type) => { const SUCCESS = `${type}_SUCCESS`; const FAILURE = `${type}_FAILURE`; return [type, SUCCESS, FAILURE]; }; export default function createRequestSage(type, request) { const SUCCESS = `${type}_SUCCESS`; const FAILURE = `${type}_FAILURE`; return function* (action) { /* 요청 전에 값을 true로 변환 */ yield put(startLoading(type)); try { const response = yield call(request, action.payload); yield put({ type: SUCCESS, payload: response.data, meta: response, }); } catch (error) { yield put({ type: FAILURE, payload: error, error: true, }); } /* 요청이 끝나고 값을 false로 변환 */ yield put(finishLoading(type)); }; }
3) 컴포넌트에서 loading 값 가져와서 사용
: store에서 값을 가져올 때 특정 액션에 대한 loading을 가져오며 값은 true 또는 false
: loading state에 따라서 조건부 렌더링을 실시