본문 바로가기
Project/[클론 코딩] 뉴스 뷰어

[클론 코딩] 뉴스 api 연동

by Bam_t 2022. 1. 2.
728x90

지난번에 전체적인 UI를 구상했습니다. 이제는 newsapi를 연동해서 본격적으로 완성시켜볼 차례입니다.


1. api 연동

컴포넌트가 렌더링 될 때마다 작업을 수행하는 useEffect를 통해 api를 연동할 예정입니다. 주의할 점은 이전에 api 연동을 위해 async로  useEffect에 콜백 함수를 등록할 때 여기에는 async를 붙이면 안된다는 점 입니다. 그 이유는 useEffect를 실행했을 때 뒷정리 함수를 반환하기 때문입니다. 그래서 지금처럼 useEffect에 비동기 처리가 필요하다면 내부에 함수를 하나 더 만들어서 async 키워드를 붙여서 이용해야 합니다.

2021.11.16 - [Programming/React] - [React] Hooks - useEffect

 

[React] Hooks - useEffect

1. useEffect useEffect는 컴포넌트가 렌더링될 때마다 특정 작업을 하도록 만드는 Hook입니다. 라이프 사이클 메소드인 componentDidMount나 componentDidUpdate, componentWillUnmount와 비슷하게 작동합니다...

bamtory29.tistory.com

 

그러면 이제 <NewsList> 컴포넌트가 api를 연동해서 렌더링할 수 있도록 수정해보겠습니다. 우선 전체 코드입니다.

import React, {useEffect, useState} 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 sampleArticle = {
    title: '제목',
    description: '내용',
    url: 'https://bamtory29.tistory.com/',
    urlToImage: 'https://via.placeholder.com/160',
}

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

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);

            try {
                const res = await axios.get(
                    'https://newsapi.org/v2/top-headlines?country=kr&apiKey=d6e76706631d4059b20e981f9fc9b930'
                );

                setArticles(res.data.articles);
            }
            catch (err) {
                console.log(err);
            }
            setLoading(false);
        };
        fetchData();
    }, []);

    if (loading) {
        return <NewsListBlock>로딩 중...</NewsListBlock>;
    }

    if (!articles) {
        return null;
    }

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

export default NewsList;

 

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

articles state는 api로 부터 받아온 기사들을 저장하는 객체 배열입니다. 처음에는 연동이전이라 당연히 비어있으므로, 초깃값을 null로 설정했습니다.

loading은 api가 연동되어서 기사를 받아오고 있는지를 판별하는 bool형 state입니다. 기사를 받아오는 중이라면 true, 기사를 받아오지 않았거나, 받아오기가 완료되었다면, false값을 가집니다.

 

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);

            try {
                const res = await axios.get(
                    'https://newsapi.org/v2/top-headlines?country=kr&apiKey=d6e76706631d4059b20e981f9fc9b930'
                );

                setArticles(res.data.articles);
            }
            catch (err) {
                console.log(err);
            }
            setLoading(false);
        };
        fetchData();
    }, []);

    if (loading) {
        return <NewsListBlock>로딩 중...</NewsListBlock>;
    }

    if (!articles) {
        return null;
    }

NewsList 컴포넌트가 렌더링 되면 api로 부터 뉴스 정보를 받아오는 useEffect Hook 입니다. useEffect가 하는 일은 fetchData();를 호출하는 일 하나입니다.

fetchData() 함수를 살펴보겠습니다. 컴포넌트가 렌더링되면, api로 부터 기사를 받아오려고 하니 loading을 true로 변경해줍니다.

그리고 try~catch 구문을 통해 newapi 홈페이지로부터 뉴스 정보를 http 응답으로 받아옵니다. 성공하면 받아온 응답의 data의 기사들을 articles state에 넣어주고 실패했다면 오류를 출력해줍니다.

작업이 완료되었다면 다시 로딩 상태를 false로 변경해줍니다.

 

    if (loading) {
        return <NewsListBlock>로딩 중...</NewsListBlock>;
    }

    if (!articles) {
        return null;
    }

로딩이 true, 로딩 되는 동안 로딩 중이라는 문구를 표시해줍니다.

그리고 articles에 어떠한 값이 없다면 null을 반환합니다. 값이 없는데 null을 반환하지 않는다면 잠시 컴포넌트 렌더링 과정에서 오류가 발생합니다.

 

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

return을 통해 렌더링 할 때 map() 함수를 이용해서 컴포넌트 배열로 만들어줍니다. 

실행했을 때 다음과 같이 뉴스들이 잘 나타나면 성공한 것 입니다.

 

 

 

2. 카테고리

뉴스 api에 카테고리가 있었던 것 기억하시나요? 그 카테고리를 이용해서 탭을 만들고 해당 카테고리에 대한 뉴스만을 나오게 하는 기능을 더하려고 합니다.

