[React] News API를 연동하여 뉴스 뷰어 만들기

soyeon·2022년 2월 27일
0
post-thumbnail

👻들어가며

<리액트를 다루는 기술> 14장 외부 API를 연동하며 뉴스 뷰어 만들기를 공부하면서 정리한 내용입니다📚
중간에 모르는 것들은 따로 서치를 해서 공부했습니다.

💥news API의 단점(필독)


홈페이지에 들어가보면 무료 개발자 플랜은 로컬호스트 같은 개발 환경에만 적용되고 외부 환경에서 적용하려면 돈을 내야함... 실화냐?
그래서 깃헙 페이지로 퍼블리싱하면 아무것도 안뜸..

✨ 앱의 컴포넌트 구조



props까지 보면 이런 느낌입니다.

🖋 NewsItem

  • 뉴스 항목을 담당하는 컴포넌트입니다.
  • 기사 데이터로 article을 props받아와서 title, description, url, urlToImage을 각 태그에 넣어줍니다.
function NewsItem({article}){
    // article을 prop로 받아온다.
    // article에는 'title', 'description', 'url', 'urlToImage'의 정보가 들어있음
    const { title, description, url, urlToImage} = article;
    //구조 분해를 이용해서 article.title → title로 할당하기
    return (
        <NewsItemBlock>
            {/* urlToImage가 있는 경우: 썸네일 요소 형성 */}
            {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>
    )
}

스타일드 컴포넌트

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;
}
`;

🖋 NewsList

리팩토링 전

  • 해당 페이지의 카테고리를 props로 받아옵니다.
  • axios를 이용해 API를 호출하고 각 데이터를 map 메서드를 통해 NewsItem 컴포넌트로 가공합니다.
  • loading State를 만들어 loading = true일 경우엔 대기중으로 표시하고 false일 때는 데이터를 표시합니다.
  • async와 await에 관한 포스트는 이쪽으로
    NewsList.js
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import axios from '../node_modules/axios/index';
import NewsItem from './NewsItem';

// 카테고리를 props로 받아옴
function NewsList({category}) {
  // 뉴스들 데이터를 담아놓는 곳
  const [articles, setArticles] = useState(null);
  // 로딩 상태를 담아놓는 곳
  const [loading, setLoading] = useState(false);
  // api 주소에 카테고리 값으로 끼워넣을 상수 생성
  // category가 'all'이면 카테고리 설정x, 아니면 &category=${category}
  const query = category === 'all' ? '' : `&category=${category}`;
  
//useEffect: 처음 렌더될 때 뉴스들의 데이터를 axios로 받아옴 
  useEffect(()=>{
      //async를 사용하는 함수 따로 선언
      const fetchData = async() => {
        //지금은 로딩중!
          setLoading(true);
        //아래의 코드를 츄라이
          try {
            //axios로 API를 호출한다.
              const response = await axios.get(
                  `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`
              );
            //articles에 데이터를 담는다.
              setArticles(response.data.articles)
          } catch(err) {
            //오류가 생길 경우엔
            // 콘솔로 오류 찍어주세요
              console.log(err)
          }
        // 로딩끝♥
          setLoading(false);
      }
      //fetchdata 사용
      fetchData();
  },[category])

  // 로딩중일 때
  if (loading) {
    return <NewsListBlock>대기중</NewsListBlock>;
  }
  //아직 article의 값이 없을 때 (null일 때)
  if (!articles) {
    return null;
  }
  return (
    <NewsListBlock>
      //map 메서드를 이용하여 각 기사들을 NewsItem 컴포넌트로 생성한다.
      {articles.map((article) => (
        //각 기사의 내용은 article props로 넘김
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
}

커스텀 훅을 만들어서 리팩토링 해보기

axios를 통해 API를 호출하는 fetchData 함수를 훅의 형태로 커스텀하기

  • 프로젝트의 다양한 곳에서 사용될 수 있도록 하는 함수를 만들 것임!
  • 내용물이 없는 붕어빵틀 같은 거고 함수에 인자(재료)만 집어넣는 느낌이랄까요ㅎㅎ
  • 리팩토링 하기 전의 코드입니다.
    NewsList.js
//useEffect: 처음 렌더될 때 뉴스들의 데이터를 axios로 받아옴 
  useEffect(()=>{
      //async를 사용하는 함수 따로 선언
      const fetchData = async() => {
        //지금은 로딩중!
          setLoading(true);
        //아래의 코드를 츄라이
          try {
            //axios로 API를 호출한다.
              const response = await axios.get(
                  `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`
              );
            //articles에 데이터를 담는다.
              setArticles(response.data.articles)
          } catch(err) {
            //오류가 생길 경우엔
            // 콘솔로 오류 찍어주세요
              console.log(err)
          }
        // 로딩끝♥
          setLoading(false);
      }
      //fetchdata 사용
      fetchData();
  },[category])

usePromise.js

  • 먼저 lib 폴더를 만들어주고 그 안에 usePromise.js 파일을 만들어줍니다.
  • 커스텀 훅인 usePromise()를 만들어줍니다. 이름은 맘대로 지어도 될 듯...

    💟 커스텀 훅 usePromise(데이터를 가져오는 함수, useEffect에 쓰일 의존배열)

import { useEffect, useState } from "react";

// promiseCreator : promise를 만들어주는 함수, 데이터를 가져오는 함수(axios,fetch)가 들어가야한다.
// deps: 의존배열
// 이 두 인자에는 지금 특별한 값이 들어가지 않았다. 고정틀을 위해 임시로 넣어준 것들임.
export default function usePromise(promiseCreator,deps) {
    // 대기중/완료/실패에 대한 상태관리
    const [loading,setLoading] = useState(false); //대기
    const [resolved, setResolved] = useState(null); //완료(데이터)
    const [error, setError] = useState(null); //실패
    // useEffect()로 
    useEffect(()=>{
      //process 함수
        const process = async () => {
            setLoading(true); //로딩중임!!!!💞
            try{
                // promiseCreator() 함수(axois, fetch 등)로 데이터를 가져와라..
              	// await - promiseCreator()가 값을 가져올 때까지 존버하겠다.
                const resolved = await promiseCreator();
                // resolved에 데이터 입력완.
                setResolved(resolved);
            } catch(err) {
                //에러가 생길 경우
                //콘솔로 찍어주세요
                setError(err)
            }
            //로딩끝♥
            setLoading(false);
        };
      //process 실행
        process();
    // eslint-disable-next-line react-hooks/exhaustive-deps
    // ↑ esLint 경고를 막기 위한 주석
    },deps);
    //deps는 useEffect의 의존배열. 특별한 값은 없고 틀을 만들기 위해 임시로 넣어준 값임
    
	//로딩 상태, 데이터 상태, 에러 상태를 반환함
    return[loading,resolved,error]
}

커스텀 훅을 적용시켜보자❤

NewsList.js

function NewsList({ category }) {
  
//usePromise는 loading, resolved, error을 반환하는 함수임.
//구조분해로 깔끔하게 할당해주고
  const [loading, response, error] = usePromise(() => {
    //어이.. 이 밑은 promiseCreator의 코드다.
    // 카테고리의 주소를 정해주는 변수
    const query = category === 'all' ? '' : `&category=${category}`;
   	
    //✨데이터를 가져오는 함수❤(promiseCreator가 뱉는 값)
    return axios.get(
      `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=a5ee1fb9d67341ec941a37c89cfc3283`,
    );
  }, [category]);
  //category가 달라질 때마다 렌더링해야함. 의존 배열에 카테고리 넣어주


  // 대기중일 때
  if (loading) {
    return <NewsListBlock>대기중</NewsListBlock>;
  }
  //아직 response 값이 설정되지 않았을 때(article 값이 null일 때)
  if (!response) {
    return null;
  }
  // 아직 response 값이 설정되지 않을때
  if (error) {
    return <NewsListBlock>에러 발생!</NewsListBlock>;
  }
  // article 값이 잇음 (article 값이 잇음)
  const { articles } = response.data;
  return (
    <NewsListBlock>
      //map으로 데이터 나눠서 컴포넌트 만들어줘
      {articles.map((article) => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
}

스타일드 컴포넌트

복붙ㄱ

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;
  }
`;

