이 글은 『리액트를 다루는 기술』(개정판/ 김민준 저 / 길벗 출판사)이라는 책을 참고하여 썼습니다.
학습범위: p354 ~p390
14.1 비동기 작업의 이해
동기: 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업 불가능.
비동기: 동시에 여러가지 요청 처리 가능. 기다리는 과정에서 다른 함수 호출 할 수 있음.
14.2 axios로 API 호출해서 데이터 받아오기
axios
: 현재 가장 많이 사용되고 있는 자바스크립트 HTTP 클라이언트이다.
이 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 처리한다는 점이다.
import { useState } from "react";
import axios from "axios";
function App() {
const [data, setData] = useState(null);
const handleBtnAxios = () => {
axios
.get("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => {
setData(response.data);
});
};
return (
<div>
<div>
<button onClick={handleBtnAxios}>불러오기</button>
</div>
{data && (
<textarea
rows={7}
value={JSON.stringify(data, null, 2)}
readOnly={true}
/>
)}
</div>
);
}
export default App;
새로 알게된 부분
axios.get 함수는 파라미터로 전달된 주소에 GET 요청을 해준다.
이에 대한 결과는 .then을 통해 비동기적으로 확인할 수 있다.
(아 저렇게 쓰는거구나를 알게되었다. axios.get()안에는 url 주소가 들어가고, .then()안에는 결과물이 들어가는가보다.
그리고 useState에서 정의해준 setData(response.data)이런식으로 들어가게 해주나보다. 그리고 쓸때는 {data}를 jsx로 써주는 거고!)
JSON.strigify(data,null,2)
첫번쨰 인자는 JSON 문자열로 변환할 값
두번째 인자는 null이면 객체의 모든 속성들이 JSON 문자열 결과에 포함된다.
세번째 인자는 공백으로 사용되는 space의 수를 나타낸다.
readOnly={true}는 읽기 전용임을 명시한다.
async/await으로 바꾸기
import { useState } from "react";
import axios from "axios";
function App() {
const handleBtnAxios = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos/1"
);
setData(response.data);
} catch (e) {
console.log(e);
}
};
return (
<div>
<div>
<button onClick={handleBtnAxios}>불러오기</button>
</div>
{data && (
<textarea
rows={7}
value={JSON.stringify(data, null, 2)}
readOnly={true}
/>
)}
</div>
);
}
export default App;
(오! 저렇게 쓰는구나 async/ await 쓸때는 try/catch구문 써주고, async는 ()앞에 붙여주고, try구문안에 const response= await axios.get(url) 써주고,setData(response.data)로 써주는 구나. 그리고 catch구문은 catch(e){ console.log(e)} 이런식으로 쓰는군)
14.4 뉴스뷰어 UI 만들기
//components/NewsItem.js
import styled from "styled-components";
const NewsItem = ({ article }) => {
const { title, description, url, urlToImage } = article;
return (
<NewsItemBlock>
{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>
);
};
export default NewsItem;
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;
}
`;
(
const NewsItem= ({article) => {
const {title, description, url, urlToImage} = article;
...
이런식으로 받아오는구나. 맞아맞아 )
//components/NewsList.js
import styled from "styled-components";
import NewsItem from "./NewsItem";
const NewsList = () => {
return (
<NewsListBlock>
<NewsItem article={sampleArtile} />
<NewsItem article={sampleArtile} />
<NewsItem article={sampleArtile} />
<NewsItem article={sampleArtile} />
<NewsItem article={sampleArtile} />
<NewsItem article={sampleArtile} />
</NewsListBlock>
);
};
export default NewsList;
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 sampleArtile = {
title: "제목",
description: "내용",
url: "https://google.com",
urlToImage: "https://via.placeholder.com/160",
};
& +& {
margin-top:3rem
}
이부분 처음 알게 되었는데, nav에서 서로간에 간격 띄울 때 좋다고 생각한다.
14.5 데이터 연동하기
import { useState, useEffect } from "react";
import styled from "styled-components";
import NewsItem from "./NewsItem";
import axios from "axios";
const NewsList = () => {
const [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = axios.get(
"https://newsapi.org/v2/top-headlines?country=kr&apiKey=6f1993113ca34824ab68673e9bdca9ee"
);
setArticles((await response).data.articles);
} catch (e) {
console.log(e);
}
setLoading(false);
};
fetchData();
}, []);
if (loading) {
return <NewsListBlock>대기중...</NewsListBlock>;
}
if (!articles) {
return null;
}
return (
<NewsListBlock>
{articles.map((article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
map 함수를 사용하기 전에 꼭 !articles를 조회하여 해당 값이 현재 null인지 아닌지 검사해야한다.
이 작업을 하지 않으면, 아직 데이터가 없을 때 null에는 map 함수가 없기 때문에 렌더링 과정에서 오류가 발생한다.
useEffect에 등록하는 함수에 async를 붙이면 안된다. useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문이다.
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = axios.get(
"https://newsapi.org/v2/top-headlines?country=kr&apiKey=6f1993113ca34824ab68673e9bdca9ee"
);
setArticles((await response).data.articles);
} catch (e) {
console.log(e);
}
setLoading(false);
};
fetchData();
}, []);
(useEffect는 함수 api 한번만 불러오려고 쓰나봐. 마지막쯤에 fetchData() 이런식으로 불러주는구나)
14.6.1 카테고리 선택 UI 만들기
//src/App.js
import { useState, useCallback } from "react";
import NewsList from "./components/NewsList";
import Categories from "./components/Categories";
function App() {
const [category, setCategory] = useState("all");
const onSelect = useCallback((category) => setCategory(category), []);
return (
<>
<Categories category={category} onSelect={onSelect} />
<NewsList category={category} />
</>
);
}
export default App;
useCallback은 메모리제이션된 함수를 반환하는 Hook이다.
//src/components/Categories.js
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 Categories = ({ onSelect, category }) => {
return (
<CategoriesBlock>
{categories.map((item) => (
<Category
key={item.name}
active={category === item.name}
onClick={() => onSelect(item.name)}
>
{item.text}
</Category>
))}
</CategoriesBlock>
);
};
export default 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.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;
}
`;
이 코드는 이해가 잘 안간다 ㅠㅠ
=> ${(props) 에는 Category 컴포넌트안에 있는 모든 props의 정보가 들어가있다.
active라는 props를 설정했다.
overflow-x:auto;
visible과 유사. 콘텐츠를 자르지 않고 넘칠 경우 스크롤 제공
white-space:pre;
공백처리. 연속 공백 유지. 줄바꿈은 개행 문자와 <br>요소에서만 일어남.
14.6.2 API 호출할 때 카테고리 지정하기
//components/NewsList.js
import NewsItem from "./NewsItem";
import axios from "axios";
const NewsList = ({ category }) => {
const [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const query = category === "all" ? "" : `&category=${category}`;
const response = axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=6f1993113ca34824ab68673e9bdca9ee`
);
setArticles((await response).data.articles);
} catch (e) {
...
category 값이 all이라면 query 값을 공백으로 설정하고, all이 아니라면 "&category=카테고리" 형태의 문자열을 만들도록 했다.
추가로 category 값이 바뀔 때 마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열에 category를 넣어주었다.
(아 query를 새로 정의하고, 삼항 연산자를 썻구나.)
최종 결과물
'📖 리액트 > 리액트를 다루는 기술' 카테고리의 다른 글
1장 리액트 시작 (리액트의 이해와 특징) (0) | 2022.04.02 |
---|---|
18장 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2022.02.16 |
13장 리액트 라우터로 SPA 개발하기 (0) | 2022.02.08 |
11장 컴포넌트 성능 최적화 (0) | 2022.02.07 |
10강 일정관리 웹 애플리케이션 만들기 (0) | 2022.02.04 |