본문 바로가기
JavaScript/React

React) 외부 API를 연동하여 뉴스 뷰어 만들기 ③ - 리액트 라우터 적용하기, usePromise 커스텀 Hook 만들기

by 박채니 2022. 11. 28.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

외부 API를 연동하여 뉴스 뷰어 만들기

 

리액트 라우터 적용하기

 

URL 파라미터를 사용하여 카테고리 값들을 관리해보겠습니다.

 

리액트 라우터의 설치 및 적용

 

$ yarn add react-router-dom

 

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

index.js에서 리액트 라우터를 적용시켜 주었습니다.

 

NewsPage 생성

 

src/pages/NewsPage.js

import { useParams } from "react-router-dom";
import Categories from "../components/Categories";
import NewsList from "../components/NewsList";

const NewsPage = () => {
    const params = useParams();
    // 카테고리가 선택되어있지 않다면 기본값은 'all'로 설정
    const category = params.category || 'all';

    return(
        <>
            <Categories />
            <NewsList category={category} />
        </>
    )
}

export default NewsPage;

URL 파라미터에 따라 카테고리가 선택되어 있지 않다면 'all', 선택되어 있다면 해당 카테고리 값을 props로 넘겨주었습니다.

 

App.js

import { Route, Routes } from '../node_modules/react-router-dom/dist/index';
import NewsPage from './pages/NewsPage';

function App() {
  //const [category, setCategory] = useState('all');
  //const onSelect = useCallback(category => setCategory(category), []);
  return (
    <Routes>
      <Route path='/' element={<NewsPage />} />
      <Route path='/:category' element={<NewsPage />} />
    </Routes>
  )
}

export default App;

NewsPage에서 URL에 따른 카테고리 값을 넘겨주므로, App에선 현재 카테고리 값과 onSelect 함수를 넘겨줄 필요가 없습니다.

라우트를 정의해준 것을 확인할 수 있습니다.

 

Categories에서 NavLink 사용하기

 

카테고리 선택 및 선택 카테고리에 대해 스타일을 부여해주는 기능을 NavLink로 대체해 보겠습니다.

특정 컴포넌트에 styled-components를 사용할 때는 styled(컴포넌트이름)``의 형식을 사용합니다.

 

Categories.js

import styled, { css } from "styled-components";
import { NavLink } from "../../node_modules/react-router-dom/dist/index";

const categories = [
    {
        name: 'all',
        text: '전체보기'
    },
    {
        name:'business',
        text: '비즈니스'
    },
    {
        name: 'entertainment',
        text: '연예'
    },
    {
        name: 'health',
        text: '건강'
    },
    {
        name: 'science',
        text: '과학'
    },
    {
        name: 'sports',
        text: '스포츠'
    },
    {
        name: 'technology',
        text: '기술'
    }
];

const CategoriesBlock = styled.div`
    display: flex;
    padding: 1rem;
    width: 768px;
    margin: 0 auto;
    @media screen and (max-width: 768px) {
        width: 100%;
        overflow-x: auto;
    }
`;

const Category = styled(NavLink)`
    font-size: 1.125rem;
    cursor: pointer;
    white-space: pre;
    text-decoration: none;
    color: inherit;
    padding-bottom: 0.25rem;

    &:hover {
        color: #495057;
    }

    &.active {
        font-weight: 600;
        border-bottom: 2px solid #22b8cf;
        color: #22b8cf;
        &:hover {
            color: #3bc9db;
        }
    }

    & + & {
        margin-left: 1rem;
    }
`;

const Categories = ({ category, onSelect }) => {
    return (
        <CategoriesBlock>
            {categories.map(c => (
                <Category 
                    key={c.name}
                    className={({isActive}) => (isActive ? 'active' : undefined)}
                    to={c.name === 'all' ? '/' : `/${c.name}`}
                >
                    {c.text}
                </Category>
            ))}
        </CategoriesBlock>
    )
}

export default Categories;

NavLink로 만들어진 Category 컴포넌트의 to값은 '/카테고리이름'으로 설정해주었습니다.

다만, 전체보기인 경우에는 '/all' 대신 '/'로 설정해주었습니다. 잘 렌더링 되는 것을 확인할 수 있습니다.

 


usePromise로 커스텀 Hook 만들기

 

API 호출 등으로 인해 Promise를 사용해야 하는 경우 코드를 더욱 간결하게 작성할 수 있도록 커스텀 Hook을 만들어 보겠습니다.

 

src/lib/usePromise.js

import { useState, useEffect } from "react";

export default function usePromise(promiseCreator, deps) {
    // 대기중/완료/실패에 대한 상태 관리
    const [loading, setLoading] = useState(false);
    const [resolved, setResolved] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        const process = async () => {
            setLoading(true);

            try {
                const resolved = await promiseCreator();
                setResolved(resolved);
            } catch(e) {
                setError(e);
            }

            setLoading(false);
        };
        process();
        /* eslint-disable react-hooks/exhaustive-deps */
    }, deps);
    return [loading, resolved, error];
}

usePromise 커스텀 Hook은 Promise의 대기 중, 완료 결과, 실패 결과에 대한 상태를 관리합니다.

usePromise의 의존 배열 deps를 파라미터로 받아오고 이를 useEffect의 의존 배열로 설정합니다.

(이 때 ESLink 경고가 나타나게 되고 해당 오류의 커서를 가져다댄 후 빠른 수정...문구를 클릭하면 해당 규칙을 무시하는 주석을 입력할 수 있음)

 

NewsList.js

import { useState, useEffect } from "react";
import styled from "styled-components";
import NewsItem from "./NewsItem";
import axios from "axios";
import usePromise from "../lib/usePromise";

const NewsListBlock = styled.div`
    box-sizing: border-box;
    padding-bottom: 3rem;
    width: 768px;
    margin: 0 auto;
    margin-top: 2rem;
    @media screen and (max-width: 768px) {
        width: 100%;
        padding-left: 1rem;
        padding-right: 1rem;
    }
`; 

const NewsList = ({ category }) => {
    const [loading, resolved, error] = usePromise(() => {
        const query = category === 'all' ? '' : `&category=${category}`;
        return axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=460a690efc5748c7979f8762eea705fe`);
    }, [category]);

    // 대기 중일 때
    if(loading) {
        return <NewsListBlock>대기 중...</NewsListBlock>
    }

    // 아직 resolved 값이 설정되지 않았을 때
    if(!resolved) {
        return null;
    }

    // 에러 발생 시
    if(error) {
       return <NewsListBlock>에러 발생!</NewsListBlock>
    }

    // resolved값이 유효하고 데이터를 가져온 후 (loading = false)
    const { articles } = resolved.data;
    return (
        <NewsListBlock>
            {articles.map(article => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    )

}

export default NewsList;

usePromise에 API 값을 가져와 리턴하는 함수 자체를 파라미터로 넘겨주었습니다.

이를 실행 한 후 그에 따른 작업을 수행하여 값을 return해주고 이를 받아 사용하는 것을 확인할 수 있습니다.