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];
}