axios로 API 호출 및 뉴스 가져오기

ho_vi·2023년 9월 19일

React

목록 보기
11/19
post-thumbnail

axios란?

axios는 자바스크립트에서 사용되는 HTTP 클라이언트 라이브러리로, 브라우저와 Node.js에서 모두 사용할 수 있습니다.

axios는 XMLHttpRequests 요청을 자동으로 생성하고, HTTP 요청과 응답을 자바스크립트 객체로 변환하여 사용자가 더 쉽게 HTTP 데이터를 조작하도록 도와줍니다. 또한, 요청을 인터셉트하여 요청이나 응답에 대한 전처리나 후처리를 할 수 있습니다.

특히, 프론트엔드 개발에서 API와의 통신을 처리하는데 많이 사용됩니다. axios는 다른 라이브러리와 함께 사용될 수 있으며, promise를 기반으로 비동기 요청을 처리합니다. 이러한 특징으로 인해 코드의 가독성과 유지보수성을 높일 수 있습니다.

특징

  • 운영 환경에 따라 브라우저의 XMLHttpRequest 객체 또는 Node.js의 HRRP API 사용
  • Promise(ES6) API 사용
  • 요청과 응답 데이터의 변형
  • REST API 규약을 따름
👉 **axios와 ajax**
  1. ajax
    ajax(Asynchronous JavaScript and XML)는 XMLHttpRequest 객체를 이용하여 서버와 비동기적으로 데이터를 주고받습니다. ajax는 JavaScript를 사용하여 웹 페이지를 동적으로 업데이트할 수 있도록 하는 기술입니다. ajax는 XML을 주고받는 기술이라고 이름붙였지만, 현재는 JSON을 주로 사용합니다. ajax는 jQuery를 비롯한 여러 라이브러리에서도 사용할 수 있습니다.
  2. axios
    axios는 Promise 기반의 HTTP 클라이언트 라이브러리입니다. axios는 브라우저와 Node.js 환경에서 모두 사용할 수 있습니다. axios는 XMLHttpRequest 객체를 사용하여 서버와 통신하지만, Promise를 사용하여 데이터를 반환합니다. axios는 JSON을 기본으로 지원하며, 또한 HTTP 요청과 응답을 인터셉터로 변경할 수 있는 기능도 제공합니다.

두 기술 모두 비동기적으로 서버와 데이터를 주고받으며, 브라우저에서 사용할 수 있습니다. 그러나 ajax는 XMLHttpRequest 객체를 직접 다루는 반면, axios는 더 간단하고 강력한 Promise 기반의 API를 제공합니다. 또한 axios는 HTTP 요청과 응답을 인터셉터로 처리하는 기능도 지원하여, 더욱 유연한 HTTP 통신을 가능하게 합니다.

👉 **REST API 응답 코드** - 200 OK : 요청이 성공적으로 수행되었음 - 201 Created : 요청이 성족적으로 수행되었고, 새로운 리소스가 생성되었음. - 400 Bad Request : 요청 값이 잘못되어 요청이 실패했음 - 403 Forbidden : 권한이 없어 요청에 실패했음 - 404 Not Found : 요청 값으로 찾은 리소스가 없어 요청에 실패했음 - 500 Internal Server Error : 서버 상에 문제가 있어 요청에 실패했음

$ yarn add axios

import React, { useState } from "react";
import axios from "axios";

