IT/React

[React] axios로 API 호출 및 데이터 받아오기

무녈 2021. 8. 15. 22:22

Axios

Axios는 현재 가장 많이 가용되고 있는 자바스크립트 HTTP 클라이언트이다.

Axios 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 처리하는 점이다.

API 키 발급받기

newapi에서 제공하는 API를 사용하여 최신 뉴스를 불러오자.

먼저 https://newsapi.org/regitser  에 가입하여 api를 발급받는다.

발급받은 API 키는 추후 API 요청시 API 주소의 쿼리 파라미터로 넣어 사용할 수 있기 때문에 따로 기록해둔다.

(기본적으로 api 주소에 본인에 해당하는 API 키가 들어있어 필수는 아니다)

 

한국 뉴스 API를 받기 위해 아래 사이트에 접속한다.

https://newsapi.org/s/south-korea-news-api

 

South Korea News API - Live top headlines from South Korea

Get live top and breaking news headlines from South Korea with our JSON API. Live example This example demonstrates the HTTP request to make, and the JSON response you will receive, when you use the News API to get live headlines from South Korea. Top head

newsapi.org

기본적으로 전체 뉴스를 불러올 수 있는 API와 

특정 카테고리 뉴스를 불러올 수 있다.

각각 링크를 통해 접속하면 

아래와 같은 API 주소를 얻을 수 있다. 

주소 뒷 부분에 apiKey={본인의 API키}를 입력하거나 본인의 api key가 미리 입력되어 있다.

api를 주소창에 입력하면 아래와 같은 데이터 형식으로 데이터가 저장되어 있음을 확인할 수 있다.

Axios로 데이터 받아 오기

필요한 라이브러리 및 프레임워크 설치

yarn create react-app [폴더명] // react 생성
yarn add axios // 데이터 호출을 위한 axios 라이브러리 추가
yarn add styled-components // 기본 format에 style을 추가하기 위한 라이브러리 추가

NewsItem 작성

{
  "source": {
    "id": "google-news",
    "name": "Google News"
  },
  "author": null,
  "title": "'성추행 피해' 해군 여중사 현충원 안장...2차 가해 수사 본격화 / YTN - YTN news",
  "description": null,
  "url": "https://news.google.com/__i/rss/rd/articles/CBMiK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9OXFvVXRGYmR0QU3SAQA?oc=5",
  "urlToImage": null,
  "publishedAt": "2021-08-15T08:12:53Z",
  "content": null
}

뉴스 데이터는 JSON 객체로 제공된다.

 

리액트 컴포넌트 요소로 나타낼 데이터는

- title: 제목
- description: 내용
- url: 링크
- urlToImage: 뉴스 이미지

네 가지 요소이다.

 

NewsItem 컴포넌트는 article이라는 객체를 props로 통째로 받아와 사용한다.

 

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: 180px;
      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 작성

News의 데이터를 불러올 컴포넌트를 작성한다.

처음 작성시에는 데이터를 불러오지 않고 불러올 데이터를 바탕으로 skelton code를 작성한다.

 

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} />
        <NewsItem article={sampleArticle} />
      </NewsListBlock>
    );
}

export default NewsList;

sampleArticle 객체는 미리 예시 데이터를 넣은 후 각 컴포넌트에 전달하여 가짜 내용이 보이게 한다.

 

이 컴포넌트를 App 컴포넌트에 삽입한다.

App.js

import React from "react";
import NewsList from "./components/NewsList.jsx";
function App() {
  return <NewsList />;
}

export default App;

 

결과 

현재까지의 결과는 이렇게 나타난다.

 

데이터 연동하기

실제 newsapi로 부터 얻은 api key를 통해 api를 호출해본다.

컴포넌트가 화면에 보이는 시점에 API를 요청하기 위해 useEffect를 사용,

컴포넌트가 처음 렌더링되는 시점에 API를 요청한다.

 

*주의

useEffect에 등록하는 함수에 async를 붙이면 안된다. ➡️ useEffect에서 반환해야 하는 값은 뒷 정리 함수이기 때문

▶️ useEffect 내부에서 async/await을 사용하고 싶다면, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어 사용한다.

useEffect(() => {
    // async를 사용하는 함수 따로 선언
      const fetchData = async () => {
          setLoading(true);
          try {
              const response = await axios.get(
                  "https://newsapi.org/v2/top-headlines?country=kr&apiKey={개인 apiKey}"
              );
              setArticles(response.data.articles);
          } catch (e) {
              console.log(e);
          }
          setLoading(false);
      }
      fetchData();
  }, []);

API를 호출하는 데 있어, loading이라는 상태도 관리하여 API 요청이 대기 중인지 판별한다.

