지금까지 배운 것들을 기반으로 뉴스 API를 받아오는 뉴스 뷰어를 만들어 보고자 한다. 여기서 먼저 알아야 할 것이 비동기 작업이다.
웹 애플리케이션을 만들면, 서버 쪽의 데이터가 필요할 때에는 Ajax 기법을 사용해서 API를 호출해서 데이터를 수신한다. 이렇듯 서버의 API를 사용해야 할 때는 네트워크 송수진 과정에서 시간이 걸리기 때문에, 작업을 즉시 처리할 수 없다.
즉 데이터 수신이 완료된 후에 그 데이터를 처리하는데, 이것을 비동기적으로 작업을 처리한다고 한다.
만약 작업을 동기적으로 처리하면, 데이터 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 처리할 수 없다. 그리고 요청이 끝나면 다음 작업을 진행할 수 있다. 만약 이를 비동기적으로 처리한다면 웹 애플리케이션이 멈추지 않기 때문에 동시에 여러가지 요청을 처리할 수 있다.
자바스크립트에서 비동기 작업을 할 때 가장 흔히 사용하는 방법이 바로 콜백 함수
를 이용하는 것이다.
function printMe() {
console.log("Hello, world!");
}
setTimeout(printMe, 3000);
console.log("대기 중");
3초 뒤에 printMe
함수를 실행하게끔 setTimeout
을 해 주었는데 여기서 setTimeout
의 인자로 전달해준 printMe
함수를 콜백 함수라고 한다. 해당 함수가 처리된 직후 어떤 작업을 하고 싶다면 콜백 함수를 활용한다.
function increase(number, callback) {
setTimeout(() => {
const result = number + 10;
if (callback) {
callback(result);
}
}, 1000);
}
increase(0, (result) => {
console.log(result);
});
또다른 예시는 위와 같다. 1초 뒤에 10을 출력하게 된다.
콜백 안에 또 콜백을 넣어서 구현할 수 있는데, 잘 작동은 하지만 겹겹이 싸여있기 때문에 코드의 가독성이 떨어져, 가급적 지양하는 방식이다.
너무 여러 겹으로 콜백 함수가 형성되지 않게 하기 위한 방안이다.
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);
}, 1000);
});
return promise;
}
increase(0)
.then((number) => {
// promise에서 resolve된 값은 .then으로 받아올 수 있음
console.log(number);
return increase(number);
})
.catch((e) => {
// 도중에 에러가 발생하면 .catch로 알 수 있음
console.log(e);
});
위와 같이 .then
을 사용하여 그 다음 작업을 설정할 수 있어 가독성이 좋다는 장점이 있다.
그리고 처음 increase
함수에서 return된 promise
를 그 이후 .then
에서 인자로 받아와 사용할 수 있다.
Promise를 더 쉽게 사용할 수 있게 해준다. 함수의 앞부분에 async
키워드를 추가하고, 함수 내부에서 promise의 앞부분에 await
키워드를 사용한다. 이렇게 하면 promise가 끝날 때까지 기다리고, 결과값을 변수에 담을 수 있다.
async function f() {
return 1;
}
async
가 붙은 함수는 항상 promise를 반환한다. promise가 아닌 값을 반환하더라도 resolved promise가 반환된다.
즉, 아래 예시 함수를 호출하면 result
가 1인 resolved promise가 반환된다.
async function f() {
return 1;
}
f().then(alert); // 1
물론 아래와 같이 명시적으로 promise를 반환할 수도 있다.
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
await은 async
함수 안에서만 동작한다.
let value = await promise;
await
키워드를 만나면 promise가 처리될 때까지 기다린다.
다음의 예시를 보자.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
alert(result); // "완료!"
}
f();
f
함수를 호출하였고 result = await promise;
로 promise 값을 result에 넣고자 하였는데, promise가 setTimeout
에 의해 1초 뒤에 반환된다. 따라서 별표 친 부분에서 실행이 중단되었다가 promise가 처리되면 다시 재개되고 alert로 완료 메시지가 나오게 된다.
프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
이후에 뉴스API 사이트에서 axios
를 이용해 데이터를 받아오는데 이전에 fetch API
를 사용해 본 경험이 있어서 axios 사용법을 배우지 않고 그냥 fetch
를 사용하기로 하였다.
async function getData() {
const response = await fetch('https://newsapi.org/v2/top-headlines?country=kr&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b');
const json = await response.json();
const news = json.articles;
console.log(news);
setData(news);
}
fetch와 async
를 이용해 뉴스를 불러오는 데에 성공했다.
이제 styled-component를 이용해서 UI를 깔끔하게 꾸며 볼 것이다.
전체 뉴스를 NewsList
컴포넌트로 배열처럼 만들고, 각각의 뉴스는 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} />
</NewsListBlock>
);
};
export default NewsList;
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 }) => {
return (
<NewsItemBlock>
{article.urlToImage && (
<div className="thumbnail">
<a href={article.url} target="_blank" rel="noopener noreferrer">
<img src={article.urlToImage} alt="thumbnail" />
</a>
</div>
)}
<div className="contents">
<h2>
<a href={article.url} target="_blank" rel="noopener noreferrer">
{article.title}
</a>
</h2>
<p>{article.description}</p>
</div>
</NewsItemBlock>
);
};
export default NewsItem;
먼저 각각의 뉴스를 표시할 NewsItem
컴포넌트를 만든다. 이 컴포넌트는 article을 통째로 props로 전달받는다.
그리고 NewsItem
컴포넌트를 여러 개 렌더링하는 NewsList
컴포넌트를 만든다.
우선 예시로 sampleArticle을 만들어서 넘겨주어서 만들어진 페이지다. 이제 진짜 뉴스를 인자로 넘겨주어 보자.
NewsItem.js
const NewsItem = ({ article }) => {
return (
<NewsItemBlock>
{article.urlToImage && (
<div className="thumbnail">
<a href={article.url} target="_blank" rel="noopener noreferrer">
<img src={article.urlToImage} alt="thumbnail" />
</a>
</div>
)}
<div className="contents">
<h2>
<a href={article.url} target="_blank" rel="noopener noreferrer">
{article.title}
</a>
</h2>
<p>{article.description}</p>
</div>
</NewsItemBlock>
);
};
export default NewsItem;
NewsList.js
const NewsList = ({articles}) => {
return (
<NewsListBlock>
{articles.map(
(article, index) => (
<NewsItem article={article} key={index} />
)
)}
</NewsListBlock>
);
};
썸네일 이미지, 제목을 포함해 간략한 내용까지 잘 나오는 것을 볼 수 있고, 클릭하면 실제 기사 원문으로 넘어간다.
우리가 사용한 뉴스 API 사이트에는 6개의 세부 카테고리를 제공한다.
그리고 API 호출 url에 카테고리 정보가 삽입된다.
business 카테고리
https://newsapi.org/v2/top-headlines?country=kr&category=business&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b
entertainment 카테고리
https://newsapi.org/v2/top-headlines?country=kr&category=entertainment&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b
카테고리 x
https://newsapi.org/v2/top-headlines?country=kr&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b
다른 URL은 같고 category
쿼리스트링만 달라진 것을 볼 수 있다. 즉 우리가 임의로 선택하여 각각 다른 카테고리의 뉴스를 보여주게끔 만들 수 있을 것이다.
먼저 카테고리 컴포넌트인 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.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;
styled-component를 이용해서 CategoriesBlock
과 Category
를 쉽게 꾸밀 수 있다.
그리고 App.js
에 추가하면 된다.
이런 식으로 위에 카테고리 선택 메뉴가 나타났다. 이제 클릭했을 때 카테고리 값을 지정해주는 useState를 App.js
에서 관리하자.
${(props) =>
props.active &&
css`
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
`}
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;
const App = () => {
const [data, setData] = useState([]);
const [category, setCategory] = useState("all");
const onSelect = useCallback(category => setCategory(category), []);
return (
<>
<Categories category={category} onSelect={onSelect}/>
{data && <NewsList category={category} articles={data} />}
</>
);
};
App.js
에서 만든 onSelect 함수는 category
state를 수정한다.
이 함수를 Categories
컴포넌트로 그대로 넘겨주고, 그 변수인 category
역시 넘겨 준다.
그리고 Categories
컴포넌트에서는 넘겨받은 category
와 각각의 카테고리의 name
이 같은지 확인 (즉 선택한 카테고리인지) 하고, 맞다면 active
프로퍼티를 참으로 한다.
이제 카테고리별로 다른 API 링크를 불러오기만 하면 된다.
NewsList.js
const NewsList = ({ category }) => {
const [data, setData] = useState([]);
useEffect(() => {
async function f() {
const response = await fetch(
`https://newsapi.org/v2/top-headlines?country=kr${
category === "all" ? "" : `&category=${category}`
}&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b`
);
const json = await response.json();
const news = json.articles;
setData(news);
}
f();
});
return (
<NewsListBlock>
{data.map((article, index) => (
<NewsItem article={article} key={index} />
))}
</NewsListBlock>
);
};
export default NewsList;
카테고리가 all
이면 아무것도 추가하지 않고, all이 아니라면 (즉 특정한 카테고리가 선택 되었을 때) 쿼리스트링을 추가하게 하여 fetch 하였다.
이런 식으로 각 카테고리마다 다른 뉴스를 볼 수 있다.