🖋 NewsPage

리액트 라우터 적용하기

리액트 라우터 설치 : npm i react-router-dom

index.js에 BrowserRouter 적용

import { BrowserRouter } from '../node_modules/react-router-dom/index';

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

App.js에 라우터 적용

function App() {

  return (
    <Routes>
      //기본 페이지 - 전체보기(all)
      <Route path='/' element={<NewsPage />} />
      //카테고리 페이지 - 카테고리를 누르면 해당 카테고리로 이동
      <Route path='/:category' element={<NewsPage />} />
    </Routes>
  );
}

export default App;

NewsPage 만들기 - URL 파라미터 이용하기(useParams)

function NewsPage() {
    //useParams으로 링크의 URL 파라미터를 이용할 것임
    // .../:category면 category가 URL의 파라미터임
    const params = useParams();
    //카테고리가 선택되지 않았으면 기본값으로 all을 사용~
    const category = params.category || 'all';
    return (
        <>
        //카테고리들
            <Categories />
        //해당 카테고리의 NewsList가 나오도록 category를 props로
            <NewsList category={category} />
        </>
    )
}

🖋 Categories

News API에는 총 6개의 카테고리가 있습니다.

  • 겉으로 보여질 카테고리의 이름을 text로
  • API 주소에 집어넣을 카테고리는 name으로

