1. 비동기 작업의 이해
- 동시에 여러 가지 요청을 처리할 수 있고, 기다리는 과정에서 다른 함수도 호출할 수 있다.
- 서버API를 호출할 때 외에도 작업을 비동기적으로 처리할 때가 있는데, 바로 setTimeout함수를 사용하여 특정 작업을 예약할 때 이다.
콜백함수
- 자바스크립트에서 비동기 작업을 할 때 가장 흔히 사용하는 방법
- 콜백 안에 콜백을 넣어 구현할 수 있는데, 너무 여러 번 중첩 되면 '콜백 지옥' 이라 한다.
Promise
- 콜백 지옥 같은 코드가 형성되지 않게 하는 방안으로 ES6에 도입된 기능
- 여러 작업을 연달아 처리한다고 해서 여러 번 감싸는 것이 아니라, then을 사용하여 그다음 작업을 설정하기 때문에 콜백 지옥이 형성되지 않는다.
async/await
- Promise를 더욱 쉽게 사용할 수 있도록 해주는 ES2017(ES8) 문법
- 함수 앞 부분에 async 키워드를 추가하고, 해당 함수 내부에서 Promise 앞부분에 await 키워드를 사용해야 한다.
2. axios로 API호출해서 데이터 받아오기
[.prettierrc]
- Prittier로 코드 스타일을 자동으로 정리하고 싶다면, 프로젝트 최상위 디렉터리에 파일 생성 후 코드 입력
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
[jsconfig.json]
- VS Code에서 파일 자동 불러오기 기능을 잘 활용하고 싶다면 최상위 디렉터리에 파일 생성 후 코드 입력
{
"compilerOptions": {
"target": "es6"
}
}
[App.js]
import React, { 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
- onClick함수에서 axios.get함수 사용
- 파라미터로 전달된 주소에 GET요청
- 이에 대한 결과는 .then을 통해 비동기적으로 확인
- 위 코드에 async를 적용하면??
[App.js]
import React, { useState } from "react";
import axios from 'axios';
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)
}
};
return (
<div>
<div>
<button onClick={onClick}>불러오기</button>
</div>
{data && <textarea rows={7} value={JSON.stringify(data, null, 2)} readOnly={true} />}
</div>
);
};
export default App
- 화살표 함수에 qsync/await를 적용할 때는 async () => {} 와 같은 형식으로 적용한다.
[결과]
3. newsapi API 키 발급받기
- newsapi.org/register 에 가입한 뒤 key를 받는다.
- 발급 받은 API key는 추후 API를 요청할 때 API주소의 파라미터로 넣어 사용하면 된다.
- newsapi.org/s/south-korea-news-api 링크에 들어가면 한국 뉴스를 가져오는 API에 대한 설명서가 있다.
- 1. 전체 뉴스 불러오기
- 2. 특정 카테고리 뉴스 불러오기
4. 뉴스 뷰어 UI 만들기
[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.5 rem;
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]
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;
[App.js]
import React from "react";
import NewsList from './components/NewsList'
const App = () => {
return <NewsList />
};
export default App
[결과]
5. 데이터 연동하기
- 컴포넌트가 화면에 보이는 기점에 API를 요청해 볼 것이다.
- useEffect를 사용하여 컴포넌트가 처음 렌더링되는 시점에 API를 요청
- useEffect에 등록하는 함수에 async를 붙이면 안된다.
- useEffect에서 반환해야 하는 값을 뒷정리 함수이기 때문
- useEffect내부에서 async/await를 사용하고싶다면, 함수 내부에 async키워드가 붙은 또다른 함수를 만들어서 사용해야함
- loading이라는 상태도 관리하여 API요청이 대기중인지 판별할 것이다.
[NewsList.js]
import React, { useEffect, useState } 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=fc444b3a393c49eb99efd6f63d669444',
);
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;
- map함수를 사용하기 전에 !articles를 조회하여 해당 값이 null이 아닌지 검사해야한다.
- 이 작업을 하지 않으면, 데이터가 없을 때 null에는 map함수가 없기 때문에 렌더링 과정에서 오류가 발생
[결과]
6. 카테고리 기능 구현
- components디렉터리에 Categories.js 컴포넌트 파일 생성
[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-w: 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;
[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}/>
</>
);
};
export default App
[NewsList.js]
import React, { useEffect, useState } 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=fc444b3a393c49eb99efd6f63d669444`,
);
setArticles(response.data.articles);
} catch(e) {
console.log(e)
}
setLoading(false);
};
fetchData();
}, [category]);
// 대기 중일 때
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;
- category 값이 all이라면 query값을 공백으로 설정하고, all이 아니라면 "&category=카테고리" 형태의 문자열을 만들도록 했다.
- query를 요청할 때 주소에 포함시켜 줬다.
- category값이 바뀔 때마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열(두번째 파라미터로 설정하는 배열)에 category를 넣어줘야 한다.
- 만약 클래스형 컴포넌트로 만들게 된다면 componentDidMount와 componentDidUpdate에서 요청을 시작하도록 설정해줘야한다.
- 함수형 컴포넌트라면 이렇게 useEffect 한 번으로 컴포넌트가 맨 처음 렌더링 될 때
- 그리고 category값이 바뀔 때 요청하도록 설정해 줄 수 있다.
[결과]
7. 리액트 라우터 적용하기
- 기존에 카테고리 값을 useState로 관리했지만, 이번에 라우터의 URL 파라미터를 사용하여 관리해 보겠다.
- 리액트 라우터 설치 ( $ yarn add react-router-dom)
- index.js에서 리액트 라우터를 적용
[index.js]
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
- src디렉터리에 pages라는 디렉터리를 생성하고, NewsPage.js 파일을 생성
[NewsPage.js]
import React from 'react';
import Categories from '../components/Categories';
import NewsList from '../components/NewsList';
const NewsPage = ({match}) => {
// 카테고리가 선택되지 않았으면 기본값 all로 사용
const category = match.params.category || 'all';
return (
<>
<Categories />
<NewsList category={category} />
</>
);
};
export default NewsPage;
- 현재 성택된 category값을 URL 파라미터를 통해 사용할 것이므로 Categories 컴포넌트에서 현재 선택된 카테고리 값을 알려 줄 필요도 없고, onSelect 함수를 따로 전달해 줄 필요도 없다.
[App.js]
import React from "react";
import {Route} from 'react-router-dom';
import NewsPage from './pages/NewsPage';
const App = () => {
return <Route path='/:category?' component={NewsPage} />;
};
export default App
- path에 /:category?와 같은 형태로 맨 뒤에 물음표 문자가 들어가 있는데, 이는 category값이 선택적이라는 의미
- category URL 파라미터가 없다면 전체 카테고리를 선택하나 것으로 간주
이제 Categories에서 기존의 onSelect 함수를 호출하여 카테고리를 선택하고, 선택된 카테고리에 다른 스타일을 주는 기능을 NavLink로 대체해 보겠다.
[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-w: auto;
}
`;
const Category = styled.div(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 = ({ onSelect, category }) => {
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;
- to값이 '/' 를 가리키고 있을 때는 exact값을 true로 해 주어야 합니다.
- 이 값을 설정하지 않으면, 다른 카테고리가 선택되었을 때도 전체보기 링크에 active 스타일이 적용되는 오류가 발생한다.
[결과]
8. usePromise 커스텀 Hook 만들기
- 컴포넌트에서 API 호출처럼 Promise를 사용해야 하는 경우 더욱 간결하게 코드를 작성할 수 있도록 해주는 커스텀 Hook을 만들어 적용해보자
- 프로젝트의 다양한 곳에서 사용될 수 있는 유틸 함수들은 보통 src 디렉터리에 lib 디렉터리를 만든 후 그 안에 작성한다.
[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/exhautive-deps
}, deps);
return [loading, resolved, error]
}
- 방금 만든 usePromise Hook은 Promise의 대기중, 완료 결과, 실패 결과에 대한 상태를 관리하며, usePromise의 의존배열 deps를 파라미터로 받아온다.
- 파라미터로 받아 온 deps배열은 usePromise내부에서 사용한 useEffect의 의존 배열로 설정되는데 이 배열을 설정하는 부분에서 ESLint경고가 나타나게 된다.
- 이 경고를 무시하려면 특정 줄에서만 ESLint 규칙을 무시하도록 주석을 작성해 주어야 한다.
- 코드를 저장한 뒤 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=fc444b3a393c49eb99efd6f63d669444`,
);
}, [category]);
// 대기 중일 때
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>
}
// 아직 .response 값이 설정되지 않았을 때
if(!response) {
// alert('에러발생')
return null;
}
// 에러가 발생했을 때
if (error) {
return <NewsList>에러 발생!</NewsList>
}
// response 값이 유효할 때
const {articles} = response.data;
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
export default NewsList;
- usePromise를 사용하면 NewsList에 대기 중 상태 관리와 useEffect 설정을 직접 하지 않아도 되므로 코드가 훨씬 간결해진다.
- 요청 상태를 관리할 때 무조건 커스텀 Hook을 만들어 사용해야 하는 것은 아니지만, 상황에 따라 적절히 사용하면 좋은 코드를 만들어 갈 수 있다.
[결과]
'Front > React' 카테고리의 다른 글
[React] 리덕스 라이브러리 (0) | 2021.01.14 |
---|---|
[React] Context API (0) | 2021.01.14 |
[React] 리액트 라우터 부가 기능 (0) | 2021.01.09 |
[React] 서브 라우트 (0) | 2021.01.09 |
[React] URL 파라미터와 쿼리 (0) | 2021.01.09 |