<리액트를 다루는 기술> 14장 외부 API를 연동하며 뉴스 뷰어 만들기를 공부하면서 정리한 내용입니다📚
중간에 모르는 것들은 따로 서치를 해서 공부했습니다.
홈페이지에 들어가보면 무료 개발자 플랜은 로컬호스트 같은 개발 환경에만 적용되고 외부 환경에서 적용하려면 돈을 내야함... 실화냐?
그래서 깃헙 페이지로 퍼블리싱하면 아무것도 안뜸..
props까지 보면 이런 느낌입니다.
function NewsItem({article}){
// article을 prop로 받아온다.
// article에는 'title', 'description', 'url', 'urlToImage'의 정보가 들어있음
const { title, description, url, urlToImage} = article;
//구조 분해를 이용해서 article.title → title로 할당하기
return (
<NewsItemBlock>
{/* urlToImage가 있는 경우: 썸네일 요소 형성 */}
{urlToImage && (
<div className="thumbnail">
<a href={url} target="_blank" rel="noopener noreferrer">
<img src={urlToImage} alt="thumbnail" />
</a>
</div>
)}
{/* 컨텐츠 영역 */}
<div className="contents">
{/* 제목 */}
<h2>
<a href={url} target="_blank" rel="noopener noreferrer">
{title}
</a>
</h2>
{/* 설명 */}
<p>{description}</p>
</div>
</NewsItemBlock>
)
}
import styled from "styled-components";
const NewsItemBlock = styled.div`
display: flex;
.thumbnail {
margin-right: 1rem;
img {
display: block;
width: 160px;
height: 100px;
object-fit: cover;
}
}
.contents {
h2{
margin: 0;
a {
color: black
}
}
p {
margin: 0;
line-height: 1.5;
margin-top: 0.5rem;
white-space: normal;
}
}
& + & {
margin-top: 3rem;
}
`;
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import axios from '../node_modules/axios/index';
import NewsItem from './NewsItem';
// 카테고리를 props로 받아옴
function NewsList({category}) {
// 뉴스들 데이터를 담아놓는 곳
const [articles, setArticles] = useState(null);
// 로딩 상태를 담아놓는 곳
const [loading, setLoading] = useState(false);
// api 주소에 카테고리 값으로 끼워넣을 상수 생성
// category가 'all'이면 카테고리 설정x, 아니면 &category=${category}
const query = category === 'all' ? '' : `&category=${category}`;
//useEffect: 처음 렌더될 때 뉴스들의 데이터를 axios로 받아옴
useEffect(()=>{
//async를 사용하는 함수 따로 선언
const fetchData = async() => {
//지금은 로딩중!
setLoading(true);
//아래의 코드를 츄라이
try {
//axios로 API를 호출한다.
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`
);
//articles에 데이터를 담는다.
setArticles(response.data.articles)
} catch(err) {
//오류가 생길 경우엔
// 콘솔로 오류 찍어주세요
console.log(err)
}
// 로딩끝♥
setLoading(false);
}
//fetchdata 사용
fetchData();
},[category])
// 로딩중일 때
if (loading) {
return <NewsListBlock>대기중</NewsListBlock>;
}
//아직 article의 값이 없을 때 (null일 때)
if (!articles) {
return null;
}
return (
<NewsListBlock>
//map 메서드를 이용하여 각 기사들을 NewsItem 컴포넌트로 생성한다.
{articles.map((article) => (
//각 기사의 내용은 article props로 넘김
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
//useEffect: 처음 렌더될 때 뉴스들의 데이터를 axios로 받아옴
useEffect(()=>{
//async를 사용하는 함수 따로 선언
const fetchData = async() => {
//지금은 로딩중!
setLoading(true);
//아래의 코드를 츄라이
try {
//axios로 API를 호출한다.
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`
);
//articles에 데이터를 담는다.
setArticles(response.data.articles)
} catch(err) {
//오류가 생길 경우엔
// 콘솔로 오류 찍어주세요
console.log(err)
}
// 로딩끝♥
setLoading(false);
}
//fetchdata 사용
fetchData();
},[category])
usePromise.js
💟 커스텀 훅 usePromise(데이터를 가져오는 함수, useEffect에 쓰일 의존배열)
import { useEffect, useState } from "react";
// promiseCreator : promise를 만들어주는 함수, 데이터를 가져오는 함수(axios,fetch)가 들어가야한다.
// deps: 의존배열
// 이 두 인자에는 지금 특별한 값이 들어가지 않았다. 고정틀을 위해 임시로 넣어준 것들임.
export default function usePromise(promiseCreator,deps) {
// 대기중/완료/실패에 대한 상태관리
const [loading,setLoading] = useState(false); //대기
const [resolved, setResolved] = useState(null); //완료(데이터)
const [error, setError] = useState(null); //실패
// useEffect()로
useEffect(()=>{
//process 함수
const process = async () => {
setLoading(true); //로딩중임!!!!💞
try{
// promiseCreator() 함수(axois, fetch 등)로 데이터를 가져와라..
// await - promiseCreator()가 값을 가져올 때까지 존버하겠다.
const resolved = await promiseCreator();
// resolved에 데이터 입력완.
setResolved(resolved);
} catch(err) {
//에러가 생길 경우
//콘솔로 찍어주세요
setError(err)
}
//로딩끝♥
setLoading(false);
};
//process 실행
process();
// eslint-disable-next-line react-hooks/exhaustive-deps
// ↑ esLint 경고를 막기 위한 주석
},deps);
//deps는 useEffect의 의존배열. 특별한 값은 없고 틀을 만들기 위해 임시로 넣어준 값임
//로딩 상태, 데이터 상태, 에러 상태를 반환함
return[loading,resolved,error]
}
NewsList.js
function NewsList({ category }) {
//usePromise는 loading, resolved, error을 반환하는 함수임.
//구조분해로 깔끔하게 할당해주고
const [loading, response, error] = usePromise(() => {
//어이.. 이 밑은 promiseCreator의 코드다.
// 카테고리의 주소를 정해주는 변수
const query = category === 'all' ? '' : `&category=${category}`;
//✨데이터를 가져오는 함수❤(promiseCreator가 뱉는 값)
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`,
);
}, [category]);
//category가 달라질 때마다 렌더링해야함. 의존 배열에 카테고리 넣어주
// 대기중일 때
if (loading) {
return <NewsListBlock>대기중</NewsListBlock>;
}
//아직 response 값이 설정되지 않았을 때(article 값이 null일 때)
if (!response) {
return null;
}
// 아직 response 값이 설정되지 않을때
if (error) {
return <NewsListBlock>에러 발생!</NewsListBlock>;
}
// article 값이 잇음 (article 값이 잇음)
const { articles } = response.data;
return (
<NewsListBlock>
//map으로 데이터 나눠서 컴포넌트 만들어줘
{articles.map((article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
복붙ㄱ
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;
}
`;
리액트 라우터 설치 : npm i react-router-dom
import { BrowserRouter } from '../node_modules/react-router-dom/index';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
function App() {
return (
<Routes>
//기본 페이지 - 전체보기(all)
<Route path='/' element={<NewsPage />} />
//카테고리 페이지 - 카테고리를 누르면 해당 카테고리로 이동
<Route path='/:category' element={<NewsPage />} />
</Routes>
);
}
export default App;
function NewsPage() {
//useParams으로 링크의 URL 파라미터를 이용할 것임
// .../:category면 category가 URL의 파라미터임
const params = useParams();
//카테고리가 선택되지 않았으면 기본값으로 all을 사용~
const category = params.category || 'all';
return (
<>
//카테고리들
<Categories />
//해당 카테고리의 NewsList가 나오도록 category를 props로
<NewsList category={category} />
</>
)
}
News API에는 총 6개의 카테고리가 있습니다.
Categories.js
const categories = [
{
name: 'all',
text: '전체보기',
},
{
name: 'business',
text: '비즈니스',
},
{
name: 'entertainment',
text: '엔터테인먼트',
},
{
name: 'health',
text: '건강',
},
{
name: 'science',
text: '과학',
},
{
name: 'sports',
text: '스포츠',
},
{
name: 'technology',
text: '기술',
},
];
Categories.js
//카테고리 wrap 스타일
const CategoriesBlock = styled.div`
display: flex;
padding: 1rem;
width: 768px;
margin: 0 auto;
@media screen and (max-width: 768px) {
width: 100%;
overflow-x: auto;
}
`;
//카테고리 NavLink
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;
}
& + & {
margin-left: 1rem;
}
//카테고리가 active일 경우 적용될 클래스: active
&.active {
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
}
`;
Categories.js
function Categories() {
return (
//카테고리 wrap
<CategoriesBlock>
{/* map 메서드로 각 카테고리들(NavLink)을 생성 */}
{categories.map((c) => (
//key에는 고유한 이름이 들어가도록 c.name을 쓴다
<Category key={c.name}
//active 상태면 active 클래스를, 아니면 그없
className={({isActive})=>(isActive ? 'active' : undefined)}
//NavLink의 주소!
//'all'이면 기본페이지로 그 외의 카테고리면 '/카테고리이름'
to={c.name ==='all' ? '/' : `/${c.name}`}>
{c.text}
</Category>
))}
</CategoriesBlock>
);
}
저번 투두리스트 앱도 그렇고 category 데이터를 props로 일일이 전달해주는게 매우 귀찮다.
투두리스트 앱 때는 아예 상단부터 맨 하단의 일정 항목까지 props로 함수와 state를 전달했다...
다음 장에 나올 context API는 이런 불편함을 어느정도 해소해준다고 한다.
<리액트를 다루는 기술> - 김민준(벨로퍼트), 길벗