📖 리액트/리액트를 다루는 기술

14장 외부 API를 연동하여 뉴스 뷰어 만들기

놀러와요 버그의 숲 2022. 2. 14. 00:49
728x90
반응형

이 글은 『리액트를 다루는 기술』(개정판/ 김민준 저 / 길벗 출판사)이라는 책을 참고하여 썼습니다.

 

학습범위: p354 ~p390

 

 

14.1 비동기 작업의 이해

 

 

동기: 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업 불가능.

 

비동기: 동시에 여러가지 요청 처리 가능. 기다리는 과정에서 다른 함수 호출 할 수 있음.

 

 

 

 

14.2 axios로 API 호출해서 데이터 받아오기

 

axios

: 현재 가장 많이 사용되고 있는 자바스크립트 HTTP 클라이언트이다.

이 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 처리한다는 점이다.

 

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

function App() {
  const [data, setData] = useState(null);
  const handleBtnAxios = () => {
    axios
      .get("https://jsonplaceholder.typicode.com/todos/1")
      .then((response) => {
        setData(response.data);
      });
  };

  return (
    <div>
      <div>
        <button onClick={handleBtnAxios}>불러오기</button>
      </div>
      {data && (
        <textarea
          rows={7}
          value={JSON.stringify(data, null, 2)}
          readOnly={true}
        />
      )}
    </div>
  );
}

export default App;

 

새로 알게된 부분

 

axios.get 함수는 파라미터로 전달된 주소에 GET 요청을 해준다.

이에 대한 결과는 .then을 통해 비동기적으로 확인할 수 있다.

 

 

(아 저렇게 쓰는거구나를 알게되었다. axios.get()안에는 url 주소가 들어가고, .then()안에는 결과물이 들어가는가보다.

그리고 useState에서 정의해준 setData(response.data)이런식으로 들어가게 해주나보다. 그리고 쓸때는 {data}를 jsx로 써주는 거고!)

 

 

JSON.strigify(data,null,2)

 

첫번쨰 인자는 JSON 문자열로 변환할 값

두번째 인자는 null이면 객체의 모든 속성들이 JSON 문자열 결과에 포함된다.

세번째 인자는 공백으로 사용되는 space의 수를 나타낸다.

 

readOnly={true}는 읽기 전용임을 명시한다.

 

 

 

async/await으로 바꾸기

 

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

function App() {
  const handleBtnAxios = 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={handleBtnAxios}>불러오기</button>
      </div>
      {data && (
        <textarea
          rows={7}
          value={JSON.stringify(data, null, 2)}
          readOnly={true}
        />
      )}
    </div>
  );
}

export default App;

 

(오! 저렇게 쓰는구나 async/ await 쓸때는 try/catch구문 써주고, async는 ()앞에 붙여주고,  try구문안에 const response= await axios.get(url) 써주고,setData(response.data)로 써주는 구나. 그리고 catch구문은 catch(e){ console.log(e)} 이런식으로 쓰는군)

 

 

 

14.4 뉴스뷰어 UI 만들기

 

//components/NewsItem.js

import styled from "styled-components";

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;

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

 

(

const NewsItem= ({article) => {

const {title, description, url, urlToImage} = article;

...

이런식으로 받아오는구나. 맞아맞아 )

//components/NewsList.js

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

const NewsList = () => {
  return (
    <NewsListBlock>
      <NewsItem article={sampleArtile} />
      <NewsItem article={sampleArtile} />
      <NewsItem article={sampleArtile} />
      <NewsItem article={sampleArtile} />
      <NewsItem article={sampleArtile} />
      <NewsItem article={sampleArtile} />
    </NewsListBlock>
  );
};
export default NewsList;

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 sampleArtile = {
  title: "제목",
  description: "내용",
  url: "https://google.com",
  urlToImage: "https://via.placeholder.com/160",
};

 

 

 

& +& {
margin-top:3rem
}

이부분 처음 알게 되었는데, nav에서 서로간에 간격 띄울 때 좋다고 생각한다.

 

 

 

 

14.5 데이터 연동하기

 

import { useState, useEffect } from "react";
import styled from "styled-components";
import NewsItem from "./NewsItem";
import axios from "axios";

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

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

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

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

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

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

 

 

useEffect에 등록하는 함수에 async를 붙이면 안된다. useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문이다.

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

(useEffect는 함수 api 한번만 불러오려고 쓰나봐. 마지막쯤에 fetchData() 이런식으로 불러주는구나)

 

 

 

 

14.6.1 카테고리 선택 UI 만들기

 

//src/App.js

import { useState, useCallback } from "react";
import NewsList from "./components/NewsList";
import Categories from "./components/Categories";

function App() {
  const [category, setCategory] = useState("all");
  const onSelect = useCallback((category) => setCategory(category), []);

  return (
    <>
      <Categories category={category} onSelect={onSelect} />
      <NewsList category={category} />
    </>
  );
}

export default App;

 

useCallback은 메모리제이션된 함수를 반환하는 Hook이다.

 

 

//src/components/Categories.js

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 Categories = ({ onSelect, category }) => {
  return (
    <CategoriesBlock>
      {categories.map((item) => (
        <Category
          key={item.name}
          active={category === item.name}
          onClick={() => onSelect(item.name)}
        >
          {item.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
};

export default Categories;

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;
  }
  ${(props) =>
    props.active &&
    css`
      font-weight: 600;
      border-bottom: 2px solid #22b8cf;
      color: #22b8cf;
      &:hover {
        color: #3bc9db;
      }
    `}
  & + & {
    margin-left: 1rem;
  }
`;

 

이 코드는 이해가 잘 안간다 ㅠㅠ

=> ${(props) 에는 Category 컴포넌트안에 있는 모든 props의 정보가 들어가있다. 

active라는 props를 설정했다.

 

overflow-x:auto;

 

visible과 유사. 콘텐츠를 자르지 않고 넘칠 경우 스크롤 제공

 

white-space:pre;

 

공백처리. 연속 공백 유지. 줄바꿈은 개행 문자와 <br>요소에서만 일어남.

 

 

 

 

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

 

//components/NewsList.js

import NewsItem from "./NewsItem";
import axios from "axios";

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 response = axios.get(
          `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=6f1993113ca34824ab68673e9bdca9ee`
        );
        setArticles((await response).data.articles);
      } catch (e) {
...

 

category 값이 all이라면 query 값을 공백으로 설정하고, all이 아니라면 "&category=카테고리" 형태의 문자열을 만들도록 했다.

 

추가로 category 값이 바뀔 때 마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존 배열에 category를 넣어주었다.

 

 

(아 query를 새로 정의하고, 삼항 연산자를 썻구나.)

 

 

 

최종 결과물