본문 바로가기
💻CODING/react. vue

[react] react로 api 이용하여 뉴스 사이트 만들기

by 코딩하는 갓디노 2021. 4. 19.

react api 서버 통신

 

newsapi에서 제공하는 API를 사용하여
최신 뉴스를 불러와
블로그 형태의 뉴스 사이트를 만들겠습니다. 

 

예제는 길벗의 리액트를 다루는 기술을 보면서 공부한 예제입니다.

 

화면 결과

 

 axios로 뉴스 api 호출 준비(ft. promise)

아래의 사이트에 회원 가입 후에 받은 각자의 API 키를 이용하여 데이터에 접근할 수 있습니다. 

https://newsapi.org/

 

News API – Search News and Blog Articles on the Web

Get JSON search results for global news articles in real-time with our free News API.

newsapi.org

발급받은 API 키를 가지고 나중에 API를 요청할 때 API 주소의 쿼리 파라미터를 넣어서 사용합니다. 

 

폴더 구조

 

뉴스 사이트 디자인

디자인은  styled-components 라이브러리를 사용합니다.

yarn add styled-components

 

react router 설치

npm i react-router-dom 
//또는 
yarn add react-router-dom

 

index.js - router 적용

react-router-dom 적용

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')
);

 

pages > NewPage.js

react router의 url 파라미터를 사용하여 관리

url 파라미터를 사용할 때는 라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조합니다.

match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어있습니다.

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 (
        <div>
            <Categories />
            <NewsList category={category} />
        </div>
    );
};

export default NewsPage;

 

App.js

route 정의

import React from 'react';
import { Route } from 'react-router-dom';
import NewsPage from './pages/NewsPage';

const App = () => {
  return (
    <div>
      <Route path='/:category?' component={NewsPage} />;
    </div>
  );
};

export default App;

 

components > Categories.js - 카테고리 선택 UI 세팅, NavLink 사용하기

NavLink로 만들어진 Category 컴포넌트에 to 값은 '/카테고리 이름'으로 설정해 주었습니다.

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)`//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 (
    <div>
      <CategoriesBlock>
        //NavLink
        {categories.map(item => (
          <Category key={item.name} 
          activeClassName='active' 
          exact={item.name === 'all'} 
          to={item.name === 'all' ? '/' : `${item.name}`}
          >{item.text}</Category>
        ))}
      </CategoriesBlock>
    </div>
  );
};

export default Categories;

 

components > NewsList.js - API 호출 

props로 받아온 category에 따라 각 category별 API 호출

주의할 점: map 함수를 사용하여 컴포넌트 배열로 변환할 때,  map 함수를 사용하기 전에 꼭 !articles를 조회하여 해당 값이 현재 null이 아닌거 검사해야 합니다. 

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

useEffect에서 usePromise 커스텀 hook으로 리팩토링

import React, { useState, useEffect } from 'react';
import NewsItem from './NewsItem';
import styled from 'styled-components';
import axios from 'axios';
import usePromise from '../lib/usePromise';

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 sample = {
//     title: 'title',
//     desc: 'story',
//     url: 'https://google.com',
//     urlToImg: 'https://via.placeholder.com/160',
// }

const NewsList = ({ category }) => {
    const [loading, response, error] = usePromise(() => {
        const query = category === 'all' ? '' : `&category=${category}`;
        const url = `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=zzz`;
        return axios.get(url);
    }, [category]);
    //const [articles, setArticles] = useState(null);
    //const [loading, setLoading] = useState(false);

    // useEffect(() => {
    //     setLoading(true);
    //     const query = category === 'all' ? '' : `&category=${category}`;
    //     const url = `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=xxxx`;
    //     axios.get(url)
    //         .then(res => {
    //             console.log(res.data.articles);
    //             setArticles(res.data.articles)
    //         })

    //         .catch(err => console.log(err))
    //     setLoading(false);
    // }, [category]);


    if (loading) {
        return <NewsListBlock>Loading...</NewsListBlock>
    }

    if (!response) { //아직 articles 값이 설정되지 않았을때,
        return null;
    }

    if (error) {
        return <NewsListBlock>error!</NewsListBlock>
    }

    const { articles } = response.data;

    return (
        <div>
            {/* <NewsListBlock>
                <NewsItem article={sample} />
                <NewsItem article={sample} />
                <NewsItem article={sample} />
            </NewsListBlock> */}

            {articles ? <NewsListBlock>
                {articles.map(item => <NewsItem article={item} />)}
            </NewsListBlock> : null}
        </div>
    );
};

export default NewsList;

 

components > NewsItem.js - 최하위 컴포넌트, 화면 디자인 기능

import React from 'react';
import styled from 'styled-components';

const NewsItemBlock = styled.div`
  display: flex;
  margin-top: 3rem;
  
  .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 (
    <div>
      <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} arget='_blank' rel='noopener noreferrer'>
              {title}
            </a>
          </h2>
          <p>{description}</p>

        </div>
      </NewsItemBlock>
    </div>
  );
};


export default NewsItem;

 

lib > usePromise.js - 대기 중/완료/실패에 대한 상태 관리

promise의 대기 중, 완료, 실패 결과에 대한 상태를 관리하며, usePromise의 의존 배열 deps를 파라미터로 받아 옵니다.

파라미터로 받아 온 deps 배열은 usePromise 내부에서 사용한 useEffect의 의존 배열로 설정됩니다.

usePromise를 사용하며 NewsList에서 대기 중 상태 관리와 useEffect 설정을 직접 하지 않아도 되므로 코드가 간결해 집니다.

import { useState, useEffect } from 'react';

export default function usePromise(promiseCreator, deps) {
    const [loading, setLoading] = useState(false);
    const [resolved, setResolved] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        const process = async () => {
            setLoading(true);
            try {
                const resolved = await promiseCreator();
                setResolved(resolved);
            } catch (e) {
                setError(e);
            }
            setLoading(false);
        };
        process();

    }, deps);

    return [loading, resolved, error];
}
반응형

댓글