관리 메뉴

거니의 velog

(3) 외부 API를 연동하여 뉴스 뷰어 만들기 3 본문

React/React_뉴스 뷰어 앱 만들기

(3) 외부 API를 연동하여 뉴스 뷰어 만들기 3

Unlimited00 2023. 12. 12. 18:32

5. 데이터 연동하기

* 이제 NewsList 컴포넌트에서 이전에 연습 삼아 사용했던 API를 호출해 보도록 하자. 컴포넌트가 화면에 보이는 시점에 API를 요청해 볼 것이다. 이때 useEffect를 사용하여 컴포넌트가 맨 처음 렌더링되는 시점에 API를 요청하면 된다. 여기서 주의할 점은 useEffect에 등록하는 함수에 async를 붙이면 안 된다는 것이다. useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문이다.

* 따라서 useEffect 내부에서 async/await 를 사용하고 싶다면, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용해 주어야 한다.

* 추가로 loading이라는 상태도 관리하여 API 요청이 대기 중인지 판별할 것이다. 요청이 대기 중일 때는 loading 값이 true가 되고, 요청이 끝나면 loading 값이 false가 되어야 한다.

[NewsList.js]

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

const NewsListBlock = styled.div`
  box-sizing: border-box;
  padding-bottom: 3rem;
  width: 768px;
  margin: 0 auto;
  margin-top: 2rem;
  @media all 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=8aa6de08f72745be9bbb2a4e91ef45ae',
        );
        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 함수가 없기 때문에 렌더링 과정에서 오류가 발생한다. 그래서 애플리케이션이 제대로 나타나지 않고 흰 페이지만 보이게 된다.

* 이제 뉴스 정보가 잘 보이는지 확인해 보자.

- http://localhost:3000/

실제 뉴스 보여 주기

* 뉴스가 잘 보이는 것을 확인할 수 있다.


6. 카테고리 기능 구현하기

* 이번에는 뉴스의 카테고리 선택 기능을 구현해 보겠다. 뉴스 카테고리는 총 여섯 개이며, 다음과 같이 영어로 되어 있다.

- business (비즈니스)

- entertainment (연예)

- health (건강)

- science (과학)

- sports (스포츠)

- technology (기술)

* 화면에 카테고리를 보여 줄 때는 영어로 된 값을 그대로 보여 주지 않고, 다음 그림처럼 한글로 보여 준 뒤 클릭했을 때는 영어로 된 카테고리 값을 사용하도록 구현할 것이다.


(1) 카테고리 선택 UI 만들기

* 먼저 components 디렉터리에 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 all 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;

* 위 코드에서는 categories 라는 배열 안에 name 과 text 값이 들어가 있는 객체들을 넣어 주어서 한글로 된 카테고리와 실제 카테고리 값을 연결시켜 주었다. 여기서 name은 실제 카테고리 값을 가리키고, text 값은 렌더링할 때 사용할 한글 카테고리를 가리킨다.

* 다 만든 컴포넌트는 App에서 NewsList 컴포넌트 상단에 렌더링하자.

[App.js]

import React from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';

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

export default App;

* 다음과 같이 상단에 카테고리 목록이 나타나는가?

카테고리 목록 보여주기

* 이제 App에서 category 상태를 useState로 관리하겠다. 추가로 category 값을 업데이트하는 onSelect 라는 함수도 만들어 줄 것이다. 그러고 나서 category 와 onSelect 함수를 Categories 컴포넌트에게 props 로 전달해 주자. 또한, category 값을 NewsList 컴포넌트에게도 전달해 주어야 한다.

[App.js]

import React, { 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 (
    <>
      <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 all 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;

* 다음과 같이 선택된 카테고리가 청록색으로 보이는가? 다른 카테고리도 클릭하면 잘 선택된다.

카테고리 선택 시 active 스타일 적용


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

* 지금은 뉴스 API를 요청할 때 따로 카테고리를 선택하지 않고 뉴스 목록을 불러오고 있다. NewsList 컴포넌트에서 현재 props로 받아온 category에 따라 카테고리를 지정하여 API를 요청하도록 구현해 보자.

[NewsList.js]

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

const NewsListBlock = styled.div`
  box-sizing: border-box;
  padding-bottom: 3rem;
  width: 768px;
  margin: 0 auto;
  margin-top: 2rem;
  a {
    color: #333;
  }
  @media all 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=8aa6de08f72745be9bbb2a4e91ef45ae`,
        );
        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 값이 all 이라면 query 값을 공백으로 설정하고, all이 아니라면 "&category=카테고리" 형태의 문자열을 만들도록 했다. 그리고 이 query를 요청할 때 주소에 포함시켜 주었다.

* 추가로 category 값이 바뀔 때마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열(두 번째 파라미터로 설정하는 배열)에 category를 넣어 주어야 한다.

* 만약 여러분이 이 컴포넌트를 클래스형 컴포넌트로 만들게 된다면 componentDidMount와 componentDidUpdate에서 요청을 시작하도록 설정해 주어야 하는데, 함수형 컴포넌트라면 이렇게 useEffect 한 번으로 컴포넌트가 맨 처음 렌더링될 때, 그리고 category 값이 바뀔 때 요청하도록 설정해 줄 수 있다.

* 여기까지 작업을 마쳤다면 브라우저를 열어서 다른 카테고리를 선택해 보자. 카테고리에 따른 뉴스가 잘 나타나는가?

카테고리 선택 기능 구현