요청이 대기 중일 때는 loading 값이 true가 되고, 요청이 끝나면 loading 값이 false가 되어야 한다.

 

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(() => {
    // async를 사용하는 함수 따로 선언
      const fetchData = async () => {
          setLoading(true);
          try {
              const response = await axios.get(
                  "https://newsapi.org/v2/top-headlines?country=kr&apiKey={개인 apiKey}"
              );
              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 함수를 사용해 컴포넌트 배열로 변환 시 신경써야하는 부분이 존재

map 함수를 사용하기 전에 !articles를 조회하여 해당 값이 null인지 안니지 검사해야 한다.

이 작업을 하지 않을 경우, 아직 데이터가 없을 때 null에는 map 함수가 없기 때문에 렌더링 과정에서 오류가 발생.

 

정상적인 결과

 

if (!articles) {
    return null;
  }

이 없는 경우

TypeError: Cannot read property 'map' of null

위와 같은 TypeError가 발생한다.

카테고리 기능 구현

뉴스 카테고리 선택 기능을 구현한다.

뉴스 카테고리는 총 여섯 개이며 다음과 같이 영어로 되어있다.

- business
- entertainment
- health
- science
- sports
- technology

카테고리 선택 UI 만들기

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;

App.js에 위치시킨다.

 

App에서는 category 상태를 useState로 관리한다. 추가로 category값을 업데이트 하는 onSelect 함수를 만들어준다.

그러고 나서 category와 onSelect 함수를 Categories 컴포넌트에게 props로 전달한다.

또한, category 값을 NewsList 컴포넌트에게 전달해주어야 한다.

 

App.js

import React, { useState, useCallback } from "react";
import NewsList from "./components/NewsList.jsx";
import Categories from "./components/Categories.js";
function App() {
  const [category, setCategory] = useState('all');
  const onSelect = useCallback(category => setCategory(category), []);

  return (
    <>
      <Categories category={category} onSelect={onSelect}/>
      <NewsList category={category}/>;
    </>
  );
}

export default App;

코드에서 categories라는 배열 안에 name과 text 값이 들어가 있는 객체들을 넣어 주어 한글로 된 카테고리와 실제 카테고리 값을 연결시켜준다.

name은 실제 카테고리 값을 가리키고, text 값은 렌더링 시 사용할 한글 카테고리를 가리킨다.

 

Categories에서 props로 전달받은 onSelect를 각 Category 컴포넌트의 onClick으로 설정해주고, 현재 선택된 카테고리 값에 따라 다른 스타일을 적용시킨다.

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;

 

App에서 category 상태를 useState로 관리, 추가로 category 값을 업데이트 하는 onSelect 함수를 생성한다.

category와 onSelect 함수를 Catergories컴포넌트에게 props로 전달, category 값을 NewsList 컴포넌트에게 전달

App.js

import React, { useState, useCallback } from "react";
import NewsList from "./components/NewsList.jsx";
import Categories from "./components/Categories.js";
function App() {
  const [category, setCategory] = useState('all');
  const onSelect = useCallback(category => { setCategory(category); console.log("category", category)}, []);

  return (
    <>
      <Categories category={category} onSelect={onSelect}/>
      <NewsList category={category}/>;
    </>
  );
}

export default App;

Categories에서는 props로 전달받은 onSelect를 각 Category 컴포넌트의 onClick으로 설정해주고, 현재 선택된 카테고리 값에 따라

다른 스타일을 적용시킨다.

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;

API 호출할 때 카테고리 지정하기

NewsList 컴포넌트에서 현재 props로 받아 온 category에 따라 카테고리를 지정하여 API를 요청

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 = ({ 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={개인 apiKey}`
              );
              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 값이 무엇인지에 따라 요청할 주소가 동적으로 바뀌고 있다.

 

category 값이 바뀔 때마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열(deps)에 category를 넣어준다.

리액트 라우터 적용하기

카테고리 값을 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 { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

NewsPage 생성

리액트 라우터를 적용할 때 사용할 페이지

NewsPage.js

import React from "react";
import Categories from "../components/Categories.jsx";
import NewsList from "../components/NewsList.jsx";

const NewsPages = ({ match }) => {
  // 카테고리가 선택되지 않으면 기본값 all 사용
  const category = match.params.category || "all";

  return (
    <>
      <Categories />
      <NewsList category={category} />
    </>
  );
};

export default NewsPages;

현재 선택된 category 값을 URL 파라미터를 통해 사용할 것이므로, Categories 컴포넌트에서 현재 선택된 카테고리 값을 알려줄 필요 없고, onSelect 함수를 따로 전달해 줄 필요도 없다.

 

console.log(match)

match 확인

 

{match} 관련 설명

더보기

https://velog.io/@leo-xee/React-Router%EC%9D%98-match-location-history-%EA%B0%9D%EC%B2%B4

 

React - Router의 match, location, history 객체

브라우저와 리액트 앱의 Router르 연결하면 Router가 history api에 접근할 수 있게 되고 각 Route와 연결된 컴포넌트의 props로 match, location, history 객체를 기본적으로 전달하게 된다.

velog.io

React의 props

브라우저와 리액트 앱의 Router르 연결하면 Router가 history api에 접근할 수 있게 되고 각 Route와 연결된 컴포넌트의 props로 match, location, history 객체를 기본적으로 전달

 

match

match 객체에는 Route path와 URL의 매칭에 대한 정보를 가지고 있다.

  • isExact: true이면 경로가 완전히 정확할 경우에만 수행한다.
  • params: 경로에 전달된 파라미터 값을 가진 객체
  • path: Route에 정의된 경로
  • url: 클라이언트로부터 실제 요청 받은 경로

App.js의 기존 내용을 모두 지우고 Route를 정의한다.

App.js

import React from "react";
import { Route } from 'react-router-dom';
import NewsPages from "./pages/NewsPage";
function App() {
  return <Route path="/:category?" component={NewsPages} />;
}

export default App;

위 코드에서 사용된 path에 /:category? 와 같은 형태로 맨 뒤에 물음표 문자가 들어감 -> 정규표현식

 

정규표현식 참고

Categories에서 NavLink 사용

Categoriesdptj rlwhsdml onSelect 함수를 호출하여 카테고리를 선택하고, 선택된 카테고리에 다른 스타일을 주는 기능을 NavLink로 대체

일반 HTML 요소가 아닌 특정 컴포넌트에 styled-components를 사용할 때는 styled(컴포넌트이름) `` 과 같은 형식을 사용한다.

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;

NavLink로 만들어진 Category 컴포넌트에 to 값은 "/카테고리이름"으로 설정

전체보기의 경우 예외적으로 "/all" 대신 '/'로 설정

to 값이 '/'를 가리길 때는 exact를 true로 설정해주어야한다.

반응형