벨로퍼트 리액트 - 7. 리덕스 미들웨어
7-6. API 재로딩 문제 해결하기
1) 포스트 목록 재로딩 문제 해결하기
해결방법 1. 데이터가 이미 존재할 경우 요청을 하지 않게 한다
containers/PostListContainer.js 수정
import React, { useEffect } from "react";
import { useSelector, useDispatch } from 'react-redux';
import PostList from "../components/PostList";
import { getPosts } from "../modules/posts";
function PostListContainer() {
const { data, loading, error } = useSelector(state => state.posts.posts);
const dispatch = useDispatch();
// 컴포넌트 마운트 후 포스트 목록 요청
useEffect(() => {
if (data) return; // 포스트 목록이 있을때는 재로딩 하지 않게 함
dispatch(getPosts());
}, [data, dispatch]);
if (loading) return <div>로딩중...</div>;
if (error) return <div>에러 발생!!</div>;
if (!data) return null;
return <PostList posts={data} />;
};
export default PostListContainer;
해결방법 2. 로딩을 새로 하되, 로딩중… 을 띄우지 않는것.
장점! : 사용자에게 좋은 경험을 제공하면서도 뒤로가기를 통해 다시 포스트 목록을 조회할때 최신 데이터를 보여줄 수 있다.
lib/asyncUtils.js 의 handleAsyncActions 수정
// 비동기 관련 액션 처리하는 리듀서
// type: 액션의 타입, key: 상태의 key (ex: posts, post)
export const handleAsyncActions = (type, key, keepData = false) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
switch (action.type) {
case type:
return {
...state,
[key]: reducerUtils.loading(keepData ? state[key].data : null)
// keepData가 true라면 로딩할때에도 데이터를 유지하게 함
};
case SUCCESS:
return {
...state,
[key]: reducerUtils.success(action.payload)
};
case ERROR:
return {
...state,
[key]: reducerUtils.error(action.payload)
};
default:
return state;
}
};
};
modules/posts.js 의 posts 리듀서 수정
// => 최종 리팩토링
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActions(GET_POST, 'post')(state, action);
default:
return state;
}
};
containers/PostListContainer.js 수정
import React, { useEffect } from "react";
import { useSelector, useDispatch } from 'react-redux';
import PostList from "../components/PostList";
import { getPosts } from "../modules/posts";
function PostListContainer() {
const { data, loading, error } = useSelector(state => state.posts.posts);
const dispatch = useDispatch();
// 컴포넌트 마운트 후 포스트 목록 요청
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
if (loading && !data) return <div>로딩중...</div>;
// 로딩중이면서 데이터가 없을때에만 로딩중...을 띄우게 함
if (error) return <div>에러 발생!!</div>;
if (!data) return null;
return <PostList posts={data} />;
};
export default PostListContainer;
⇒ 뒤로가기를 눌렀을때 새 데이터를 요청하지만, 문구는 나타나지 않음!
2) 포스트 조회시 재로딩 문제 해결하기
특정 포스트를 조회하는 과정에서 재로딩 문제를 해결하려면 위의 해결 방법으로는 할 수 없다.
→ 어떤 파라미터가 주어졌는지에 따라 결과물이 다르기 때문.
해결 방법 1. 컴포넌트가 언마운트될때 포스트 내용을 비우도록 한다.
modules/posts.js ← CLEAR_POST 액션 추가
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
import {
createPromiseThunk,
reducerUtils,
handleAsyncActions
} from '../lib/asyncUtils';
// 액션 타입
// 포스트 여러개 조회하기
const GET_POSTS = 'GET_POSTS'; // 요청 시작
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // 요청 성공
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // 요청 실패
// 포스트 하나 조회하기
const GET_POST = 'GET_POST'; // 요청 시작
const GET_POST_SUCCESS = 'GET_POST_SUCCESS'; // 요청 성공
const GET_POST_ERROR = 'GET_POST_ERROR'; // 요청 실패
// 포스트 비우기
const CLEAR_POST = 'CLEAR_POST';
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
export const clearPost = () => ({type: CLEAR_POST});
// => 리팩토링
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// => 최종 리팩토링
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActions(GET_POST, 'post')(state, action);
case CLEAR_POST :
return {
...state,
post: reducerUtils.initial()
}
default:
return state;
}
};
containers/PostContainer.js ← useEffect의 cleanup 함수(useEffect에서 반환하는 함수)에서 해당 액션 디스패치
import React, {useEffect} from "react";
import { useDispatch, useSelector } from "react-redux";
import { clearPost, getPost } from "../modules/posts";
import Post from "../components/Post";
function PostContainer({ postId }) {
const { data, loading, error } = useSelector(state => state.posts.post);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPost(postId));
return () => {
dispatch(clearPost());
}
}, [postId, dispatch]);
if (loading) return <div>로딩중...</div>;
if (error) return <div>에러발생!</div>;
if (!data) return null;
return <Post post={data} />;
}
export default PostContainer;
포스트 페이지에서 떠날대마다 포스트를 비우게 됨.
⇒ 다른 포스트를 읽을때 이전 포스트가 보여지는 문제 해결!
그러나 이미 읽었던 포스트를 불러올때도 새로 요청하는 이슈 발생함
⇒ posts 모듈에서 관리하는 상태의 구조를 바꿔야 함.
lib/asyncUtils.js 수정
- createPromiseThunkById 작성
- handleAsyncActionsById 작성
// 특정 id를 처리하는 Thunk 생성함수
const defaultIdSelector = param => param;
export const createPromiseThunkById = (
type,
promiseCreator,
// 파라미터에서 id를 어떻게 선택할지 정의하는 함수
// 기본값 = 파라미터를 그대로 id로 사용
// 파라미터가 { id: 1, details: true } 등의 형태라면 idSelector를
// param => param.id 등으로 설정
idSelector = defaultIdSelector
) => {
const [SUCCESS, ERROR]=[`${type}_SUCCESS`, `${type}_ERROR`];
return param => async dispatch => {
const id = idSelector(param);
dispatch({type, meta: id}); // 액션이 어떤 id를 가리키는지 알기 위함
try {
const payload = await promiseCreator(param);
dispatch({ type: SUCCESS, payload, meta: id});
} catch (e) {
dispatch({type: ERROR, error: true, payload: e, meta: id})
}
};
};
// id별로 처리하는 유틸함수
export const handleAsyncActionsById = (type, key, keepData = false) => {
const [SUCCESS, ERROR]=[`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
const id = action.meta;
switch (action.type) {
case type:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.loading(
// state[key][id]가 만들어져있지 않을수도 있으니까 유효성을 먼저 검사 후 data 조회
keepData ? state[key][id] && state[key][id].data : null
)
}
};
case SUCCESS :
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.success(action.payload)
}
};
case ERROR :
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.error(action.payload)
}
};
default:
return state;
}
}
};
modules/posts.js 수정
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
import {
createPromiseThunk,
reducerUtils,
handleAsyncActions,
createPromiseThunkById,
handleAsyncActionsById
} from '../lib/asyncUtils';
// 액션 타입
// 포스트 여러개 조회하기
const GET_POSTS = 'GET_POSTS'; // 요청 시작
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // 요청 성공
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // 요청 실패
// 포스트 하나 조회하기
const GET_POST = 'GET_POST'; // 요청 시작
const GET_POST_SUCCESS = 'GET_POST_SUCCESS'; // 요청 성공
const GET_POST_ERROR = 'GET_POST_ERROR'; // 요청 실패
// thunk 사용시 꼭 모든 액션들에 대해 액션 생성함수를 만들 필요는 없다.
// 그냥 thunk 함수에서 바로 액션 객체를 만들어줘도 된다!
// => 리팩토링
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunkById(GET_POST, postsAPI.getPostById);
// => 리팩토링
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// => 최종 리팩토링
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActionsById(GET_POST, 'post', true)(state, action);
default:
return state;
}
};
containers/PostContainer.js
import React, {useEffect} from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPost } from "../modules/posts";
import Post from "../components/Post";
function PostContainer({ postId }) {
const { data, loading, error } = useSelector(
state => state.posts.post[postId]
) || {
loading: false,
data: null,
error: null
}; // 아예 데이터가 존재하지 않을때가 있으므로 비구조화 할당이 오류나지 않게 함
const dispatch = useDispatch();
useEffect(() => {
// if (data) return; // 포스트가 존재하면 아예 요청하지 않음
dispatch(getPost(postId));
}, [postId, dispatch]);
if (loading && !data) return <div>로딩중...</div>;
if (error) return <div>에러발생!</div>;
if (!data) return null;
return <Post post={data} />;
}
export default PostContainer;