Categories.js

const categories = [
  {
    name: 'all',
    text: '전체보기',
  },
  {
    name: 'business',
    text: '비즈니스',
  },
  {
    name: 'entertainment',
    text: '엔터테인먼트',
  },
  {
    name: 'health',
    text: '건강',
  },
  {
    name: 'science',
    text: '과학',
  },
  {
    name: 'sports',
    text: '스포츠',
  },
  {
    name: 'technology',
    text: '기술',
  },
];

스타일드 컴포넌트

Categories.js

//카테고리 wrap 스타일
const CategoriesBlock = styled.div`
  display: flex;
  padding: 1rem;
  width: 768px;
  margin: 0 auto;
  @media screen and (max-width: 768px) {
    width: 100%;
    overflow-x: auto;
  }
`;
//카테고리 NavLink
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;
  }
  & + & {
    margin-left: 1rem;
  }
//카테고리가 active일 경우 적용될 클래스: active
  &.active {
    font-weight: 600;
    border-bottom: 2px solid #22b8cf;
    color: #22b8cf;
    &:hover {
      color: #3bc9db;
    }
  }
`;

Categories.js

function Categories() {
  return (
    //카테고리 wrap
    <CategoriesBlock>
      {/* map 메서드로 각 카테고리들(NavLink)을 생성 */}
      {categories.map((c) => (
        //key에는 고유한 이름이 들어가도록 c.name을 쓴다
        <Category key={c.name} 
        //active 상태면 active 클래스를, 아니면 그없
        className={({isActive})=>(isActive ? 'active' : undefined)}
        //NavLink의 주소! 
        //'all'이면 기본페이지로 그 외의 카테고리면 '/카테고리이름'
        to={c.name ==='all' ? '/' : `/${c.name}`}>
          {c.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
}

👻마치며

props 전달... 너무 거슬림


저번 투두리스트 앱도 그렇고 category 데이터를 props로 일일이 전달해주는게 매우 귀찮다.
투두리스트 앱 때는 아예 상단부터 맨 하단의 일정 항목까지 props로 함수와 state를 전달했다...
다음 장에 나올 context API는 이런 불편함을 어느정도 해소해준다고 한다.

📎참고

<리액트를 다루는 기술> - 김민준(벨로퍼트), 길벗

profile
공부중

0개의 댓글