[리액트공부] 14. 외부 API 연동

kkado·2022년 8월 16일
0

리다기

목록 보기
15/16
post-thumbnail

지금까지 배운 것들을 기반으로 뉴스 API를 받아오는 뉴스 뷰어를 만들어 보고자 한다. 여기서 먼저 알아야 할 것이 비동기 작업이다.

14.1 비동기 작업

웹 애플리케이션을 만들면, 서버 쪽의 데이터가 필요할 때에는 Ajax 기법을 사용해서 API를 호출해서 데이터를 수신한다. 이렇듯 서버의 API를 사용해야 할 때는 네트워크 송수진 과정에서 시간이 걸리기 때문에, 작업을 즉시 처리할 수 없다.

즉 데이터 수신이 완료된 후에 그 데이터를 처리하는데, 이것을 비동기적으로 작업을 처리한다고 한다.

만약 작업을 동기적으로 처리하면, 데이터 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 처리할 수 없다. 그리고 요청이 끝나면 다음 작업을 진행할 수 있다. 만약 이를 비동기적으로 처리한다면 웹 애플리케이션이 멈추지 않기 때문에 동시에 여러가지 요청을 처리할 수 있다.

14.1.1 콜백 함수

자바스크립트에서 비동기 작업을 할 때 가장 흔히 사용하는 방법이 바로 콜백 함수 를 이용하는 것이다.

function printMe() {
  console.log("Hello, world!");
}

setTimeout(printMe, 3000);
console.log("대기 중");

3초 뒤에 printMe 함수를 실행하게끔 setTimeout을 해 주었는데 여기서 setTimeout의 인자로 전달해준 printMe 함수를 콜백 함수라고 한다. 해당 함수가 처리된 직후 어떤 작업을 하고 싶다면 콜백 함수를 활용한다.

function increase(number, callback) {
  setTimeout(() => {
    const result = number + 10;
    if (callback) {
      callback(result);
    }
  }, 1000);
}

increase(0, (result) => {
  console.log(result);
});

또다른 예시는 위와 같다. 1초 뒤에 10을 출력하게 된다.

콜백 안에 또 콜백을 넣어서 구현할 수 있는데, 잘 작동은 하지만 겹겹이 싸여있기 때문에 코드의 가독성이 떨어져, 가급적 지양하는 방식이다.

14.1.2 Promise

너무 여러 겹으로 콜백 함수가 형성되지 않게 하기 위한 방안이다.

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    // resolve는 성공, reject는 실패
    setTimeout(() => {
      const result = number + 10;
      if (result > 50) {
        // 50보다 크면 에러 발생시키기
        const e = new Error("NumberTooBig");
        return reject(e);
      }
      resolve(result);
    }, 1000);
  });
  return promise;
}

increase(0)
  .then((number) => {
    // promise에서 resolve된 값은 .then으로 받아올 수 있음
    console.log(number);
    return increase(number);
  })
  .catch((e) => {
    // 도중에 에러가 발생하면 .catch로 알 수 있음
    console.log(e);
  });

위와 같이 .then 을 사용하여 그 다음 작업을 설정할 수 있어 가독성이 좋다는 장점이 있다.
그리고 처음 increase 함수에서 return된 promise를 그 이후 .then 에서 인자로 받아와 사용할 수 있다.

14.1.3 async/await

Promise를 더 쉽게 사용할 수 있게 해준다. 함수의 앞부분에 async 키워드를 추가하고, 함수 내부에서 promise의 앞부분에 await 키워드를 사용한다. 이렇게 하면 promise가 끝날 때까지 기다리고, 결과값을 변수에 담을 수 있다.

async

async function f() {
	return 1;
}

async가 붙은 함수는 항상 promise를 반환한다. promise가 아닌 값을 반환하더라도 resolved promise가 반환된다.

즉, 아래 예시 함수를 호출하면 result가 1인 resolved promise가 반환된다.

async function f() {
  return 1;
}

f().then(alert); // 1

물론 아래와 같이 명시적으로 promise를 반환할 수도 있다.

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

await

await은 async 함수 안에서만 동작한다.

let value = await promise;

await 키워드를 만나면 promise가 처리될 때까지 기다린다.

다음의 예시를 보자.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

f 함수를 호출하였고 result = await promise; 로 promise 값을 result에 넣고자 하였는데, promise가 setTimeout 에 의해 1초 뒤에 반환된다. 따라서 별표 친 부분에서 실행이 중단되었다가 promise가 처리되면 다시 재개되고 alert로 완료 메시지가 나오게 된다.

프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.


이후에 뉴스API 사이트에서 axios를 이용해 데이터를 받아오는데 이전에 fetch API를 사용해 본 경험이 있어서 axios 사용법을 배우지 않고 그냥 fetch를 사용하기로 하였다.

async function getData() {
    const response = await fetch('https://newsapi.org/v2/top-headlines?country=kr&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b');
    const json = await response.json();
    const news = json.articles;
    console.log(news);
    setData(news);
  }

fetch와 async를 이용해 뉴스를 불러오는 데에 성공했다.


14.4 뉴스 뷰어 UI 만들기

이제 styled-component를 이용해서 UI를 깔끔하게 꾸며 볼 것이다.
전체 뉴스를 NewsList 컴포넌트로 배열처럼 만들고, 각각의 뉴스는 NewsItem 컴포넌트로 렌더링해보자.

