Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 14
- 외부 API 연동 - 뉴스 뷰어 제작
화면에 렌더링시 영어로 보여주지 않고, 한글로 보여주도록.
import styled from 'styled-components';
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.div`
font-size: 1.125rem;
cursor: pointer;
white-space: pre;
text-decoration: none;
color: inherit;
padding-bottom: 0.25rem;
&:hover {
color: #495057;
}
& + & {
margin-left: 1rem;
}
`;
const Categories = () => {
return (
<CategoriesBlock>
{categories.map(c => (<Category key={c.name}>{c.text}</Category>))}
</CategoriesBlock>
);
};
export default Categories;
CategoriesBlock
이라는 전체를 감싸는 div 스타일 컴포넌트와Category
라는 각 카테고리를 꾸미는 스타일 컴포넌트를 만듬.useState
로 관리 -> App.js 에서.App.js
import { useState, useCallback } from 'react';
import Categories from './components/Categories';
import NewsList from './components/NewsList';
const App = () => {
const [category, setCategory] = useState('all');
const onSelect = useCallback(category => setCategory(category), []);
return (
<>
<Categories category={category} onSelect={onSelect} />
<NewsList category={category}/>
</>
);
}
export default App;
state
)는 현재 클릭한 카테고리로 이동할 수 있게 해주는 상태이고,Categoriesjs
import styled, { css } from 'styled-components';
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.div`
font-size: 1.125rem;
cursor: pointer;
white-space: pre;
text-decoration: none;
color: inherit;
padding-bottom: 0.25rem;
&:hover {
color: #495057;
}
${props =>
props.active && css`
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
`}
& + & {
margin-left: 1rem;
}
`;
const Categories = ({onSelect, category}) => {
return (
<CategoriesBlock>
{categories.map(c => (
<Category
key={c.name}
active={category === c.name}
onClick={() => onSelect(c.name)}
>
{c.text}
</Category>))}
</CategoriesBlock>
);
};;
export default Categories;
${props =>
props.active && css`
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
`}
-> 이 구문처럼 사용하므로, 형식 외워두자.
&{props => props.active && css
스타일
}
<Category
key={c.name}
active={category === c.name}
onClick={() => onSelect(c.name)}
>
active
를 줌.state
)값이 c.name이 됨.active
props가 true가 됨.이제 클릭시 스타일이 적용되는 것은 구현했으니,
데이터 연동시 카테고리를 지정하는 것을 구현해야 한다.
NewsList.js (수정)
import { useState, useEffect } from 'react';
import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
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 [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
// 🔻 수정된 부분 (query + url 템플릿 리터럴)
try {
const query = category === 'all' ? '' : `&category=${category}`;
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f6052deb31a34f38b602753e2ddf0daf`
);
setArticles(response.data.articles);
} catch (e) {
console.log(e);
}
setLoading(false);
};
fetchData();
}, [category]);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>
}
if (!articles) {
return null;
}
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
추가로 category 값이 바뀔 때마다 뉴스를 새로 불러와야 하므로, useEffect의 deps
에 category를 넣어줌.
이 컴포넌트는 componentDidMount와 componentDidUpdate시 요청을 시작하도록 설정해야 함.
-> useEffect에서는 한번에 설정할 수 있음. (두개를 합쳐놓은 기능임)
$ yarn add react-router-dom
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
src/pages/NewsPage.js
import { useParams } from "react-router-dom";
import Categories from "../Categories";
import NewsList from "../NewsList";
const NewsPage = () => {
const params = useParams();
const category = params.category || 'all';
return (
<>
<Categories />
<NewsList category={category} />
</>
)
}
export default NewsPage;
props
로 보냄.import { Route, Routes } from 'react-router-dom';
import NewsPage from './components/pages/NewsPage';
const App = () => {
return (
<Routes>
<Route path="/" element={<NewsPage />} />
<Route path="/:category" element={<NewsPage />} />
</Routes>
);
}
export default App;
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
const categories = [
...
];
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 = () => {
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;
const query = category === 'all' ? '' : `&category=${category}`;
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f6052deb31a34f38b602753e2ddf0daf`
);
styled(NavLink)
className={({ isActive }) => (isActive ? 'active' : undefined)}
Promise
를 사용해야 하는 경우, 더욱 간단하게 해주는 커스텀 Hook을 만들어보자.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];
}
deps
는 배열의 형태로 받아야 한다. // eslint-disable-next-line react-hooks/exhaustive-deps
커스텀 Hook 사용
- NewList 컴포넌트에서 API를 불러오므로, 여기서
usePromise
를 사용해보자.- return [loading, resolved, error] 이므로, 각 값을 구조분해 할당으로 받아온다.
import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
import usePromise from '../lib/usePromise';
const NewsListBlock = styled.div`
...
`;
const NewsList = ({category}) => {
const [loading, response, error] = usePromise(() => {
const query = category === 'all' ? '' : `${category}`;
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f6052deb31a34f38b602753e2ddf0daf`
);
}, [category]);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>
}
if (!response) {
return null;
}
if (error) {
return <NewsListBlock>에러 발생!</NewsListBlock>
}
const { articles } = response.data;
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
중요
- useEffect 내부 함수에는 async/await을 직접 작성하면 안된다.
- 함수 내부에 async함수를 따로 등록한 후, 호출하는 방식으로 사용해야 한다.
styled-components 문서 참조하기.
참고 - API가 많아지면 요청을 위한 상태관리가 번거로움. (리덕스로 해결)