React, NewsList + Router

2024. 12. 24. 09:19카테고리 없음

main.jsx

import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom';
import './index.css'
import App from './App.jsx'

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

 

App.jsx

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

const App = () => {
    return (
        <>
            <Routes>
                <Route path="/" element={<NewsPage />} />
                <Route path="/:category" element={<NewsPage />} />
            </Routes>
        </>
    )
};

export default App;

 

/components/Categories.jsx

import styled from 'styled-components';
import { NavLink } from 'react-router-dom';

const arrCategories = [
    { name: 'all', text: '전체보기' },
    { name: 'business', text: '비즈니스' },
    { name: 'entertainment', text: '엔터테인먼트' },
    { name: 'health', text: '건강' },
    { name: 'science', text: '과학' },
    { name: 'sports', text: '스포츠' },
    { name: 'technology', text: '기술' },
];

const CategoryTab = styled.div`
    a {
        display: inline-block;
        margin-bottom: 10px;
        border: solid 1px #000;
        text-decoration: none;
        padding: 5px 10px;
        border-left: 0;
        color: #000;
    &.active {
        font-weight: 600;
        color: #fff;
        background-color: #000;
    }
    &:first-of-type {
        border-left: solid 1px #000;
    }
`;

const Categories = ({ onSelect, category }) => {
    return (
        <CategoryTab>
            {arrCategories.map((ca) => (
                <NavLink
                    key={ca.name}
                    className={({ isActive }) => (isActive ? 'active' : '')}
                    to={ca.name === 'all' ? '/' : `/${ca.name}`}
                    onClick={() => onSelect(ca.name)}
                >
                    {ca.text}
                </NavLink>
            ))}
        </CategoryTab>
    );
};

export default Categories;

 

/components/NewsItem.jsx

import React from 'react';
import styled from 'styled-components';

const NewsItemBlock = styled.div`
    display: flex;
    align-items: center;
    margin: 5px 0;
    border: solid 1px #ccc;
    .thumbnail {
        flex-basis: 120px;
        img {
            margin: 0;
            object-fit: cover;
            max-width: 100%;
            vertical-align: middle;
        }
    }
    .contents {
        p {
            margin: 0 10px;
            padding: 0;
            &.title {
                font-weight: bold;
            }
        }
        a {
            color: #000;
        }
    }
`;

const NewsItem = ({ article }) => {
    const { title, description, url, urlToImage } = article;

    if (title.includes("Removed")) {
        return null; // 렌더링하지 않음
    }

    return (
        <NewsItemBlock>
                {urlToImage &&
                    <div className="thumbnail">
                        <a href={url} target="_blank">
                            <img src={urlToImage} alt="" />
                        </a>
                    </div>
                }
                <div className="contents">
                    <a href={url} target="_blank">
                        <p className="title">
                            {title}
                        </p>
                        <p className="description">
                            {description}
                        </p>
                    </a>
                </div>
        </NewsItemBlock>
    )
}

export default NewsItem;

 

/components/NewsList.jsx

import { useState, useEffect } from 'react';
import axios from 'axios';
import styled from 'styled-components';

import NewsItem from './NewsItem';

const NewsListBlock = styled.div`
  border: solid 1px #ccc;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.1);
  background: #fff;
`;

const ErrorMessage = styled.div`
  color: red;
  font-weight: bold;
  margin-top: 20px;
`;

// 로딩 메시지 스타일
const LoadingMessage = styled.div`
  font-size: 1.2em;
  color: #555;
  text-align: center;
  margin-top: 20px;
`;

const URL_API = 'https://newsapi.org/v2/top-headlines?country=us&pageSize=10&apiKey={APIKEY}';

const NewsList = ({ category }) => {

    const [articles, setArticles] = useState(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            setError(null);
            try {
                const query = (category === 'all' || category === undefined ) ? '&all' : `&category=${category}`;
                const URL = URL_API + query;
                const response = await axios.get(URL); // API 호출
                console.log(URL, response);
                setArticles(response.data.articles);
            } catch (e) {
                setError('데이터를 불러오는 중 오류가 발생했습니다.');
                console.error(e);
            }
            setLoading(false);
        };
        fetchData();
    }, [category]); // 최초 한 번만 실행


    if (loading) {   // 로딩 상태 처리
        return <LoadingMessage>로딩 중...</LoadingMessage>;
    }

    if (error) {
        return (
            <NewsListBlock>
                <ErrorMessage>{error}</ErrorMessage>
            </NewsListBlock>
        );
    }

    if (!articles) { // 필수요소, 없으면 오류가 발생됨, 데이터가 없을 때 처리
        return null; // 아무것도 렌더링하지 않음
    }

    return (
        <NewsListBlock>
            {articles.map((article) => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    );
};

export default NewsList;

 

 

위 코드에서 usePromise 커스텀 훅을 사용한다면

NewsList.jsx 를 수정

import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
import usePromise from '../lib/usePromise';

const NewsListBlock = styled.div`
  border: solid 1px #ccc;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.1);
  background: #fff;
`;

const ErrorMessage = styled.div`
  color: red;
  font-weight: bold;
  margin-top: 20px;
`;

// 로딩 메시지 스타일
const LoadingMessage = styled.div`
  font-size: 1.2em;
  color: #555;
  text-align: center;
  margin-top: 20px;
`;

const URL_API = 'https://newsapi.org/v2/top-headlines?country=us&pageSize=10&apiKey={APIKEY}';

const NewsList = ({ category }) => {

    const [loading, response, error] = usePromise(() => { // usePromise 커스텀 훅
        const query = (category === 'all' || category === undefined ) ? '&all' : `&category=${category}`;
        const URL = URL_API + query;
        return axios.get(URL);
    }, [category]);

    if (!response) {
        return null;
    }

    if (loading) {   // 로딩 상태 처리
        return <LoadingMessage>로딩 중...</LoadingMessage>;
    }

    if (error) {
        return (
            <NewsListBlock>
                <ErrorMessage>{error}</ErrorMessage>
            </NewsListBlock>
        );
    }

    const { articles } = response.data;

    console.log(articles);

    return (
        <NewsListBlock>
            {articles.map((article, i) => (
                <NewsItem key={article.url + i} article={article} />
            ))}
        </NewsListBlock>
    );
};

export default NewsList;

 

/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();
    }, deps);

    return [loading, resolved, error];
}