작업 동기적 처리 : 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없음
작업 비동기적 처리 : 동시에 여러 가지 요청 처리 가능. 기다리는 과정에서 다른 함수도 호출 가능
→ 자바스크립트에서 비동기 작업을 할 때 콜백 함수를 사용함
ex) setTimeout - 특정 작업 예약
function printMe() {
console.log('Hello World');
}
setTimeout(printMe, 3000); //3초 뒤 printMe 호출
console.log('대기 중...');
콜백 지옥 : 콜백 안에 또 콜백을 넣어서 구현. 코드의 가독성↓
⇒ 콜백 지옥 형성되지 않게 하는 방안 : Promise
function increase(number) {
const promise = new Promise((resolve, reject) => {
// resolve는 성공, reject는 실패
setTimeout(() => {
const result = number + 10;
if(result>50){
const e = new Error('NumberTooBig');
return reject(e);
}
resolve(result);
}, 1000);
});
return promise;
}
increase(0)
.then(number => {
//Promise에서 resolve된 값은 .then을 통해 받아 올 수 있음
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.then(number => {
console.log(number);
return increase(number);
})
.catch(e=>{
console.log(e);
});
Promise를 더욱 쉽게 사용할 수 있도록 해 주는 ES8 문법
사용법
함수의 앞부분에 async 키워드
해당 함수 내부에서 Promise의 앞부분에 await 키워드 사용
⇒ Promise가 끝날 때까지 기다리고, 결과 값을 특정 변수에 담을 수 있음
async function runTasks() {
try {
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);
}
}
💡 화살표 함수에 async/awiat 적용 : async () ⇒ {} 형식으로 사용
axios : 자바스크립트 HTTP 클라이언트
HTTP 요청을 Promise 기반으로 처리
💡 가짜 API : https://jsonplaceholder.typicode.com/todos/1
https://newsapi.org/register → 가입해서 API 키 발급받기
뉴스 데이터 JSON 객체
▶NewsItem.js - 각 뉴스 정보를 보여주는 컴포넌트
import React from "react";
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="contents">
<h2>
<a href={url} target="_blank" rel="noopener noreferrer">
{title}
</a>
</h2>
<p>{description}</p>
</div>
</NewsItemBlock>
);
};
export default NewsItem;
▶NewsList.js - API를 요청하고 뉴스 데이터가 들어 있는 배열을 컴포넌트 배열로 변환하여 렌더링 해주는 컴포넌트
import React from 'react';
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} />
</NewsListBlock>
);
};
export default NewsList;
useEffect를 사용하여 컴포넌트가 화면에 보이는 시점에 API를 요청하려고 할 때,
❗ useEffect에 등록하는 함수에 async를 붙이면 안 됨
→ useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문에
⇒ 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용해 주어야 함
데이터 배열을 map 함수를 사용하여 컴포넌트 배열로 변환할 때,
❗ map함수를 사용하기 전에 꼭 !articles를 조회하여 해당 값이 현재 null인지 아닌지 검사해야 함
▶NewsList.js
import React, { 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(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await axios.get(
'https://newsapi.org/v2/top-headlines?country=kr&apiKey=발급받은 API 키',
);
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;
▶components/Categories.js
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(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;
}
`;
const Categories = () => {
return (
<CategoriesBlock>
{categories.map(c => (
<Category key={c.name}>{c.text}</Category>
))}
</CategoriesBlock>
);
};
export default Categories;
name : 실제 카테고리 값
text : 렌더링 할 때 사용할 한글 카테고리
▶App.js
import React, {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} />
</>
);
};
▶components/Categories.js
import React from 'react';
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;
▶components/NewsList.js
...
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 = await axios.get(
'https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=발급받은 API 키',
);
setArticles(response.data.articles);
} catch(e) {
console.log(e);
}
setLoading(false);
};
fetchData();
}, [category]);
...
카테고리 값을 리액트 라우터의 URL 파라미터를 사용해 관리해 보기
$ yarn add react-router-dom
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
// pages/NewsPage.js
import React from 'react';
import Categories from '../components/Categories';
import NewsList from '../components/NewsList';
const NewsPage = ({ match }) => {
const category = match.params.category || 'all';
return (
<>
<Categories />
<NewsList category={category} />
</>
);
};
export default NewsPage;
// App.js
import React from "react";
import { Route } from 'react-router-dom';
import NewsPage from './pages/NewsPage';
//API key : ec1b127d878f4e23b281eac02db4510e
const App = () => {
return <Route path="/:category?" component={NewsPage} />;
};
export default App;
path에 /:category? 형태 : category 값이 선택적이라는 의미.
파라미터 값이 없다면 전체 카테고리를 선택한 것으로 간주
//components/Categories.js
import React from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
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(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}
activeClassName="active"
exact={c.name === 'all'}
to={c.name === 'all' ? '/' : `/${c.name}`}
>{c.text}</Category>
))}
</CategoriesBlock>
);
};
export default Categories;
컴포넌트에서 API 호출처럼 Promise를 사용해야 하는 경우, 간결하게 코드 작성 위해 커스텀 Hook을 만들어 줌
▶lib/usePromise.js
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];
}
▶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=ec1b127d878f4e23b281eac02db4510e`,
);
}, [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;
▶Q1. 자바스크립트에서 비동기 작업을 할 때는 (__________)를 사용한다.
▶Q2. Promise를 더욱 쉽게 사용할 수 있도록 해주는 ES8문법으로, 함수의 앞부분에 () 키워드를 추가하고, 해당 함수 내부에서 Promise의 앞부분에 (_) 키워드를 사용한다.
▶Q3. useEffect에 등록하는 함수는 async로 작성하면 안 됩니다. 이를 해결하기 위해서는 어떻게 해야 할까요?
[리액트를 다루는 기술] 17장 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 (0) | 2022.01.17 |
---|---|
[리액트를 다루는 기술] 15장 Context API (0) | 2022.01.10 |
[리액트를 다루는 기술]13장 리액트 라우터로 SPA 개발하기 (0) | 2021.12.27 |
[리액트를 다루는 기술] 12장 immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2021.12.27 |
[리액트를 다루는 기술] 11장 컴포넌트 성능 최적화 (0) | 2021.12.27 |