카테고리 컴포넌트를 만들어보겠습니다. 다음은 전체 코드입니다.

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;

 

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

카테고리의 항목별 이름과 표시될 text를 저장한 객체 배열입니다. 1개의 전체보기와 6개의 서브 카테고리입니다.

 

const Categories = () => {
    return (
        <CategoriesBlock>
            {categories.map(c => (
                <Category key={c.name}>{c.text}</Category>
            ))}
        </CategoriesBlock>
    );
};

카테고리 컴포넌트 본문입니다. 카테고리 블록안에, 카테고리 배열의 요소들을 보여줍니다. text에 써진 문구가 우리 눈에 보이게 되고, 처리할 때는 key를 통해 접근합니다.

App.js에 카테고리 컴포넌트를 추가합니다.

import React, {Fragment} from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';

const App = () => {
    return (
        <Fragment>
            <Categories/>
            <NewsList/>
        </Fragment>
    );
};

export default App;

이제 실행하면 상단에 카테고리란이 보이시나요? (엔터테인먼트 외국어 표기 오타 죄송합니다.)

 

이번에는 우리가 선택한 카테고리의 값이 무엇인지 state로 관리해보겠습니다.

App.js를 다음과 같이 수정합니다.

import React, {Fragment, useCallback, useState} from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';

const App = () => {
    const [category, setCategory] = useState('all');
    const onSelect = useCallback(category => setCategory(category), []);
    
    return (
        <Fragment>
            <Categories category={category} onSelect={onSelect}/>
            <NewsList category={category}/>
        </Fragment>
    );
};

export default App;

category 상태는 useState를 통해 우리가 어떤 카테고리를 선택했는지 추적합니다. 초기값은 전체보기인 'all'입니다. 그리고 카테고리를 선택했을 때 동작하는 함수인 onSelect도 추가했습니다. 카테고리 컴포넌트의 카테고리를 선택하면 onSelect가 동작해 상태가 클릭한 카테고리로 변경되게 합니다.

다시 카테고리 컴포넌트로 돌아와서 내용을 수정합니다. Category styled-component를 다음과 같이 수정해주세요.

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

추가된 구문은 props를 전달해서 해당 props가 active 되었을 때 특정 스타일을 주는 styled-component 구문입니다. 카테고리에선, 카테고리가 선택되면 그에 대한 효과를 주도록 되도록 만들었습니다.

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

App.js에서 전달받은 props인 onSelect를 카테고리 컴포넌트의 onClick 이벤트로 설정해줍니다. 그리고 그에 따른 스타일이 나타나도록 만들어줍니다. 이 과정이 끝나고 뉴스 뷰어 앱을 보면 선택된 카테고리가 하늘색 계열로 나타나게 됩니다.

하지만 아직 다른 카테고리를 눌렀을 때 해당 카테고리 뉴스만 보여주고 있지는 않습니다. 그러므로 기능을 구현해보겠습니다. NewsList 컴포넌트를 수정하겠습니다.

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

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);

            try {
                const query = category === 'all' ? '' : `&category=${category}`;
                const res = await axios.get(
                    `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=d6e76706631d4059b20e981f9fc9b930`
                );

                setArticles(res.data.articles);
            }
            catch (err) {
                console.log(err);
            }
            setLoading(false);
        };
        fetchData();
    }, [category]);

    if (loading) {
        return <NewsListBlock>로딩 중...</NewsListBlock>;
    }

    if (!articles) {
        return null;
    }

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

 

변경 된 부분은 두 가지 입니다. 우선 컴포넌트의 props로 category를 전달해주었습니다.

const NewsList = ({category}) =>

 

다른 부분은 api요청 부분입니다. all이라면 query를 비우고, all외에 다른 카테고리라면 카테고리 name을 쿼리로 받아오도록 했습니다. 그리고 axios.get 요청도 자바스크립트 쿼리문을 위해 백틱 문자열 템플릿으로 교체하고 apikey 입력 이전에 쿼리가 들어가게 했습니다.

const query = category === 'all'? '':`&category=${category}`;
const res = await axios.get(
  `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=d6e76706631d4059b20e981f9fc9b930`
);

 

그리고 category를 선택하고 그때마다 컴포넌트를 업데이트 해야하므로 useEffect의 두번째 인자에 category props를 넣어줍니다.

useEffect(() => {
        const fetchData = async () => {
            setLoading(true);

            try {
                const query = category === 'all' ? '' : `&category=${category}`;
                const res = await axios.get(
                    `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=d6e76706631d4059b20e981f9fc9b930`
                );

                setArticles(res.data.articles);
            }
            catch (err) {
                console.log(err);
            }
            setLoading(false);
        };
        fetchData();
    }, [category]);

 

다음 사진처럼 카테고리 탭에 따라 다른 뉴스들이 나타난다면 성공적인 카테고리별 api연동입니다.

728x90

댓글