axios로 API 호출하기
axios는 브라우저와 Node.js에서 모두 사용할 수 있는 Promise 기반의 HTTP 클라이언트 라이브러리이다.
axios의 주요 특징은 다음과 같다.
- 간결하고 직관적인 API
- Promise 기반의 비동기 처리
- 브라우저 및 Node.js에서 모두 사용 가능
- 요청 및 응답 데이터의 자동 변환 (JSON, XML 등)
프로젝트 생성 및 axios 설정
npx create-react-app news-tutorial --template typescript
cd news-tutorial
npm install axios
// 타입스크립트는 추가로 설정
npm install @types/axios
// src/App.tsx
import axios from "axios";
import { useState } from "react";
export default function App() {
const [data, setData] = useState(null);
function 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>
);
}
위 코드는 불러오기 버튼을 누르면 JSONPlaceholder에서 제공하는 가짜 API를 호출하고, 이에 대한 응답을 컴포넌트 상태에 넣어서 보여주는 예제이다.
onClick 함수에서는 axios.get 함수를 사용했다. 이 함수는 파라미터로 전달된 주소에 GET요청을 해준다. 이에 대한 결과는 .then을 통해 비동기적으로 확인할 수 있다.
위 코드에 async를 적용해보자.
import axios from "axios";
import { useState } from "react";
export default function App() {
const [data, setData] = useState(null);
async function onClick() {
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>
);
}
onClick 함수를 async 키워드를 사용해 비동기 함수로 선언했다. 이 함수는 버튼 클릭 시 실행되며, axios를 사용해 JSONPlaceholder API의 'todos/1' 경로에서 데이터를 가져온다. 데이터를 가져오는 데 실패하면 catch 블록에서 에러를 처리하고, 성공하면 response.data를 setData 함수를 통해 data 상태값에 저장해주었다.
newsapi API 키 발급
newsapi에서 제공하는 API를 사용해 최신 뉴스를 불러온 후 보여줄 것이다. 이를 수행하기 위해서 사전에 newsapi에서 API키를 발급받아야 한다. API 키는 https://newsapi.org/register
에 가입하면 발급받을 수 있다.
발급받은 API 키는 추후 API를 요청할 때 API주소의 쿼리 파라미터로 넣어서 사용하면 된다.
우리가 사용할 API는 https://newsapi.org/s/south-korea-news-api
링크에 들어가면 한국 뉴스를 가져오는 API에 대한 설명서가 있다.
사용할 API 주소는 두 가지 형태이다.
- 전체 뉴스 불러오기
- 특정 카테고리 뉴스 불러오기
이 사이트에 api값을 기존 App 컴포넌트에 넣어보자.
전체 뉴스를 불러올 수도 있고, 카테고리를 선택하면 그에 맞는 뉴스만 가져올 수도 있다.
뉴스 뷰어 UI 만들기
styled-components를 사용해 뉴스 정보를 보여 줄 컴포넌트를 만들어보자. 우선 프로젝트에 styled-components를 추가하자.
npm install styled-components @types/styled-components
그리고 src 디렉터리 안에 components 디렉터리를 생성한 뒤, 그 안에 NewsItem.tsx와 NewsList.tsx를 생성한다.
- NewsItem : 각 뉴스 정보를 보여주는 컴포넌트
- NewsList : API를 요청하고 뉴스 데이터가 들어 있는 배열을 컴포넌트 배열로 변환해 렌더링해주는 컴포넌트
NewsList
NewsList 컴포넌트를 통해 API를 요청하게 될 것인데, 아직 데이터를 불러오지 않고 있으니, sampleArticle이라는 객체에 미리 예시 데이터를 넣은 후 각 컴포넌트에 전달해 가짜 내용이 보이도록 만들어보자.
// src/components/NewsList.tsx
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",
};
export default function NewsList() {
return (
<NewsListBlock>
<NewsItem article={sampleArticle} />
<NewsItem article={sampleArticle} />
<NewsItem article={sampleArticle} />
<NewsItem article={sampleArticle} />
<NewsItem article={sampleArticle} />
<NewsItem article={sampleArticle} />
</NewsListBlock>
);
}
이 코드는 styled-components를 사용해 스타일링을 해주었다. NewsListBlock은 styled-components로, 뉴스 목록 전체를 감싸는 div이다. box-sizing, padding, width, margin 등의 스타일 속성이 정의되어 있다. 미디어 쿼리를 사용해 화면 크기가 768px 이하힐 때는 전체 화면 너비를 차지하도록 변경된다.
sampleArticle은 뉴스 목록에 보여지는 각각의 뉴스 항목의 데이터를 담고 있는 객체이다. title, description, url, urlToImage 등의 프로퍼티를 가지고 있다.
NewsList 컴포넌트는 NewsListBlock을 렌더링하고, 그 안에 NewsItem 컴포넌트를 여러 개 렌더링해준다. article 프로퍼티를 통해 각각의 뉴스 항목 데이터를 전달하고, NewsItem 컴포넌트에서는 해당 데이터를 화면에 표시하고 있다.
그리고 샘플 뉴스 항목을 여러 개 렌더링하고 있으며, 더 많은 뉴스 항목이 있다면 동적으로 데이터를 받아와 렌더링하도록 수정할 수 있다.
// src/App.tsx
import NewsList from "./components/NewsList";
export default function App() {
return <NewsList />;
}
NewsItem
이전에 API를 통해 받아온 뉴스 데이터를 보자면,
{
"source": {
"id": "google-news",
"name": "Google News"
},
"author": "MBN News",
"title": "일본 영화 돌풍, 한일 젊은층 호감도는? [shorts] - MBN News",
"description": null,
"url": "https://news.google.com/rss/articles/CCAiC3Q2N2RRRW4wMzkwmAEB?oc=5",
"urlToImage": null,
"publishedAt": "2023-03-19T02:00:19Z",
"content": null
}
이렇게 되어있는데, 그 중에서 필요한 필드들만 추려보자면
- title : 제목
- description : 설명
- url : 링크
- urlToImage : 뉴스 이미지
정도를 사용할 예정이다. NewsItem 컴포넌트는 article이라는 객체를 props 통째로 받아와서 사용하게 된다. NewsItem을 작성해보자.
import styled from "styled-components";
interface Props {
article: {
title: string;
description: string;
url: string;
urlToImage: string;
};
}
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;
}
`;
export default function NewsItem({ article }: Props) {
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>
);
}
컴포넌트의 Props 인터페이스는 Props라는 이름으로 선언되어 있으며, article 프로퍼티가 필수적으로 전달되어야 한다. article 프로퍼티는 객체 형태로, title, description, url, urlToImage 프로퍼티가 필수적으로 존재해야 한다.
컴포넌트의 본문에서는 article 객체로부터 title, description, url, urlToImage 프로퍼티를 추출하여 각각의 정보를 화면에 렌더링하고, urlToImage가 존재하는 경우에는 thumbnail 클래스의 이미지 태그를 렌더링하며, url에 대한 링크는 a 요소를 이용하여 설정한다.
데이터 연동
이제는 이전 뉴스 API를 이용해 API를 호출해보자. useEffect를 사용해 컴포넌트가 처음 렌더링되는 시점에 API를 요청하면 된다. 주의할 점은 useEffect에 등록하는 함수에 async를 붙이면 안된다는 것이다. useEffect에서 반환해야하는 값은 뒷정리 함수이기 때문이다.
따라서 useEffect 내부에서 async/await을 사용하고 싶다면, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어 사용해야 한다.
추가로 loading이라는 상태를 관리해 API 요청이 대기 중인지 판별할 것이다. 요청이 대기 중일 때는 loading 값이 true 가 되고, 요청이 끝나면 loading 값이 false가 되어야 한다.
// src/components/NewsList.tsx
import axios from "axios";
import { useEffect, useState } from "react";
import styled from "styled-components";
import NewsItem from "./NewsItem";
interface Article {
title: string;
description: string;
url: string;
urlToImage: string;
}
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;
}
`;
export default function NewsList() {
const [articles, setArticles] = useState<Article[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchData(): Promise<void> {
setLoading(true);
try {
const response = await axios.get(
"https://newsapi.org/v2/top-headlines?country=kr&apiKey=181cd9b8483e41039a15395e41f3b50b"
);
setArticles(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: Article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
NewsList는 axios를 사용하여 뉴스 API에서 최신 뉴스를 가져와서 표시하는 목록을 렌더링한다.
컴포넌트의 상태에는 useState 훅을 사용하여 뉴스 목록과 로딩 상태가 저장된다. useEffect 훅은 컴포넌트가 마운트될 때 API를 호출하고 뉴스 목록을 가져오도록 구현했다.
Article 인터페이스는 타입스크립트 인터페이스로써, 뉴스 기사의 속성을 정의합니다.
fetchData 함수는 async/await 구문을 사용하여 axios.get() 메서드를 호출하고 최신 뉴스 데이터를 가져오며, 가져온 뉴스 목록은 setArticles를 사용하여 컴포넌트의 상태에 저장된다.
loading 상태가 true인 경우, 대기 중... 텍스트가 표시된다. articles가 null인 경우, null을 반환하고, 이를 통해 컴포넌트가 렌더링되지 않도록 한다.
마지막으로, NewsListBlock 컴포넌트에는 map() 메서드를 사용하여 뉴스 목록을 반복하고, NewsItem 컴포넌트를 사용하여 각 뉴스 항목을 렌더링한. 각 뉴스 항목의 고유 식별자로 key 속성이 사용된다.
불러온 결과이다. 현재 기사들의 urlToImage와 description이 다 null로 되어있어 기사제목만 나오고 있음을 확인했다.
카테고리 기능 구현
뉴스의 카테고리 선택 기능을 구현해보자. 뉴스 카테고리는 총 여싯개이며, 다음과 같이 영어로 되어있다.
- business
- entertainment
- health
- science
- sports
- technology
이 종류들을 화면 상에서는 영어 대신 한글로 보여준 뒤, 클릭했을 때는 영어로 된 카테고리 값을 사용하도록 구현해보자.
카테고리 선택 UI 만들기
// src/components/Categories.tsx
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;
}
`;
export default function Categories() {
return (
<CategoriesBlock>
{categories.map((c) => (
<Category key={c.name}>{c.text}</Category>
))}
</CategoriesBlock>
);
}
먼저, 카테고리 목록은 categories라는 배열에 객체들의 배열로 정의되어 있다. 각 객체는 name과 text 두 개의 프로퍼티를 가지고 있고, name은 카테고리 이름, text는 카테고리가 표시될 텍스트이다.
그 다음, 이 배열을 기반으로 CategoriesBlock과 Category라는 스타일드 컴포넌트를 만들어 각각 부모와 자식 역할을 하게 된다. CategoriesBlock은 flexbox를 사용하여 카테고리를 가로로 정렬하고 있다. 카테고리 목록이 너무 길어질 경우를 대비해, 스크롤이 가능하도록 media query를 사용하여 가로 스크롤을 추가했다.
마지막으로, categories 배열을 map 함수를 사용하여 각 카테고리에 대해 Category 컴포넌트를 만들어 준다. key로는 카테고리 이름을 사용하며, 카테고리 텍스트를 표시한다. 마우스를 올리면 색상이 변경되도록 hover 스타일도 추가해 주었다.
이렇게 구현된 Categories 컴포넌트는 웹 페이지의 상단에 카테고리 목록을 보여주는 역할을 한다.
이것을 App 컴포넌트에 적용해 렌더링된 결과를 보자.
// src/App.tsx
import Categories from "./components/Categories";
import NewsList from "./components/NewsList";
export default function App() {
return (
<>
<Categories />
<NewsList />
</>
);
}
이제 App에서 category 상태를 useState로 관리하자. 추가로 category 값을 업데이트하는 onSelect라는 함수도 만들어준다. 그리고나서 category와 onSelect 함수를 Categories 컴포넌트에게 props로 전달해주자. 또한, category 값을 NewsList 컴포넌트에게도 전달해주어야 한다.
// src/App.tsx
import { useCallback, useState } from "react";
import Categories from "./components/Categories";
import NewsList from "./components/NewsList";
export default function App() {
const [category, setCategory] = useState<string>("all");
const onSelect = useCallback((category: string) => setCategory(category), []);
return (
<>
<Categories category={category} onSelect={onSelect} />
<NewsList category={category} />
</>
);
}
// src/components/Category.tsx
import styled, { css } from "styled-components";
interface CategoryProps {
active: boolean;
}
interface CategoriesProps {
onSelect: (category: string) => void;
category: string;
}
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<CategoryProps>`
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;
}
`;
export default function Categories({ onSelect, category }: CategoriesProps) {
return (
<CategoriesBlock>
{categories.map((c) => (
<Category
key={c.name}
active={category === c.name}
onClick={() => onSelect(c.name)}
>
{c.text}
</Category>
))}
</CategoriesBlock>
);
}
Categories 컴포넌트는 CategoriesProps 인터페이스를 통해 onSelect과 category props를 받는다. onSelect은 선택된 카테고리를 App 컴포넌트의 상태에 업데이트하는 함수이고, category는 현재 선택된 카테고리를 나타낸다.
categories 배열은 카테고리 이름과 텍스트를 포함하고 있다. Categories 컴포넌트는 map 함수를 사용하여 각 카테고리를 렌더링하고, active props를 통해 현재 선택된 카테고리를 표시한다. 클릭 이벤트는 onClick 함수를 호출하여 선택된 카테고리를 App 컴포넌트에 전달한다.
렌더링해보면, 선택된 카테고리가 청록색으로 보이고, 다른 카테고리도 잘 클릭되는것을 확인할 수 있었다.
API 호출 시 카테고리 지정하기
지금은 뉴스 API를 요청할 때 따로 카테고리를 선택하지 않고 뉴스 목록을 불러오고 있다. NewsList 컴포넌트에서 현재 props로 받아 온 category에 따라 카테고리를 지정해 API를 요청하도록 구현해보자.
// src/components/NewsList.tsx
// (...)
interface Props {
category: string;
}
export default function NewsList({ category }: Props) {
const [articles, setArticles] = useState<Article[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchData(): Promise<void> {
setLoading(true);
try {
const query = category === "all" ? "" : `&category=${category}`;
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=181cd9b8483e41039a15395e41f3b50b`
);
setArticles(response.data.articles);
} catch (e) {
console.log(e);
}
setLoading(false);
}
fetchData();
}, [category]);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>;
}
if (!articles) {
return null;
}
return (
<NewsListBlock>
{articles.map((article: Article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
Props 인터페이스를 통해 NewsList 컴포넌트에 전달되는 props의 타입을 정의해주었다. 또한, useEffect 훅을 사용해 컴포넌트가 마운트되거나 category가 변경될 때마다 API를 호출해 해당 카테고리의 뉴스를 가져오고, 가져온 뉴스 데이터를 articles 상태에 저장한다. API 호출 중인동안 loading 상태를 true로 변경해 로딩 중임을 나타낸다.
추가로 category 값이 바뀔 때마다 뉴스를 새로 불러와야 하기 때문에 useEffect 의 의존 배열에 category를 넣어 주어야 한다.
결과를 렌더링해보면 선택한 카테고리 별로 그에 맞는 기사들이 분류되고 있음을 확인할 수 있다.
리액트 라우터 적용
기존에는 useState로 카테고리 값을 관리했는데, 리액트 라우터를 적용해보자.
npm install react-router-dom @types/react-router-dom
라이브러리를 추가했다면 index.tsx에 라우터를 적용해보자.
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
reportWebVitals();
NewsPage 생성
이 프로젝트에 리액트 라우터를 적용할 때 만들어야 할 페이지는 단 하나이다. NewsPage.tsx 파일을 만들어 다음과 같이 작성한다.
// src/pages/NewsPage.tsx
import { useParams } from "react-router-dom";
import Categories from "../components/Categories";
import NewsList from "../components/NewsList";
export default function NewsPage() {
const params = useParams();
const category = params.category || "all";
return (
<>
<Categories />
<NewsList category={category} />
</>
);
}
현재 선택된 category 값을 URL 파라미터를 통해 사용할 것이므로 Categories 컴포넌트에서 현재 선택된 카테고리 값을 알려줄 필요도 없고, onSelect 함수를 따로 전달해줄 필요도 없다.
import { Route, Routes } from "react-router-dom";
import NewsPage from "./pages/NewsPage";
export default function App() {
return (
<Routes>
<Route path="/" element={<NewsPage />} />
<Route path="/:category" element={<NewsPage />} />
</Routes>
);
}
경로에 category URL 파라미터가 없어도 NewsPage 컴포넌트를 보여줘야 하고, category가 있어도 NewsPage를 보여줘야하기 때문에 Route 컴포넌트를 두 번 사용했다.
Categories 컴포넌트에서 NavLink 사용
기존의 onSelect 함수를 호출해 카테고리를 선택하는 기능을 NavLink로 대체해보자. div, a, button 과 같은 일반 HTML 요소가 아닌 특정 컴포넌트에 styled-components를 사용할 때는 styled(컴포넌트이름)``와 같은 형식을 사용한다.
// src/components/Categories.tsx
import { NavLink } from "react-router-dom";
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;
}
&.active {
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
}
& + & {
margin-left: 1rem;
}
`;
export default function 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>
);
}
Category는 NavLink 컴포넌트를 styled-components를 사용하여 스타일링했다. NavLink는 현재 페이지의 경로와 매칭되는 경우에 active 클래스를 추가할 수 있다.
className={({ isActive }) => (isActive ? "active" : undefined)}
는 NavLink의 className 속성을 지정하는 부분이다. isActive는 NavLink가 활성화되어 있는지 여부를 나타내는 불리언 값이다. isActive가 true이면 active 클래스를 추가하고, 그렇지 않으면 undefined를 반환한다.
to={c.name === "all" ? "/" : /${c.name}}
는 NavLink가 이동할 경로를 지정하는 부분이다. name이 "all"인 경우에는 루트 경로로 이동하고, 그 외의 경우에는 name을 경로로 사용한다.
Categories 컴포넌트는 categories 배열을 map 함수를 사용하여 순회하면서 Category 컴포넌트를 렌더링한다. key 속성은 각 카테고리를 구분하기 위한 유일한 값을 지정하고, to 속성은 각 카테고리의 경로를 지정한다. className 속성은 현재 활성화된 카테고리에 active 클래스를 추가한다. c.text는 카테고리에 대한 텍스트를 표시한다.
결과를 렌더링해보면, 이전 결과와 동일하게 작동하는 페이지를 확인할 수 있다.
usePromise 커스텀 Hook 만들기
컴포넌트에서 API 호출처럼 Promise를 사용해야 하는 경우 더욱 간결하게 코드를 작성할 수 있도록 해 주는 커스텀 Hook을 만들어 프로젝트에 적용해보자.
이번에 만들 Hook은 usePromise이다.
// src/lib/usePromise.tsx
import { useEffect, useState } from "react";
type PromiseCreator = () => Promise<any>;
export default function usePromise(
promiseCreateor: PromiseCreator,
deps: any[]
) {
const [loading, setLoading] = useState(false);
const [resolved, setResolved] = useState<any>(null);
const [error, setError] = useState<any>(null);
useEffect(() => {
async function process() {
setLoading(true);
try {
const resolved = await promiseCreateor();
setResolved(resolved);
} catch (e) {
setError(e);
}
setLoading(false);
}
process();
}, deps);
return [loading, resolved, error];
}
위 코드는 promiseCreator와 deps라는 두 개의 매개변수를 받는다. promiseCreator는 Promise를 생성하는 함수이며, deps는 useEffect 훅에서 의존성 배열로 사용된다.
usePromise 훅에서는 loading, resolved, error 세 가지 상태를 useState Hook을 통해 관리한다. loading은 비동기 작업이 실행 중인지 여부를 나타내며, resolved와 error는 각각 비동기 작업이성공한 경우와 실패한 경우의 결과값을 저장한다.
이 Hook은 useEffect Hook을 이용해 비동기 작업을 수행한다. deps 배열이 변경될 때마다 useEffect Hook이 실행되며, 이 때, process 함수를 실행한다. process 함수 내부에서는 비동기 작업이 실행 중임을 나타내는 loading 값을 true로 설정한 후, promiseCreator 함수를 실행한다. 이때 promise 객체가 resolved되면 resolved 값을 setResolved 함수를 통해 업데이트하고, rejected 되면 error 값을 setError 함수를 통해 업데이트한다. 비동기 작업이 종료된 후에는 loading 값을 false로 업데이트한다.
마지막으로 loading, resolved, error 값을 배열 형태로 반환한다. 이를 이용해 해당 비동기 작업의 상태값을 반환하거나, 비동기 작업의 결과값을 이용한 로직을 처리할 수 있다.
usePromise를 NewsList 컴포넌트에서 사용할 수 있다.
// src/components/NewsList.tsx
import axios from "axios";
import styled from "styled-components";
import usePromise from "../lib/usePromise";
import NewsItem from "./NewsItem";
interface Article {
title: string;
description: string;
url: string;
urlToImage: string;
}
interface Props {
category: string;
}
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;
}
`;
export default function NewsList({ category }: Props) {
const [loading, response, error] = usePromise(() => {
const query = category === "all" ? "" : `&category=${category}`;
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=181cd9b8483e41039a15395e41f3b50b`
);
}, [category]);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>;
}
if (!response) {
return null;
}
if (error) {
return <NewsListBlock>Error!</NewsListBlock>;
}
const { articles } = response.data;
return (
<NewsListBlock>
{articles.map((article: Article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
usePromise를 사용하면 NewsList에서 대기 중 상태 관리와 useEffect 설정을 직접 하지 않아도 되므로 코드가 훨씬 간결해진다. 요청 상태를 관리할 때 무조건 커스텀 Hook을 만들어서 사용해야하는 것은 아니지만, 상황에 따라 적절히 사용하면 좋은 코드를 만들어 갈 수 있다.
정리
외부 API를 연동하고 사용하는 방법을 알아보았는데, 절대 잊지 말아야할 유의 사항은 useEffect에 등록하는 함수는 async로 작성하면 안된다는 점이다. 그 대신 함수 내부에 async 함수를 따로 만들어주어야 한다.
마지막 부분에서 usePromise라는 커스텀 Hook을 만들어 사용함으로써 코드가 조금 더 간결해지기는 했지만, API의 종류가 많아지면 요청을 위한 상태 관리를 하는 것이 번거로워질 수 있다. 이 경우 리덕스를 사용하면 조금 더 쉽게 요청에 대한 상태를 관리할 수 있다.
'WEB > React' 카테고리의 다른 글
리액트 리덕스 상태관리 - 1 [react/redux/typescript] (0) | 2023.03.22 |
---|---|
리액트 Context API [react/typescript] (0) | 2023.03.21 |
리액트 라우터 써보기 [react/router/typescript] (0) | 2023.03.19 |
리액트 불변성 유지하기 [react/immer/typescript] (0) | 2023.03.18 |
리액트 컴포넌트 성능 최적화 [react/component/typescript] (0) | 2023.03.18 |