Front-End

vsoghlv@naver.com

React.js redux Thunk 로 promise 다루기2

먼저 만들어 놓은 것들을 modules/index.js 의 rootReducer 에 적용해주자.

 //modules/index.js
 const rootReducer = combineReducers({ counter, posts });

그 후 리덕스 안의 상태들과 연동할 컴포넌트를 만들어주자.

//프리젠테이셔널 컴포넌트  = 리덕스 스토어에 직접적으로 접근하지 않고 필요한 값, 함수를 props 로 받아와 사용
//components/PostList.js
import React from "react";

function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id} id={post.id}>
          {post.title}
        </li>
      ))}
    </ul>
  );
}

export default PostList;

//컨테이너 컴포넌트 = 리덕스 스토어의 상태조회, 액션 디스패치를 할 수 있는 컴포넌트, HTML 태그를 이용하지 않고 다른 프리젠테이셔널 컴포넌트 불러와 사용
//containers/PostListContainers.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import PostList from "../components/PostList";
import { getPosts } from "../modules/posts";

function PostListContainers() {
  //modules/index.js 안의 rootReducer 의 두번째 파라미터 posts 안의 posts 객체
  const { data, loading, error } = useSelector((state) => state.posts.posts);
  const dispatch = useDispatch();

  //처음 렌더링 될 때만
  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);

  if (loading) return <div>로딩중</div>;
  if (error) return <div>에러발생</div>;
  if (!data) return null;

  return <PostList posts={data} />;
}

export default PostListContainers;

라우터 적용하기

먼저 react-router-dom 설치 후 index.js 에서 Provider 을 감싸주고, post 를 조회를 위한 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 작성해주자

//index.js
import { BrowserRouter } from "react-router-dom";


ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

//components/Post.js
import React from "react";

function Post({ post }) {
  const { title, body } = post;
  return (
    <div>
      <h1>{title}</h1>
      <p>{body}</p>
    </div>
  );
}

export default Post;

//containers/PostContainer.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import Post from "../components/Post";
import { getPost } from "../modules/posts";

function PostContainer({ postId }) {
  const { data, loading, error } = useSelector((state) => state.posts.post);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getPost(postId));
  }, [postId, dispatch]);

  if (loading) return <div>로딩중</div>;
  if (error) return <div>에러발생</div>;
  if (!data) return null;

  return <Post post={data} />;
}

export default PostContainer;

이후 라우트를 설정해주기 위해 두개의 파일을 더 작성하고 App.js 에서 적용해줬다.

//pages/PostListPage.js
import React from 'react';
import PostListContainers from '../containers/PostListContainers';

function PostListPage() {
  return <PostListContainers />;
}

export default PostListPage;

//pages/PostPage.js
import React from "react";
import PostContainer from "../containers/PostContainer";

function PostPage({ match }) {
  const { id } = match.params; // URL파라미터 조회
  //파라미터는 무조건 문자열로 들어옴 항상 주의할 것.
  const postId = parseInt(id, 10);
  return <PostContainer postId={postId} />;
}

export default PostPage;

//App.js
import { Route } from "react-router-dom";
import PostListPage from "./pages/PostListPage";
import PostPage from "./pages/PostPage";

function App() {
  return (
    <>
      <Route path="/" component={PostListPage} exact />
      <Route path="/:id" component={PostPage} />
    </>
  );
}

export default App;

이제 기존의 components/PostList.js 파일도 변경해줘야 하는데 li 클릭시 다른 주소로 보내기 위해 Link 태그를 넣어줘야 한다.

//components/PostList.js
import React from "react";
import { Link } from "react-router-dom";

