Reference: 리액트를 다루는 기술 - 김민준
https://newsapi.org/s/south-korea-news-api
여기서 API를 연동하여 뉴스 뷰어를 만들어보자
한국 뉴스를 가져오는 API에 대한 설명서를 볼 수 있는데, axios로 fetch해온 후 보여주는 컴포넌트를 작성했다. 그러면 아래처럼 데이터를 볼 수 있는데, 이제 이 데이터를 가지고 화면에 잘 보여주면 된다.
이번에는 scss 전처리기 말고 styled-component를 활용할 예정이다.
이제 useEffect를 사용해서 컴포넌트가 처음 렌더링 되는 시점에 API를 요청하면 되는데 여기서 useEffect에 async를 붙이면 안된다.
왜냐면 useEffect 의 리턴값은 뒷정리함수이고 async는 Promise 객체를 반환하기 때문이다.
따라서 useEffect 내부에서 async/await을 사용하고 싶다면 또 다른 함수를 그 안에 만들어서 내장해 사용해야한다.
//NewsList.js
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await axios.get("https://newsapi.org/v2/top-headlines?country=kr&apiKey=213efe280f8443fcabd141b11f4d00d2")
setArticles(response.data.articles)
} catch (e) {
console.log(e)
}
setLoading(false);
}
fetchData();
},[])
아직 데이터가 준비되지 않았다면 보여지지 않게 한다. (로딩일 때 로딩중) 그리고 map을 돌기 전 articles 를 조회하여 해당 값이 null이 아닌지 검사해야한다.
if (loading) {
return <NewsListBlock>로딩중...</NewsListBlock>
}
if (!articles) {
return null;
}
map을 사용할 때는 꼭 key를 넣어줘야 DOM 업데이트때 효율성을 높일 수 있다. (업데이트 및 삭제 시)
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article}/>
))}
</NewsListBlock>
다음은 카테고리를 선택하면 해당하는 카테고리의 뉴스만 보여주는 기능을 구현해보자
선택한 <Category> styled-component 에 스타일을 다르게 적용하는게 좋다. styled-component를 사용하기 때문에 props를 받아와서 active 일 경우 이 스타일을 적용할 수 있다!
${props => props.active && css`
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
`}
props에 active 속성을 넣어주면 된다.
const Categories = ({ category, onSelect }) => {
return (
<CategoriesBlock>
{categories.map(c => (
<Category
key={c.name}
//selected(active)일 경우 props 줘서 다르게 적용
active={category === c.name}
onClick={() => onSelect(c.name)}>
{c.text}
</Category>
))}
</CategoriesBlock>
)
}
이제 데이터를 가져오면 되는데 newsapi의 url 파라미터에는 아래와 같은 옵션들이 있다.
https://newsapi.org/docs/endpoints/sources
여기서 카테고리에 selected category를 적용해주면 원하는 category의 뉴스리스트를 보여준다.
//NewsList.js
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
// 쿼리문 따로 선언하기 (all 카테고리는 없으므로 빈 스트링 주기)
const query = category === "all" ? "" : `&category=${category}`
const response = await axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
setArticles(response.data.articles)
} catch (e) {
console.log(e)
}
setLoading(false);
}
fetchData();
//카테고리가 바뀔 때 리렌더링 필요 (새로 뉴스 불러오기)
},[category])
default가 all 이고 all은 옵션에 없어서 쿼리문에 넣으면 안되기 때문에 따로 빼서 삼항연산자로 쿼리를 할당한다.
잘 보여지고 있지만
이번에는 리액트 라우터를 써서 URL 파라미터를 이용하는것으로 바꿔보자
👇
Categories안의 Category 는 NavLink로 바꿔서 styled-component 를 다시 적용해줬다. active 일 때 스타일 다르게 하려면 props로 전달했는데, NavLink는 자체적으로 isActive 라는 boolean 값을 가지고 있으므로 아래와 같이 사용하면 된다.
style = {({isActive}) => (isActive ? 'active' : undefined )}
그러면 동적으로 클래스네임에 active 일경우 active 가 추가된다. (selected 된 경우)
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 React.memo(Categories)
속성명도 className으로 바꿔야한다.
그리고 Newspage.js 를 만들어 "/"으로 사용한다.
const Newspage = () => {
const params = useParams();
const category = params.category || 'all' //없으면 기본값 all 적용
return (
<div>
<NewsList category={category}/>
</div>
)
}
선택된 카테고리가 없으면 'all'로 기본값을 적용하고 그렇지 않으면 params 에 들어있는 값을 NewsList로 전달해 category를 보내준다.
이렇게 하면 페이지 전환이 되는데, 만약 프로젝트의 크기가 더 커질 경우 useState을 이용해 상태를 관리하는 것 보다 ux를 향상시킬 수 있다.
마지막으로, API 호출처럼 비동기적 처리를 위해 Promise를 사용하는 경우 더욱 간결하게 코드를 작성할 수 있도록 custom hook을 만들어보자.
src 디렉토리 안에 lib 디렉토리를 만들고 그 안에 커스텀 훅을 넣으면 된다.
lib 디렉토리는 주로 프로젝트에서 재사용 가능한 코드를 관리하는 용도로 사용되며 API 요청을 처리하거나 데이터를 가공하는 함수, 유틸리티 함수나 상수 등을 넣어 관리한다.
=> 즉 공통적으로 사용하는 코드를 모듈화해서 중복을 피하고 유지보수성을 개선하기 위해 사용함!
usePromise.js
import { useEffect } from "react";
import { useState } from "react"
const 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-next-line react-hooks/exhaustive-deps
},deps) //dependencies eslint 경고 무시
return [loading, resolved, error] //상태 return
}
export default usePromise
주의할 점!
usePromise의 return
return [loading, resolved, error] //상태 return
NewsList에서 사용할 때 구조분해할당
const [loading, response, error] =usePromise(()=>{
const query = category === 'all' ? '' : `/${category}`
//promise 객체를 건네줘야 함
return axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
},[category])
loading, response, error로 받아서 resolved 일때의 response를 전달받음. 이후 articles는 response.data로 접근해서 사용하기!
dependencies도 두번째 인자로 잘 넘겨주고
가독성있게 articles로 할당해 추가해주면 좋다.
const { articles } = response.data
const NewsList = ({ category }) => {
const [loading, response, error] =usePromise(()=>{
const query = category === 'all' ? '' : `&category=${category}`
//promise 객체를 건네줘야 함
return axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
},[category])
if (loading) {
return <NewsListBlock active={loading === true}>로딩중...</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 React.memo(NewsList)
custom hook 만들어서 사용하는 부분이 조금 어려웠지만 어떤 값이 리턴되고, 어떤 값을 받아와서 사용하는지에 대한 큰 그림을 그려서 이해해야하는 것 같다. 이렇게 함수를 모듈화시켜서 사용하는것이 훨씬 가독성있고 간결하다 👻
[리액트를 다루는 기술] 책을 바탕으로 만들었습니다.
'Computer Programming > [리액트를 다루는 기술]' 카테고리의 다른 글
[리액트를 다루는 기술] 17. 리덕스로 리액트 앱 상태 관리하기 (2) | 2023.06.27 |
---|---|
[리액트를 다루는 기술] 15. Context API 연습 w/ Vite (0) | 2023.06.24 |
[리액트를 다루는 기술] 13. 리액트 라우터 (URL /:, 쿼리스트링, 라우트 중첩, useNavigate, Navigate) (0) | 2023.06.20 |
React immer 라이브러리로 불변성 유지하기 (0) | 2023.06.20 |
react-virtualized를 사용한 렌더링 최적화 (0) | 2023.06.20 |