본문 바로가기

프로젝트 회고 🌊

React 기능 구현 스터디 - 1. 페이지네이션 (리팩토링 회고)

스터디에서 한 React에서 Pagination 구현하기 과제를 리팩토링해보았다. 스터디 팀장님의 피드백과 팀원들의 코드를 살펴보며 추가하면 좋을 점을 정리해서 issue에 작성해서 올렸다.

 

  1. react-router-dom의 params를 사용하여 페이지 이동하기
  2. Custom hook으로 usePromise.js 만들어 재사용하기
  3. fetch 시에 5페이지씩 가져오기 (params : random -> asc 변경)
  4. 이미지 onLoad 속성 추가하기

 

 

 

 

 

 

이제 리팩토링을 해보자 🐱

 


 

1. react-router-dom 의 useParams를 통해 라우팅을 구현
 
 
이를 통해 유저가 3 페이지 혹은 4페이지에 새로고침을 하더라도 그 페이지에 계속 머물러있게 하는 기능
 
 
//App.js
function Nav() {
  return (
    <Routes>
      <Route path="/" element={<Layout />} />
      <Route path=":page" element={<PictureList />} />
      <Route index element={<Layout />} />
    </Routes>
  );
}

 

Route index란 default child routes로, 네스팅이 되기 때문에 현재까지의 경로 + '/' 를 설정할 수 있다. 

 

 

 

App.js에서 라우팅을 설정한다. 처음에는 Layout 안에 PictureList를 중첩시키고 Nav()에도 포함시켰더니 두번 렌더링이 되서 Nav()에서 PictureLsit만 라우팅해주고, Layout에 Nav()를 중첩시켰다.

 

그리고 꼭 index.js에 BrowserRouter로 감싸주자.. 꼭.. 이거 발견 못 했다가 시간 쪼끔 날렸다 휴....

//App.js

function Nav() {
  return (
    <Routes>
      <Route path="/" element={<PictureList />} />
      <Route path=":/page" element={<PictureList />} />
    </Routes>
  );
}

function App() {
  return (
    <>
      <ListProvider>
        <Layout>
          <Nav />
        </Layout>
      </ListProvider>
    </>
  );
}

export default App;

 

그리고 Pagination에 코드가 길어질 것 같아서 Pagination.jsx 컴포넌트를 따로 만들었다. 여기서 페이징을 하고 알맞은 라우트로 navigate을 이용해 이동하게 된다.

 

const Pagination = ({ currentPageGroup }) => {
  const navigate = useNavigate();

  const pageNumbers = useMemo(
    () => [...Array(5)].map((_, i) => (currentPageGroup - 1) * 5 + i + 1),
    [currentPageGroup],
  );

  const moveNextPagesGroup = () => {
    navigate(`/${currentPageGroup * 5 + 1}`);
  };

  const movePreviousPageGroup = () => {
    navigate(`/${(currentPageGroup - 1) * 5}`);
  };

  return (
    <PaginationBlock>
      {currentPageGroup === 1 ? null : (
        <button onClick={movePreviousPageGroup}>{`<`}</button>
      )}
      {pageNumbers.map((_, idx) => {
        return (
          <NavLink
            key={idx}
            className={({ isActive }) => (isActive ? "active" : null)}
            to={`/${(currentPageGroup - 1) * 5 + idx + 1}`}>
            <button>{(currentPageGroup - 1) * 5 + idx + 1}</button>
          </NavLink>
        );
      })}
      <button onClick={moveNextPagesGroup}>{`>`}</button>
    </PaginationBlock>
  );
};

 

 

PictureList.jsx 여기서는 정해진 수 만큼 데이터를 fetch 해 와야 한다. 

 

const PictureList = () => {
  let { page } = useParams();
  page = parseInt(page, 10);
  if (isNaN(page) || page < 0) {
    page = 1;
  }
  const [loading, data, error] = usePromise(page); //1page, 45개씩 fetch
  const currentPageGroup = useMemo(() => Math.ceil(page / 5), [page]);
  
  //...
  
}

 

 

usePromise는 커스텀 훅으로 데이터를 가져오는 역할을 한다.

loading, resolved, error을 하나의 배열로 return 하며, 이 훅을 호출한 곳에서 destructuring하면 된다

 

usePromise.js

//usePromise.js

function usePromise(page, limit = 9) {
  const URL = "https://api.thecatapi.com/v1/images/search";
  const API_KEY =
    "live_TMHkfzpN281MrIv3tbYggwCuoviA3a5CjNGvVIbY9bPIVbeSbTZ6rY5Ndnc2BbdP";

  const [loading, setLoading] = useState(false);
  const [resolved, setResolved] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(URL, {
          headers: {
            "x-api-key": API_KEY,
          },
          params: {
            page: page,
            limit: limit,
            order: "ASC",
          },
        });
        setResolved(response.data);
        setLoading(false);
      } catch (e) {
        setLoading(false);
        setError(e);
      }
    };
    fetchData();
  }, [page, limit]);

  return [loading, resolved, error];
}

 

 

 

마지막으로 Picture Item 컴포넌트에서는

1. context API를 통해 listed view === true 인지 데이터를 읽어온다. 만약 true라면 각 아이템의 width를 넓혀 자리를 길게 차지해 listed view가 되게 한다.

2. imgURL을 아직 못 가져왔다면 onLoad속성을 주어 ImgLoading(false)를 한다. 여전히 true일 경우 loading... 이라는 글자를 각 아이템마다 표시한다.

 

const PictureItem = ({ id, imgURL }) => {
  const [imgLoading, setImgLoading] = useState(true);
  const { listed } = useContext(ListContext).state;
  const [listview, setListview] = useState(listed);

  useEffect(() => {
    setListview(listed);
  }, [listed]);

  return (
    <PictureItemBlock listview={listview}>
      {imgLoading && <div className="loading">Loading...</div>}
      {<img src={imgURL} alt="item" onLoad={() => setImgLoading(false)} />}
      <p>Name: {id}</p>
    </PictureItemBlock>
  );
};

export default PictureItem;

 

listed === true면 styled-component에서 props를 이용해 스타일을 바꾼다.

 

${({ listview }) =>
    listview &&
    css`
      width: 500px;
      border-bottom: 0.5px solid gray;
      text-align: start;
    `}

 

listed의 데이터를 관리하는 context API는 요기

 

/* eslint-disable react/prop-types */
import { createContext, useState } from "react";

const ListContext = createContext({
  state: { listed: false },
  actions: {
    setListed: () => {},
  },
});

const ListProvider = ({ children }) => {
  const [listed, setListed] = useState(false);

  const value = {
    state: { listed: listed },
    actions: { setListed: setListed },
  };
  return <ListContext.Provider value={value}>{children}</ListContext.Provider>;
};

const ListConsumer = ListContext.Consumer;

export { ListProvider, ListConsumer };

export default ListContext;

App 어디에서나 사용 가능하다. 

 

 

 

 


 

 

routing 개념이 익숙하지 않아서 어디서 어디로 가는지 헷갈렸었다.. 연습을 더 많이 해봐야할듯 🥹 아직 공부할게 산더미같이 쌓였다는 느낌을 받았다. 좋은 경험이었따 🌊