function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id} id={post.id}>
          <Link to={`/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

export default PostList;

굳이 페이지 파일을 만들지 않고 기존의 PostContainer 과 PostListContainer 에 바로 연결해도 정상적으로 작동되는데 강의에서 설명 중 편의를 위해 그렇게 한건지, 아니면 정해진 규칙인건지 잘모르겠다.

문제해결

작동은 정상적으로 잘 되는데 몇가지 문제점이 있다. 특정 포스트를 보고 다른 포스트를 볼 때 순간적으로 이전의 데이터가 남아있거나, 뒤로 가기를 했을 때 재로딩되는 문제점 등이 있다.

포스트 재로딩 해결

//container/PosiLostContainer.js
  //처음 렌더링 될 때만
  useEffect(() => {
    //data 가 존재한다면 바로 리턴
    if (data) return;
    dispatch(getPosts());
  }, [dispatch, data]);

  if (loading) return <div>로딩중</div>;
  if (error) return <div>에러발생</div>;
  if (!data) return null;

data 가 존재하면 바로 리턴하는 방식으로 변경해주면 된다. 또 다른 방법이 있는데 새로 불러는 오지만 만약 데이터가 있는 경우 로딩중 을 보여주지 않는 방법이 있다.

이 방법을 사용하면 로딩중이 보이지 않으면서 사용자가 항상 최신 상태를 받을 수 있다는 장점이 있다.

기존의 asyncUtils.js 에서 설정한 상태를 보면 로딩중일때 받는 파라미터가 없으면 null 이 들어가도록 설정했었다.

//lib/asyncUtils.js
export const handleAsyncAction = (type, key) => {
  //key 는 각 상태에서 관리하는 값. ex posts, post
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];

  return (state, action) => {
    switch (action.type) {
      case type:
        return {
          ...state,
          [key]: reducerUtils.loading(),
        };
      case SUCCESS:
        return {
          ...state,
          [key]: reducerUtils.success(action.payload),
        };
      case ERROR:
        return {
          ...state,
          [key]: reducerUtils.error(action.payload),
        };
      default:
        return state;
    }
  };
};

export const reducerUtils = {
  //기본상태
  initial: (data = null) => ({
    data,
    loading: false,
    error: null,
  }),

  //상태
  loading: (prevState = null) => ({
    data: prevState,
    loading: true,
    error: null,
  }),
  success: (data) => ({
    data,
    loading: false,
    error: null,
  }),
  error: (error) => ({
    data: null,
    loading: false,
    error,
  }),
};

해결을 위해 handleAsyncAction 에서 파라미터를 하나 더 받아와 데이터에 따라 로딩의 상태를 처리해줬다.

//lib/asyncUtils.js
export const handleAsyncAction = (type, key, keepData) => {
  //key 는 각 상태에서 관리하는 값. ex posts, post
  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),
        };
      case SUCCESS:
        return {
          ...state,
          [key]: reducerUtils.success(action.payload),
        };
      case ERROR:
        return {
          ...state,
          [key]: reducerUtils.error(action.payload),
        };
      default:
        return state;
    }
  };
};

//modules/posts.js
const getPostsReducer = handleAsyncAction(GET_POSTS, "posts", true);

//containers/PostListContainers.js

  //처음 렌더링 될 때만
  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);

  //로딩중이거나 데이터가 없을 때
  if (loading && !data) return <div>로딩중</div>;

이제 데이터가 있는 경우에도 API 를 호출하여 최신 상태를 들고오지만 필요 없는 로딩중이라는 글은 나오지 않게 되었다.

포스트를 다시 클릭했을 때 잠시 이전 내용이 보이는 문제

해결방식이 여러가지가 있는데 그 중 하나는 포스트에서 뒤로 갈 때 상태를 비워버리는 방법이다.

먼저 modules/posts.js 에서 새로운 상태와 thunk 생성함수, 그리고 리듀서를 작성해주고 container/PostContainer.js 에서 클린업 함수로 호출해주면 된다.

//modules/posts.js
//뒤로 갈 때 포스트 내용 비워버리기
const CLEAR_POST = "posts/CLEAT_POST";

export const clearPost = () => ({ type: CLEAR_POST });

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
    case GET_POSTS_SUCCESS:
    case GET_POSTS_ERROR:
      return getPostsReducer(state, action);
    case GET_POST:
    case GET_POST_SUCCESS:
    case GET_POST_ERROR:
      return getPostReducer(state, action);
    //클리어 포스트인 경우 post 값을 기본 값으로
    case CLEAR_POST:
      return {
        ...state,
        post: reducerUtils.initial(),
      };
    default:
      return state;
  }
}

//container/PostContainer.js
  useEffect(() => {
    dispatch(getPost(postId));
    //클린업 함수 컴포넌트가 언마운트 되거나, postId 바뀌어 위의 dispatch 가 호출되기 직전
    return () => {
      dispatch(clearPost());
    };
  }, [postId, dispatch]);