const Axios = () => {
    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 Axios;

화살표 함수에 async/await를 적용 할 때는 async() ⇒ {}와 같은 형식으로 적용합니다.

import React, { useState } from "react";
import axios from "axios";

const Axios = () => {
    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 Axios;

Styled Components로 React 스타일 하기

CSS in JS는 스타일 정의를 CSS 파일이 아닌 JavaScript로 작성된 컴포넌트에 바로 삽입하는 스타일 기법입니다.

기존에 웹사이트를 개발할 때는 HTML과 CSS, JavaScript는 각자 별도의 파일에 두는 것이 best practice로 여겨졌었습니다. 하지만 React나 Vue, Angular와 같은 모던 자바스크립트 라이브러리가 인기를 끌면서 웹개발의 패러다임이 바뀌고 있습니다. 최근에는 웹 애플리케이션을 여러 개의 재활용이 가능한 빌딩 블록으로 분리하여 개발하는 컴포넌트 기반 개발 방법이 주류가 되고 있습니다.

따라서, 웹페이지를 HTML, CSS, JavaScript 3개로 분리하는 것이 아니라, 여러 개의 컴포넌트로 분리하고, 각 컴포넌트에 HTML, CSS, JavaScript를 몽땅 때려 박는 패턴이 많이 사용되고 있습니다. React는 JSX를 사용해서 이미 JavaScript가 HTML을 포함하고 있는 형태를 취하고 있는데, 여기에 CSS-in-JS 라이브러리만 사용하면 CSS도 손쉽게 JavaScript에 삽입할 수 있습니다.

$ yarn add styled-components

기본 문법

먼저 위에서 설치한 styled-components패키지에서 styled함수를 임포트합니다. styled는 Styled Components의 근간이 되는 가장 중요한 녀석인데요. HTML 엘리먼트나 React 컴포넌트에 원하는 스타일을 적용하기 위해서 사용됩니다.

import styled from "styled-components";

styled.button`
  font-size: 1rem;
`;
button {
  font-size: 1rem;
}

newsapi API 키 발급받기

이번 프로젝트에서는 newsapi에서 제공하는 API를 사용하여 최신 뉴스를 불러온 후 보여 줄 것입니다.

Login - News API

ffbbc82f694941a7b0e2d4f4515abcc7

사용할 API 알아 보기

South Korea News API - Live top articles from South Korea

1. 전체뉴스 불러오기

"https://newsapi.org/v2/top-headlines?country=kr&apiKey=ffbbc82f694941a7b0e2d4f4515abcc7"

2. 특정 카테고리 뉴스 불러오기

"https://newsapi.org/v2/top-headlines?country=kr&category=business&apiKey=ffbbc82f694941a7b0e2d4f4515abcc7"

import { useState } from 'react';
import axios from 'axios';

const App = () => {
  const [data, setData] = useState(null);
  const onClick = async () => {
    try {
      const response = await axios.get(
        "https://newsapi.org/v2/top-headlines?country=kr&apiKey=ffbbc82f694941a7b0e2d4f4515abcc7",
      );
      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)} />}
    </div>
  );
};

export default App;

뉴스 뷰어 UI 만들기

styled-components 설치

**$ yarn add styled-components**

폴더 및 파일 생성

  • src 디렉토리 안에 components 디렉토리 생성한 뒤, 그 안에 NewsItem.js와 NewsList.js 파일 생성
  • NewsItem은 각 뉴스 정보를 보여 주는 컴포넌트
  • NewsList는 API를 요청하고 뉴스 데이터가 들어 있는 배열을 컴포넌트 배열로 변환하여 렌더링

NewsItem 만들기

각 뉴스 데이터가 지니고 있는 정보는 JSON 객체 입니다.

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

NewsItem 컴포넌트는 article 객체를 props로 통째로 받아서 사용 합니다.

components/NewsItems.js

  • rel="noopener noreferrer"<a> 태그에서 사용되는 보안 속성입니다. 이 속성은 다음과 같은 이유로 사용됩니다.
  • noopener 속성은 하이퍼링크를 클릭하여 새 창이나 새 탭이 열릴 때, 새로 열리는 페이지가 이전 페이지(window.opener)를 참조하지 못하도록 합니다. 이전 페이지를 참조할 경우, 보안상 문제가 될 수 있기 때문에, 이를 방지하기 위해 noopener 속성을 사용합니다.
  • noreferrer 속성은 이전 페이지의 URL이 새로 열리는 페이지에 전달되지 않도록 하는 역할을 합니다. 이 속성을 사용하지 않으면, 이전 페이지의 URL이 새 페이지에 노출될 수 있습니다.

따라서 rel="noopener noreferrer"noopenernoreferrer 속성을 모두 적용하는 것으로, 하이퍼링크를 클릭하여 새 창이나 새 탭이 열리는 경우, 이전 페이지를 참조하지 못하도록 하고, 이전 페이지의 URL이 새 페이지에 전달되지 않도록 합니다. 이는 보안성을 높이기 위한 방법 중 하나입니다.

import styled from 'styled-components';

const NewsItemBlock = styled.div`
    display: flex;
    .thumbnail {
			margin-right: 1em;
			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: 3em;
		}
`;
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 만들기

