외부 API 연동하여 뉴스 뷰어 만들기
이번에는 지금까지 배운 것들을 활용하여 외부 API 를 연동하여 뉴스 뷰어 프로젝트를 만들어보겠습니다. 이번 프로젝트에서는 styled-components 를 사용하여 스타일링 하겠습니다.
1. 비동기 작업의 이해
우리가 웹 애플리케이션을 만들다보면 처리를 할 때 시간이 걸리는 작업들이 있습니다. 예를 들어서, 웹 애플리케이션에서 서버쪽의 데이터가 필요 할 때는 Ajax 기법을 사용하여 서버의 API 를 호출하여 데이터를 수신합니다. 이렇게 서버의 API 를 사용해야 하는 상황에서는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 시작하는 즉시 바로 처리되는것이 아니라, 응답을 받을 때 까지 기다렸다가 전달받은 응답 데이터를 처리하게 됩니다. 이 과정에서 우리는 이 작업을 비동기적으로 처리하게 됩니다.
만약에 작업을 동기적으로 처리를 한다면, 요청이 끝날 때 까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없습니다. 그리고, 요청이 끝나야 비로소 그 다음 예정된 작업을 할 수 있죠. 하지만 이를 비동기적으로 처리를 한다면, 웹 애플리케이션이 멈추지 않기 때문에 동시에 여러가지 요청을 처리 할 수도 있고, 기다리는 과정에서 다른 함수도 호출 할 수 있습니다.
이렇게 서버 API 를 호출 할 떄 외에도 작업을 비동기적으로 처리를 하게 될 때가 있는데, 바로 setTimeout
함수를 사용하여 특정 작업을 예약 할 때 입니다. 예를 들어서 다음 코드는 3초 후에 printMe 함수를 호출해줍니다:
function printMe() {
console.log('Hello World!');
}
setTimeout(printMe, 3000);
console.log('대기중...');
결과
대기중...
Hello World!
코드가 setTimeout 시점에서 코드가 3초동안 멈추는게 아니라, 일단 코드가 위부터 아래까지 다 호출이 되고, 3초뒤에 우리가 지정해준 printMe가 호출되고 있죠.
JavaScript 에서 비동기 작업을 할 떄 가장 흔히 사용되는 방법은 콜백(Callback) 함수를 사용하는 것입니다. 위 코드에서는, printMe 가 3초뒤 호출 되도록 printMe 함수 자체를 setTimeout 함수의 인자로 전달해 줬는데, 이런 함수를 콜백함수라고 부릅니다.
콜백 함수
자, 이번에는 다른 코드를 확인해보겠습니다. 예를 들어서 파라미터 값이 주어지면 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('작업 완료');
});
});
});
});
작업 시작
10
20
30
40
작업 완료
이렇게 콜백 안에 콜백을 또 넣어서 구현 할 수 있는데, 너무 여러번 중첩되니까 코드의 가독성이 나빠졌지요? 이러한 형태의 코드를 콜백지옥이라고 부릅니다. 왠만하면 지양해야 할 형태의 코드죠.
Promise
Promise 는 콜백지옥같은 코드가 형성되는 것에 대한 해결방안으로 ES6에 도입된 기능입니다. 우리가 위에서 봤던 코드를 Promise 를 사용하여 구현 된 예제를 확인해봅시다.
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); // 정상적인 상황에서는 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);
})
.then(number => {
console.log(number);
return increase(number);
})
.catch(e => {
// 도중에 에러가 발생한다면 .catch 를 통하여 알 수 있음
console.log(e);
})
여러 작업을 연달아서 한다고 해서, 함수를 여러번 감싸는 것이 아니기 때문에 콜백 지옥이 형성되지 않습니다.
async/await
async/await 은 Promise 를 더욱 쉽게 사용 할 수있게 해주는 ES2017 (ES8) 문법입니다. 이 문법을 사용하고 함수의 앞부분에 async
키워드를 추가하고, 해당 함수 내부에서 Promise 의 앞부분에 await
키워드를 사용하면 Promise 가 끝날 때 까지 기다려주고, 결과값을 특정 변수에 담을 수 있습니다.
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); // 정상적인 상황에서는 10 성공
}, 1000)
});
return promise;
}
async function runTasks() {
try { // try / catch 구문을 사용하여 에러를 처리 합니다.
let result = await increment(0);
console.log(result);
result = await increment(result);
console.log(result);
result = await increment(result);
console.log(result);
result = await increment(result);
console.log(result);
result = await increment(result);
console.log(result);
result = await increment(result);
console.log(result);
} catch (e) {
console.log(e);
}
}
2. axios 로 API 호출해서 데이터 받아오기
axios 는 현재 가장 많이 사용되고 있는 JavaScript HTTP Client 입니다. 이 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 만들어져 있다는 점 입니다. 한번 리액트 프로젝트를 생성하여 이 라이브러리를 설치하고, 사용하는 방법을 알아보겠습니다.
$ yarn create react-app news-viewer
$ cd news-viewer
$ yarn add axios
프로젝트를 에디터로 열고, App.js 코드를 전부 지우고 다음과 같이 새로 작성해보세요:
App.js
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
state = {
data: null
};
handleClick = () => {
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(response => {
this.setState({
data: response.data
});
});
};
render() {
const { data } = this.state;
return (
<div>
<div>
<button onClick={this.handleClick}>불러오기</button>
</div>
{data && <textarea rows={7} value={JSON.stringify(data, null, 2)} />}
</div>
);
}
}
export default App;
위 코드는 불러오기 버튼을 누르면 JSONPlaceholder 에서 제공하는 가짜 API 호출하고 이에 대한 응답을 state 에 넣어서 보여주는 예제입니다.
handleClick 함수에서는 axios.get 함수를 사용하였는데요, 이 함수는 파라미터로 전달된 주소에 GET 요청을 해줍니다. 그리고, 이에 대한 결과는 .then 을 통하여 비동기적으로 조회 할 수 있습니다.
위 코드에 async 를 얹으면 어떨까요?
App.js - handleClick
handleClick = async () => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/todos/1'
);
this.setState({
data: response.data
});
} catch (e) {
console.log(e);
}
};
화살표 함수에 async/await 을 적용 할 때에는 async () => {}
와 같은 형식으로 적용합니다.
3. newsapi API 키 발급받기
이번 프로젝트에서 우리는 newsapi 에서 제공하는 API 를 사용하여 최신 뉴스를 불러와서 보여줄 것입니다. 이를 수행하기 위해서는 사전에 newsapi 에서 API 키를 발급받아야 합니다.
https://newsapi.org/register 에서 가입을 하시면,
가입 성공 후 이렇게 API 키가 발급되는데 이를 추후 API 요청 할때 사용하시면 됩니다.
이제 우리가 사용 할 API 에 대하여 알아봅시다. https://newsapi.org/s/south-korea-news-api 링크에 들어가시면, 한국 뉴스를 가져오는 API 에 대한 설명서가 있습니다.
우리가 사용하게 될 API 주소는 두가지 형태 입니다.
전체 뉴스 불러오기
GET https://newsapi.org/v2/top-headlines?**country=kr**&apiKey=0a8c4202385d4ec1bb93b7e277b3c51f
특정 카테고리 불러오기
여기서 카테고리는 business, entertainment, health, science, sports, technology 중에서 골라서 사용 할 수 있습니다. 만약에 카테고리가 생략되면 모든 카테고리의 뉴스를 불러오게 됩니다. apiKey 값의 경우엔 이전에 여러분이 발급받았던 API Key 를 입력해주세요.
이제 기존에 리액트 프로젝트에서 사용했던 JSONPlaceholder 가짜 API 를 전체 뉴스를 불러오는 API 로 대체해보세요.
App.js - handleClick
handleClick = async () => {
try {
const response = await axios.get(
'https://newsapi.org/v2/top-headlines?country=kr&apiKey=0a8c4202385d4ec1bb93b7e277b3c51f'
);
this.setState({
data: response.data
});
} catch (e) {
console.log(e);
}
};
이제 이 데이터를 화면에 예쁘게 보여주면 되겠지요?
4. 뉴스 뷰어 UI 만들기
이번엔 styled-components 를 사용하여 뉴스 정보를 보여줄 컴포넌트를 만들어보겠습니다. 우선 styled-components 를 설치해주세요.
$ yarn add styled-components
그리고, src/components 디렉토리에 NewsItem.js 와 NewsList.js 를 생성하세요.
NewsItem 는 각 뉴스 정보를 보여주는 컴포넌트이고, NewsList 는 API 를 요청을 하고 뉴스 데이터가 들어있는 배열을 컴포넌트 배열로 변환하여 렌더링해주는 컴포넌트 입니다.
NewsItem 만들기
먼저 NewsItem 컴포넌트 코드를 작성해볼건데요, 그 전에 각 뉴스 데이터에는 어떤 필드들이 있는지 확인해봅시다:
{
"source": {
"id": null,
"name": "Donga.com"
},
"author": null,
"title": "“새 집 냄새” “주택 청약 고마워!”…이시언 아파트 공개 - 동아일보",
"description": "배우 이시언(37)이 자신의 새 아파트를 공개했다. 이시언은 25일 방송한 MBC 예능 ‘나 혼자 산다’에서 정든 옛집을 떠나 새 아파트로 이사했다. 이사한 아파트에 도착한 …",
"url": "http://news.donga.com/Main/3/all/20190126/93869524/2",
"urlToImage": "http://dimg.donga.com/a/600/0/90/5/wps/NEWS/IMAGE/2019/01/26/93869523.2.jpg",
"publishedAt": "2019-01-26T00:21:00Z",
"content": null
},
이 중에서 우리는 다음 필드들을 리액트 컴포넌트에서 보여주겠습니다:
- title: 제목
- description: 내용
- url: 링크
- urlToImage: 뉴스 이미지
NewsItem 컴포넌트는 뉴스 데이터를 article 이라는 props로 통째로 받아와서 사용하게 됩니다.
NewsItem 컴포넌트를 다음과 같이 작성해보세요:
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;
styled-components 를 사용한다고 해서 내부에 있는 모든 HTML 요소들을 styled-components 로 하나하나 컴포넌트화 하실 필요는 없습니다. 예를 들자면, 다음과 같은 형태로 코드를 작성 할 수도 있긴 하지만:
import React from 'react';
import styled from 'styled-components';
const NewsItemBlock = styled.div`
display: flex;
& + & {
margin-top: 3rem;
}
`;
const Thumbnail = styled.div`
margin-right: 1rem;
`;
const ThumbnailImg = styled.img`
display: block;
width: 160px;
height: 100px;
object-fit: cover;
`;
const Contents = styled.div``;
const Title = styled.h2`
margin: 0;
`;
const TitleLink = styled.a`
color: black;
`;
const Description = styled.p`
margin: 0;
line-height: 1.5;
margin-top: 0.5rem;
white-space: normal;
`;
const NewsItem = ({ article }) => {
const { title, description, url, urlToImage } = article;
return (
<NewsItemBlock>
{urlToImage && (
<Thumbnail>
<a href={url} target="_blank" rel="noopener noreferrer">
<ThumbnailImg src={urlToImage} alt="thumbnail" />
</a>
</Thumbnail>
)}
<Contents>
<Title>
<TitleLink
href={url}
tTitleLinkget="_blank"
rel="noopener noreferrer"
>
{title}
</TitleLink>
</Title>
<Description>{description}</Description>
</Contents>
</NewsItemBlock>
);
};
export default NewsItem;
이런 형태로 컴포넌트를 작성하시면 내부에 컴포넌트를 너무 많이 만들게 될 수도 있습니다. 조건부 스타일링이 없는 간단한 스타일을 가진 HTML 요소도 하나하나 컴포넌트로 만들어주면 컴포넌트의 전체 스타일을 한눈에 보기 힘들 수도 있습니다.
이전에는 이렇게 컴포넌트를 너무 많이 만들게 되면 성능이 조금 느려지는 이슈가 있었지만, styled-components v4 기준 정말 많이 최적화 되어있기 때문에 이런 방식은 성능에 큰 방해가 되지는 않습니다. 만약에 여러분이 CSS / Sass 에 익숙하시다면 컴포넌트에 CSS Selector 를 자주 사용하는 형태로 코드를 작성하시고, 하나하나 컴포넌트로 만들어주는게 맘에 드신다면 취향에 따라 그렇게 작업을 하셔도 무방합니다.
이 프로젝트에서는 첫번째 방식을 사용하여 스타일링 하겠습니다.
NewsList 만들기
이번에는 NewsList 컴포넌트를 만들어보겠습니다. 우리가 추후 API 를 이 컴포넌트에서 요청 할 것이기 때문에, 클래스 형태 컴포넌트로 만들어주겠습니다. 지금은 데이터를 아직 불러오지 않고 있으니까 sampleArticle 이란 객체에 예시 데이터를 미리 넣어서 각 컴포넌트에 전달하세요.
NewsList.js
import React, { Component } 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'
};
class NewsList extends Component {
render() {
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 컴포넌트에서 보여주세요.
App 컴포넌트에 기존에 작성했던 코드들은 모두 지워주고, NewsList 만 렌더링 해보세요.
추후 이 컴포넌트에서 카테고리를 상태를 관리 하게 될 예정이니 클래스형 컴포넌트로 작성하겠습니다.
import React, { Component } from 'react';
import NewsList from './components/NewsList';
class App extends Component {
render() {
return <NewsList />;
}
}
export default App;
컴포넌트들이 잘 보여지고 있나요?
5. 데이터 연동하기
이제 NewsList 컴포넌트에서 이전에 연습삼아 사용했었던 API 를 호출해보겠습니다. 우리는 컴포넌트가 화면에 보여지는 시점에서 API 요청을 할 건데요, 이 때 componentDidMount 메서드에서 API 를 요청하시면 됩니다. 하지만, 라이프사이클 API 함수 앞에 async
키워드를 사용하면 오류가 나지는 않지만 헷갈리는 형태의 코드이기 때문에, 따로 loadData
라는 메서드를 만들어서 해당 메서드 안에서 API 를 요청하겠습니다.
API 를 요청 할 때에는 요청을 시작 할 때 state 에 있는 loading 값을 true 로 설정하고, 응답을 받고나서 다시 false 로 전환합니다. 이렇게 하면 UI 단에서 현재 로딩중인지 아닌지를 판별 할 수 있게 됩니다.
NewsList.js
import React, { Component } 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;
}
`;
class NewsList extends Component {
state = {
loading: false,
articles: null
};
loadData = async () => {
try {
this.setState({
loading: true
});
const response = await axios.get(
'https://newsapi.org/v2/top-headlines?country=kr&apiKey=0a8c4202385d4ec1bb93b7e277b3c51f'
);
this.setState({
articles: response.data.articles
});
} catch (e) {
console.log(e);
}
this.setState({
loading: false
});
};
componentDidMount() {
this.loadData();
}
render() {
const { articles, loading } = this.state;
if (loading || !articles) {
return <NewsListBlock>로딩중..</NewsListBlock>;
}
return (
<NewsListBlock>
{articles.map(article => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
}
}
export default NewsList;
데이터를 불러와서 컴포넌트로 map 할 때 신경써야 할 부분은, map 을 하시기전에 꼭 !articles
를 조회하여 해당 값이 현재 null 이 아닌지 검사를 하셔야 한다는 것 입니다. 만약에 이 작업을 하지 않으면 데이터가 아직 없을 때 null 에는 .map 함수가 없기 때문에 리액트 애플리케이션이 크래쉬 될 수 있습니다.
이제, 뉴스 정보가 잘 보여지는지 확인해보세요.
6. 카테고리 기능 구현하기
이번에는 뉴스의 카테고리 선택 기능을 구현해보겠습니다. 뉴스 카테고리는 총 6개가 있는데요, 이 카테고리들은 다음과 같은 형식으로 영어로 이루어져있습니다:
- business (비지니스)
- entertainment (연예)
- health (건강)
- science (과학)
- sports (스포츠)
- technology (기술)
화면에 카테고리를 보여주게 될 때 영어로 이루어진 값을 그대로 보여주는 대신에, 다음 이미지에서 보여지는 것 처럼 한국어로 보여주고, 클릭 했을 때는 영어로된 카테고리 값을 사용하도록 구현하겠습니다.
카테고리 선택 UI 만들기
먼저 components 디렉터리에 Categories.js 라는 컴포넌트를 생성하여 다음 코드를 작성하세요.
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;
&:hover {
color: #495057;
}
& + & {
margin-left: 1rem;
}
`;
const Categories = () => {
return (
<CategoriesBlock>
{categories.map(c => (
<Category key={c.name}>{c.text}</Category>
))}
</CategoriesBlock>
);
};
export default Categories;
위 코드에서는 한국어로된 카테고리와 실제 카테고리 값을 연결시켜주기 위하여 categories 라는 배열안에 name 과 text 값이 들어가있는 객체들을 넣어주었습니다. 여기서 name 은 실제 카테고리 값을 가르키게 되고, text 값은 렌더링할때 사용 할 한국어 카테고리를 가르키게 됩니다.
다 만드셨으면 해당 컴포넌트를 App 에서 NewsList 컴포넌트 상단에 렌더링하세요.
App.js
import React, { Component } from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';
class App extends Component {
render() {
return (
<>
<Categories />
<NewsList />
</>
);
}
}
export default App;
다음과 같이 상단에 카테고리 목록이 나타났나요?
이제 App 에서 category 값을 state 객체 안에 넣으세요. 그리고, 이 값을 업데이트 하는 handleSelect 라는 함수를 만드세요.
App.js
import React, { Component } from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';
class App extends Component {
state = {
category: 'all'
};
handleSelect = category => {
this.setState({
category
});
};
render() {
return (
<>
<Categories />
<NewsList />
</>
);
}
}
export default App;
그리고 나서, this.state.category 값과 handleSelect 함수를 Categories 에게 props 로 전달하세요. 또, NewsList 컴포넌트에도 추후 어떤 카테고리의 뉴스를 불러와야 할 지 알아야 하기 때문에 this.state.category 값을 props 로 전달하세요.
App.js
import React, { Component } from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';
class App extends Component {
state = {
category: 'all'
};
handleSelect = category => {
this.setState({
category
});
};
render() {
const { category } = this.state;
return (
<>
<Categories category={category} onSelect={this.handleSelect} />
<NewsList category={category} />
</>
);
}
}
export default App;
이제 Categories 에서 방금 전달받은 category 값을 사용하여 현재 선택된 카테고리에 조건부 스타일링을 해주도록 하겠습니다. 추가적으로 각 Category 컴포넌트에 onClick 이벤트를 지정해주세요.
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;
}
${props =>
props.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}
active={category === c.name}
onClick={() => onSelect(c.name)}
>
{c.text}
</Category>
))}
</CategoriesBlock>
);
};
export default Categories;
다음과 같이 선택된 카테고리가 청록색으로 보여졌나요? 다른 카테고리들도 클릭해보세요. 선택이 잘 이루어지나요?
API 호출 할 때 카테고리 지정하기
지금은 우리가 뉴스 API 를 요청하게 될 때 따로 카테고리를 선택하지 않고 뉴스 목록을 불러오고 있습니다. NewsList 컴포넌트에서 현재 props 로 받아온 category 에 따라 카테고리를 지정하여 API 를 요청하도록 구현해보세요.
NewsList.js - loadData, componentDidUpdate
loadData = async () => {
try {
this.setState({
loading: true
});
const { category } = this.props;
// 카테고리 선택 쿼리
const query = category === 'all' ? '' : `&category=${category}`;
const response = await axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=0a8c4202385d4ec1bb93b7e277b3c51f`
);
this.setState({
articles: response.data.articles
});
} catch (e) {
console.log(e);
}
this.setState({
loading: false
});
};
componentDidUpdate(prevProps, prevState) {
// category 값이 바뀔 때 함수 재호출
if (prevProps.category !== this.props.category) {
this.loadData();
}
}
category 값이 all 일때는 query 값을 비우고, 만약에 all 을 제외한 다른 값으로 되어있다면 &category=카테고리 형태의 값은 주소에 포함시켜서 요청을 하도록 loadData 함수를 수정해주었습니다.
그리고, category 값이 바뀔 때 마다 loadData 함수가 재호출 되어야 하니, componentDidUpdate 에서 이 작업을 수행해주었습니다.
여기까지 작업을 마치셨다면 브라우저를 열어서 다른 카테고리를 선택해보세요. 카테고리에 따른 뉴스들이 잘 나타나고 있나요?
7. 리액트 라우터 적용하기
이번에는, 우리가 만든 프로젝트에 리액트 라우터를 적용해보겠습니다. 기존에는 우리가 카테고리값을 컴포넌트의 state 에 넣어서 관리를 했는데요, 이번에는 이 값을 리액트 라우터의 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 * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
serviceWorker.unregister();
NewsPage 생성
이번 프로젝트에서 만들어야 할 페이지는 단 하나입니다. src 디렉터리에 pages 라는 디렉터리를 생성하고, 그 안에 NewsPage.js 파일을 만들어서 다음과 같이 작성해보세요.
pages/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 에서 기존에 있던 내용들을 다 지우고 Route 를 정의해주세요.
import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import NewsPage from './pages/NewsPage';
class App extends Component {
render() {
return (
<>
<Route path="/:category?" component={NewsPage} />
</>
);
}
}
export default App;
위 코드에서 사용된 path 에 /:category? 이런식으로 맨 뒤에 물음표 문자가 들어가있는데요, 이 의미는 category 값이 선택적(optional)이라는 의미입니다. 즉, 있을수도 있고 없을 수도 있다는 것이죠. 만약에 category URL 파라미터가 없다면 전체 카테고리를 선택한것으로 간주하게 됩니다.
Categories 에서 NavLink 사용하기
이제 Categories 에서 기존에 onSelect 함수를 호출하여 카테고리를 선택하고, 선택된 카테고리에 다른 스타일을 주는 기능을 NavLink 로 대체하겠습니다. div, a, button, input 처럼 일반 HTML 요소가 아닌 특정 컴포넌트에 styled-components 를 사용할 땐 styled(컴포넌트이름)`` 와 같은 형식으로 사용합니다.
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 = ({ onSelect }) => {
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;
NavLink 로 만들어진 Category 컴포넌트에 to 값은 기본적으로 "/카테고리이름" 주소로 이동하게끔 설정을 해주었는데요, map 을 통하여 렌더링을 하는 과정에서 전체보기 링크를 보여줄 때에는 "/all" 대신에 "/" 으로 설정하였습니다. 그리고 "/" 를 가르키고 있을 땐 exact 값이 true 가 되도록 설정하였습니다. 만약 이 작업을 하지 않으면 전체보기가 선택되지 않았는데도 전체보기 쪽에 스타일이 적용되는 오류가 발생 할 수 있습니다.
작업을 마치셨다면 카테고리를 클릭 할 때 페이지 주소가 바뀌고, 이에 따라 뉴스 목록을 잘 보여주고 있는지 확인하세요.
8. 정리
이번 튜토리얼에서는 외부 API 를 연동하여 사용하는 방법을 알아보고, 지금까지 배운 것들을 활용하여 실제로 쓸모있는 프로젝트를 개발해보았습니다. 리액트 프로젝트에서 외부 API 를 연동하여 개발하게 될 때에 잘 알아두어야 할 두가지 팁이 있습니다. 첫째는, 컴포넌트를 처음 보여줄 때 외부 데이터를 보여줘야 한다면 componentDidMount 에서 API 를 호출해야 한다는 것과, props 변화에 따라 새로 요청을 해야 할 때는 componentDidUpdate 에서 호출해야 한다는 것 입니다.
이 프로젝트에서는 API 가 현재 로딩중임을 명시하기 위하여 컴포넌트의 state 에 loading 이라는 값을 사용하여 로딩중 상태를 관리하고 있습니다. 하지만 나중에 API 개수가 많아지면 컴포넌트의 state 로 일일히 관리하는 것이 번거로워 질 수도 있는데요, 이는 추후 Redux 와 Redux 미들웨어를 배우게 되면 조금 더 손쉽게 관리 할 수 있게 됩니다.