NewsList.js

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

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://google.com",
  urlToImage: "https://via.placeholder.com/160",
};

const NewsList = () => {
  return (
    <NewsListBlock>
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
    </NewsListBlock>
  );
};
export default NewsList;
import React from "react";
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;
  }
`;

const NewsItem = ({ article }) => {
  return (
    <NewsItemBlock>
      {article.urlToImage && (
        <div className="thumbnail">
          <a href={article.url} target="_blank" rel="noopener noreferrer">
            <img src={article.urlToImage} alt="thumbnail" />
          </a>
        </div>
      )}
      <div className="contents">
        <h2>
          <a href={article.url} target="_blank" rel="noopener noreferrer">
            {article.title}
          </a>
        </h2>
        <p>{article.description}</p>
      </div>
    </NewsItemBlock>
  );
};
export default NewsItem;

먼저 각각의 뉴스를 표시할 NewsItem 컴포넌트를 만든다. 이 컴포넌트는 article을 통째로 props로 전달받는다.

그리고 NewsItem 컴포넌트를 여러 개 렌더링하는 NewsList 컴포넌트를 만든다.

우선 예시로 sampleArticle을 만들어서 넘겨주어서 만들어진 페이지다. 이제 진짜 뉴스를 인자로 넘겨주어 보자.

NewsItem.js

const NewsItem = ({ article }) => {
  return (
    <NewsItemBlock>
      {article.urlToImage && (
        <div className="thumbnail">
          <a href={article.url} target="_blank" rel="noopener noreferrer">
            <img src={article.urlToImage} alt="thumbnail" />
          </a>
        </div>
      )}
      <div className="contents">
        <h2>
          <a href={article.url} target="_blank" rel="noopener noreferrer">
            {article.title}
          </a>
        </h2>
        <p>{article.description}</p>
      </div>
    </NewsItemBlock>
  );
};
export default NewsItem;

NewsList.js

const NewsList = ({articles}) => {
  return (
    <NewsListBlock>
      {articles.map(
        (article, index) => (
          <NewsItem article={article} key={index} />
        )
      )}
    </NewsListBlock>
  );
};

결과

썸네일 이미지, 제목을 포함해 간략한 내용까지 잘 나오는 것을 볼 수 있고, 클릭하면 실제 기사 원문으로 넘어간다.


카테고리 분류 만들기

우리가 사용한 뉴스 API 사이트에는 6개의 세부 카테고리를 제공한다.
그리고 API 호출 url에 카테고리 정보가 삽입된다.

business 카테고리
https://newsapi.org/v2/top-headlines?country=kr&category=business&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b

entertainment 카테고리
https://newsapi.org/v2/top-headlines?country=kr&category=entertainment&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b

카테고리 x
https://newsapi.org/v2/top-headlines?country=kr&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b

다른 URL은 같고 category 쿼리스트링만 달라진 것을 볼 수 있다. 즉 우리가 임의로 선택하여 각각 다른 카테고리의 뉴스를 보여주게끔 만들 수 있을 것이다.

먼저 카테고리 컴포넌트인 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 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;

styled-component를 이용해서 CategoriesBlockCategory를 쉽게 꾸밀 수 있다.
그리고 App.js에 추가하면 된다.


이런 식으로 위에 카테고리 선택 메뉴가 나타났다. 이제 클릭했을 때 카테고리 값을 지정해주는 useState를 App.js 에서 관리하자.

${(props) =>
    props.active &&
    css`
      font-weight: 600;
      border-bottom: 2px solid #22b8cf;
      color: #22b8cf;
      &:hover {
        color: #3bc9db;
      }
    `}

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;
const App = () => {
  const [data, setData] = useState([]);
  const [category, setCategory] = useState("all");

  const onSelect = useCallback(category => setCategory(category), []);

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

App.js에서 만든 onSelect 함수는 category state를 수정한다.
이 함수를 Categories 컴포넌트로 그대로 넘겨주고, 그 변수인 category 역시 넘겨 준다.

그리고 Categories 컴포넌트에서는 넘겨받은 category 와 각각의 카테고리의 name이 같은지 확인 (즉 선택한 카테고리인지) 하고, 맞다면 active 프로퍼티를 참으로 한다.

이제 카테고리별로 다른 API 링크를 불러오기만 하면 된다.
NewsList.js

const NewsList = ({ category }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function f() {
      const response = await fetch(
        `https://newsapi.org/v2/top-headlines?country=kr${
          category === "all" ? "" : `&category=${category}`
        }&apiKey=4144e3d02e2f425986ab4e18ae9a4d5b`
      );
      const json = await response.json();
      const news = json.articles;
      setData(news);
    }
    f();
  });
  return (
    <NewsListBlock>
      {data.map((article, index) => (
        <NewsItem article={article} key={index} />
      ))}
    </NewsListBlock>
  );
};
export default NewsList;

카테고리가 all 이면 아무것도 추가하지 않고, all이 아니라면 (즉 특정한 카테고리가 선택 되었을 때) 쿼리스트링을 추가하게 하여 fetch 하였다.


이런 식으로 각 카테고리마다 다른 뉴스를 볼 수 있다.

참고 사이트

https://ko.javascript.info/async-await

profile
베이비 게임 개발자

0개의 댓글