14.1 비동기 작업의 이해
- 서버의 API를 사용해야 할 때는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 즉시 처리되는 것이 아니라, 응답을 받을 때까지 기다렸다가 전달받은 응답 데이터를 처리. 이 과정에서 해당 작업을 비동기적으로 처리
14.1.1 콜백 함수
ex) 파라미터 값이 주어지면 1초 뒤에 10을 더해서 반환하는 함수
function increase(number, callback) {
setTimeout(() => {
const result = number + 10;
if(callback) {
callback(result);
}
},1000)
}
increase(0, result => {
console.log(result);
});
- 1초에 걸쳐서 10,20,30,40과 같은 형태로 여러 번 순차적으로 처리하고 싶다면 콜백함수를 중첩해서 구현할 수 있음.
function increase(number, callback) {
setTimeout(() => {
const result = number + 10;
if(callback) {
callback(result);
}
},1000)
}
console.log('작업시작');
increase(0, result => {
console.log(result);
increase(result, result => {
console.log(result);
increase(result, result => {
console.log(result);
increase(result, result => {
console.log(result);
console.log('작업 완료');
});
});
});
});
- 여러번 중첩되니까 코드의 가독성이 나빠짐 -> '콜백 지옥'
14.1.2 Promise
- 콜백 지옥 같은 코드가 형성되지 않게 하는 방안으로 ES6에 도입된 기능
- 여러 작업을 연달아 처리한다고 해서 함수를 여러 번 감싸는 것이 아니라, .then을 사용하여 그다음 작업을 설정하기 때문에 콜백 지옥이 형성되지 않음
function increase(number) {
const promise = new Promise((resolve, reject) => {
//resolve는 성공, reject는 실패
setTimeout(() => {
const result = number +10;
if(result > 50) {
//50보다 높으면 에러 발생시키기
const e = new Error('NumberTooBig');
return reject(e);
}
resolve(result); //number 값에 +10 후 성공처리
},1000);
});
return promise;
}
increase(0)
.then(number => {
//promise에서 resolve된 값은 .then을 통해 받아 올 수 있음
console.log(number);
return increase(number); //promise를 리턴하면
})
.then(number => {
//또 .then으로 처리 가능
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.catch(e => {
//도중에 에러가 발생한다면 .catch를 통해 알 수 있음
console.log(e);
});
14.1.3 async / await
- Promise를 더욱 쉽게 사용할 수 있도록 해주는 ES2017(ES8) 문법
- 함수 앞부분에 async 키워드를 추가하고, 해당 함수 내부에서 Promise의 앞부분에 await 키워드를사용
function increase(number) {
const promise = new Promise((resolve, reject) => {
//resolve는 성공, reject는 실패
setTimeout(() => {
const result = number +10;
if(result > 50) {
//50보다 높으면 에러 발생시키기
const e = new Error('NumberTooBig');
return reject(e);
}
resolve(result); //number 값에 +10 후 성공처리
},1000);
});
return promise;
}
async function runTascks() {
try {
//try, catch 구문을 사용하여 에러를 처리
let result = await increase(0);
console.log(result);
result = await increase(result);
console.log(result);
result = await increase(result);
console.log(result);
result = await increase(result);
console.log(result);
result = await increase(result);
console.log(result);
result = await increase(result);
console.log(result);
}catch(e) {
console.log(e);
}
}
14.2 axios로 API 호출해서 데이터 받아오기
- axios는 자바스크립트 HTTP 클라이언트
- HTTP 요청을 Promise 기반으로 처리
App.js
- 불러오기 버튼을 누르면 'Jsonplaceholder.typicode.com/'에서 제공하는 가짜 API를 호출하고 이에 대한 응답을 컴포넌트 상태에 넣어서 보여줌
- onClick 함수에서는 axios.get 함수를 사용, 파라미터로 전달된 주소에 GET 요청을 해줌
- 결과는 .then을 통해 비동기적으로 확인할 수 있음
import { useState } from 'react';
import axios from 'axios';
const App = () => {
const [data, setData] = useState(null);
const onClick = () => {
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(response => {
setData(response.data);
});
};
return (
<div>
<div>
<button onClick={onClick}>불러오기</button>
</div>
{data && <textarea rows={7} value={JSON.stringify(data,null,2)} readOnly={true}/>}
</div>
);
};
export default App;
- async 적용
...
const App = () => {
const [data, setData] = useState(null);
const onClick = async() => {
try {
const response = await axios.get(
' https://jsonplaceholder.typicode.com/todos/1',
);
setData(response.data);
}catch (e) {
console.log(e);
}
};
...
14.3 newsapi API 키 발급받기
- newsapi에서 제공하는 API를 사용하여 최신 뉴스를 불러온 후 보여줌
https://newsapi.org/s/south-korea-news-api
- api 호출
14.4 뉴스 뷰어 UI 만들기
14.4.1 NewsItem 만들기
- 각 뉴스 정보를 보여주는 컴포넌트
- NewsItem 컴포넌트는 aritcle이라는 객체를 props로 통째로 받아 와서 사용
- title : 제목
- description : 내용
- url : 링크
- urlToImage : 뉴스 이미지
components/NewsItem.js
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;
}
`;
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="content">
<h2>
<a href={url} target="_blank" rel="noopener noreferrer">
{title}
</a>
</h2>
<p>{description}</p>
</div>
</NewsItemBlock>
);
};
export default NewsItem;
14.4.2 NewsList 만들기
- API를 요청하고 뉴스 데이터가 들어 있는 배열을 컴포넌트 배열로 변환하여 렌더링해주는 컴포넌트
- sampleArticle이라는 객체에 미리 예시 데이터를 넣은 후 각 컴포넌트에 전달하여 가짜 내용이 보이게 함
components/NewsList.js
import styled from 'styled-components';
import NewsItem from './NewsItem';
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 sampleArticle = {
title:'제목',
description: '내용',
url: 'https://google.com',
urlToImage: 'https://via.placeholder.com/160',
};
const NewsList = () => {
return (
<NewsListBlock>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
<NewsItem article={sampleArticle}/>
</NewsListBlock>
);
};
export default NewsList;
14.5 데이터 연동하기
- 컴포넌트가 화면에 보이는 시점에 API를 요청
- useEffect를 사용하여 컴포넌트가 처음 렌더링되는 시점에 API를 요청
- useEffect에 등록하는 함수에 async를 붙이면 안됨
- useEffect 내부에서 async/await를 사용하고 싶다면, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용해야 함
- loading이라는 상태를 관리하여 API 요청이 대기 중인지 판별
-> 요청이 대기중일 때는 loading 값이 true가 되고 ,요청이 끝나면 loading 값이 false가 되어야 함
components/NewsList.js
- 뉴스 데이터 배열을 map 함수를 사용하여 컴포넌트 배열로 변환
- map 함수를 사용하기 전에 !articles를 조회하여 해당 값이 현재 null이 아닌지 검사 해야함
- 위 작업을 하지 않으면 아직 데이터가 없을 때 null에는 map 함수가 없기 때문에 렌더링 과정에서 오류 발생
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 = () => {
const [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
//async를 사용하는 함수 따로 선언
const fetchData = async () => {
setLoading(true);
try {
const response = await axios.get (
'https://newsapi.org/v2/top-headlines?country=kr&apiKey=6c232a3858524e94a97156e2afeed3f5',
);
setArticles(response.data.articles);
} catch(e) {
console.log(e);
}
setLoading(false);
};
fetchData();
},[]);
//대기 중일 때
if(loading) {
return <NewsListBlock>대기중...</NewsListBlock>
}
//아직 articles 값이 설정되지 않았을 때
if(!articles) {
return null;
}
//articles 값이 유효할 때
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
14.6 카테고리 기능 구현하기
14.6.1 카테고리 선택 UI 만들기
Categories.js
- categories라는 배열 안에 name과 text 값이 들어가 있는 객체들을 넣어 주어서 한글로 된 카테고리와 실제 카테고리 값을 연결시켜 줌
- name은 실제 카테고리 값을 가리키고, text 값은 렌더링할 때 사용할 한글 카테고리를 가리킴
import React from 'react';
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;
App.js
- App에서 category 상태를 useState로 관리
- category 값을 업데이트하는 onSelect 함수 작성
- category와 onSelect 함수를 Categories 컴포넌트에게 props로 전달
- category 값을 NewsList 컴포넌트에게도 전달
import { useState, useCallback } from 'react';
import NewsList from "./components/NewsList";
import Categories from "./components/Categories";
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;
Categories.js
- props로 전달받은 onSelect를 각 Category 컴포넌트의 onClick으로 설정해 주고, 현재 선택된 카테고리 값에 따라 다른 스타일을 적용
import React from 'react';
import styled, { css } from 'styled-components';
...
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;
14.6.2 API를 호출할 때 카테고리 지정하기
- NewsList 컴포넌트에서 현재 props로 받아 온 category에 따라 카테고리를 지정하여 API를 요청
components/NewsList.js
- 현재 category 값이 무엇인지에 따라 요청할 주소가 동적으로 바뀌고 있음
- category값이 all이라면 query 값을 공백으로 설정하고, all이 아니라면 "&category=카테고리" 형태의 문자열을 생성
- 이 query를 요청할 때 주소에 포함
- category 값이 바뀔 때마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열에 category를 넣어줌
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(() => {
//async를 사용하는 함수 따로 선언
const fetchData = async () => {
setLoading(true);
try {
const query = category === 'all' ? '' : `&category=${category}`;
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=6c232a3858524e94a97156e2afeed3f5`,
);
setArticles(response.data.articles);
}catch(e) {
console.log(e);
};
setLoading(false);
};
fetchData();
},[category]);
// 대기중일 때
if (loading) {
return <NewsListBlock>대기중...</NewsListBlock>;
}
// 아직 articles 값이 유효할 때
if (!articles) {
return null;
}
// response 값이 유효할 때
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
14.7 리액트 라우터 적용하기
- 카테고리 값을 리액트 라우터의 URL 파라미터를 사용하여 관리
14.7.1 NewsPage 생성
pages/NewsPage.js
- 현재 선택된 category 값을 URL 파라미터를 통해 사용할 것이므로 Categoreis 컴포넌트에서 현재 선택된 카테고리 값을 알려 줄 필요도 없고, onSelect 함수를 따로 전달해 줄 필요도 없음
import { useParams } from "react-router-dom";
import Categories from "../components/Categories";
import NewsList from "../components/NewsList";
const NewsPage = () => {
const params = useParams();
//카테고리가 선택되지 않았으면 기본값 all로 사용
const category = params.category || 'all';
return (
<>
<Categories />
<NewsList category={category} />
</>
);
};
export default NewsPage;
App.js
- 경로에 category URL 파라미터가 없어도 NewsPage 컴포넌트를 보여줘야 하고, category가 있어도 NewsPage를 보여줘야 함
import React from 'react';
import { Route, Routes } 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;
14.7.2 Categoreis에서 NavLink 사용하기
- div, a, button, input처럼 일반 HTML 요소가 아닌 특정 컴포넌트에 styled-components를 사용할 떄는 styled(컴포넌트 이름)' '과 같은 형식을 사용
components/Categories.js
import React from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
...
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;
14.8 usePromise 커스텀 Hook 만들기
lib/usePromise.js
- 프로젝트의 다양한 곳에서 사용될 수 있는 유틸 함수들은 src 디렉토리에 lib 디렉토리를 만든 후 그 안에 작성
- usePromise 훅은 Promise의 대기 중, 완료 결과, 실패 결과에 대한 상태를 관리
- usePromise의 의존 배열 deps를 파라미터로 받아옴
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();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return [loading, resolved, error];
}
- NewsList 컴포넌트에서 usePromise 사용
components / NewsList.js
import React from 'react';
import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
import usePromise from '../lib/usePromise';
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 [loading, response, error] = usePromise(() => {
const query = category === 'all' ? '' : `&category=${category}`;
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=6c232a3858524e94a97156e2afeed3f5`,
);
}, [category]);
// 대기중일 때
if (loading) {
return <NewsListBlock>대기중...</NewsListBlock>;
}
// 아직 response 값이 설정되지 않았을 때
if (!response) {
return null;
}
// 에러가 발생했을 때
if (error) {
return <NewsListBlock>에러 발생!</NewsListBlock>;
}
// response 값이 유효할 때
const { articles } = response.data;
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 16장 리덕스 라이브러리 이해하기 (0) | 2023.11.22 |
---|---|
[리액트를 다루는 기술] 15장 Context API (2) | 2023.11.22 |
[리액트를 다루는 기술] 13장 리액트 라우터로 SPA 개발하기 (1) | 2023.11.17 |
[리액트를 다루는 기술] 12장 immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2023.11.16 |
[리액트를 다루는 기술] 11장 컴포넌트 성능 최적화 (0) | 2023.11.15 |