이 컴포넌트는 API를 요청해서 정보를 보여주지만, 아직 데이터를 부러오는 코드를 작성하기 이전 이기 때문에 sampleArticle이라는 객체를 샘플로 만들어서 사용 합니다.

components/NewsList.js

import styled from 'styled-components';
import NewsItem from './NewsItem';

const NewsListBlock = styled.div`
    box-sizing: border-box;
    padding-bottom: 3em;
    width: 768px;
    margin: 0 auto;
    margin-top: 2rem;
    @media screen and (max-width: 768px) {
        width: 100%;
        padding-left: 1em;
        padding-right:1em;
    }
`;

const sampleArticle = {
    title: "제목",
    description: "내용",
    url: "http://google.com",
    urlToImage: "http://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 NewsList from "./components/NewsList";
const App = () => {
  return <NewsList />
};

export default App;

데이터 연동하기

컴포넌트가 화면에 보이는 시점에서 API 요청하려면 useEffect를 사용하여 컴포넌트가 처음 렌더링되는 시점에서 API를 요청하면 됩니다.

여기서 주의할 점은 useEffect에 등록되는 함수에 async를 붙이면 안됩니다.

useEffect 내부에서 async/await를 사용하는 경우, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용 합니다.

추가로, 요청이 대기 중일 때는 loading 값이 true가 되고, 요청이 끝나면 false가 되도록 구현 합니다.

map 함수를 사용하기 전에 articles를 조회하여 해당 값이 현재 null 이 아닌지 검사해야 합니다.

components/NewsList.js

import { 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: 3em;
    width: 768px;
    margin: 0 auto;
    margin-top: 2rem;
    @media screen and (max-width: 768px) {
        witdh: 100%;
        padding-left: 1em;
        padding-right:1em;
    }
`;

const NewsList = () => {
    const [articles, setArticles] = useState(null);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            try {
                const response = await axios.get(
                    "https://newsapi.org/v2/top-headlines?country=kr&apiKey=ffbbc82f694941a7b0e2d4f4515abcc7",
                );
                setArticles(response.data.articles);
            } catch (e) {
                console.log(e);
            }
            setLoading(false);
        };
        fetchData();
    }, []);

		// 대기 중일 때
    if(loading) {
        return <NewsListBlock>대기 중...</NewsListBlock>
    }

    return(
        <NewsListBlock>
            {articles && articles.map(article => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    );
}
export default NewsList;

카테고리 기능 구현하기

뉴스의 카테고리 선택 기능을 구현해 보겠습니다. 뉴스 카테고리는 총 여섯 개 입니다.

  • business(비즈니스)
  • entertainment(연예)
  • health(건강)
  • science(과학)
  • sports(스포츠)
  • technology(기술)

아래의 형태로 구현

카테고리 선택 UI 만들기

components/Categories.js

  • active props는 현재 선택된 카테고리인지 여부를 나타내는 Boolean 값입니다.
import styled, {css} from 'styled-components';
const categories = [
    {
        name: 'all',
        text: '전체보기'
    },
    {
        name: 'business',
        text: '비즈니스'
    },
    {
        name: 'entertainment',
        text: '엔터테인먼트'
    },
    {
        name: 'health',
        text: '건강'
    },
    {
        name: 'sport',
        text: '스포츠'
    },
    {
        name: 'technology',
        text: '기술'
    },
]

const CategoriesBlock = styled.div`
    display: flex;
    padding: 1rem;
    width: 768px;
    margin: 0 auto;
		// 화면 너비가 768픽셀 이하 적용
    @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: .25rem;

    &:hover {
        color: #495057;
    }
    ${props => 
        props.active && css`
        font-weight: 600;
        border-bottom: 2px solid #22bbcf;
        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;
  • NewsList를 Home 상단에 렌더링
import {useState, useCallback} from 'react';
import NewsList from '../components/NewsList';
import Categories from "../components/Categories";

const Home = () => {
    const [category, setCategory] = useState('all');
    const onSelect = useCallback(category => setCategory(category), []);
		// const onSelect = (category) => setCategory(category);

    return (
       <div>
        <Categories category={category} onSelect={onSelect} />
        <NewsList category={category}/>
       </div>
    );
};
export default Home;

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

NewsList 컴포넌트에서 현재 props로 받아 온 category에 따라 카테고리를 지정하여 API를 요청하도록 구현 할 수 있습니다.

주소가 변경 될 때마다 뉴스를 불러와야 하기 때문에 useEffect의 두번째 파라미터에 category를 넣어 줍니다.

components/NewsList.js

import { 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: 3em;
    width: 768px;
    margin: 0 auto;
    margin-top: 2rem;
    @media screen and (max-width: 768px) {
        witdh: 100%;
        padding-left: 1em;
        padding-right:1em;
    }
`;

const NewsList = (props) => {
    const [articles, setArticles] = useState(null);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            try {
                const query = props.category === 'all' ? 'all' : `category=${props.category}`;
                const response = await axios.get(
                    `https://newsapi.org/v2/top-headlines?country=kr&${query}&apiKey=ffbbc82f694941a7b0e2d4f4515abcc7`,
                );
                console.log(response.data.articles);
                setArticles(response.data.articles);
            } catch (e) {
                console.log(e);
            }
            setLoading(false);
        };
        fetchData();
    }, [props.category]);

    if(loading) {
        return <NewsListBlock>대기 중...</NewsListBlock>
    }
    if(!articles) return null;

    return(
        <NewsListBlock>
            {articles.map(article => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    );
}
export default NewsList;

App.js

  • App에서 category 상태를 useState로 관리
  • category 값을 업데이트하는 onSelect 함수 생성
import { useState, useCallback } from 'react';
import Categories from "./components/Categories";
import NewsList from "./components/NewsList";

const App = () => {
  const [category, setCategory] = useState('all');
  // onSelect 함수를 캐싱하여 최적화
  const onSelect = useCallback(category => setCategory(category), []);
	// const onSelect = (category) => setCategory(category);

  return(
    <>
			{/* props로 하위 컴포넌트에서 함수 전달해서 category의 state값을 변경 함 */}
      <Categories category={category} onSelect={onSelect}/>
      <NewsList category={category}/>
    </>
  );
};

export default App;

리액트 라우터 적용하기

기존에는 카테고리 값을 useState로 관리했는데 리액트 라우터의 URL 파라미터를 사용하여 관리해 보겠습니다.

$ yarn add react-router-dom

NewsPage 생성

이번 프로젝트에서 리액트 라우터를 적용할 때 만들어야 할 페이지는 단 하나 입니다.

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

pages/NewsPage.js

import { useParams } from "react-router-dom";
import Categories from "../components/Categories";
import NewsList from "../components/NewsList";

const NewsPage = () => {
	const params = useParams();
	// 카테고리가 선택되지 않았으면 기본값 all로 사용
	const category = params.category || 'all';
	return (
		<>
			<Categories />
			<NewsList category={category} />
		</>
	);
}
export default NewsPage;

App.js

기존 내용을 지우고 Route를 정의 (Home.js 사용하지 않음)

경로에 category URL 파라미터가 없어도 NewsPage 보여줘야 하고, category가 있어도 NewsPage를 보여줘야 하기 때문에 Route 컴포넌트를 두 번 사용 합니다.

import { BrowserRouter as Router, Route, Routes} from "react-router-dom";
import NewsPage from "./pages/NewsPage";

const App = () => {
  return (
    <Router>
      <Routes>
      <Route path="/" element={<NewsPage />} />
      <Route path="/:category" element={<NewsPage />} />
      </Routes>
    </Router>
  );
}
export default App;

Categories에서 기존의 onSelect 함수를 호출하여 카테고리를 선택하고, 선택된 카테고리에 다른 스타일을 주는 기능을 NavLink로 대체 합니다.

👉 NavLink 컴포넌트는 링트에서 사용하는 경로가 **현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트** 입니다. 는 의 special version으로, 특정 링크에 스타일을 넣어 줄 수 있습니다.
<NavLink to="/article/1" style={({isActive}) => (isActive ? activeStyle : undefined)}

div, a, button,input처럼 일반 HTML 요소가 아닌 특정 컴포넌트에 styled-components를 사용할 때는 styled(컴포넌트이름)``과 같은 형식을 사용 합니다.

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: 'sport',
        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}
                    className={({ isActive }) => (isActive ? 'active' : undefined)}
                    to={c.name === 'all' ? '/' : `/${c.name}`} >
										{c.text}
                </Category>
            ))}
        </CategoriesBlock>
    );
};

export default Categories;
profile
FE 개발자🌱

0개의 댓글