본문 바로가기
FE/React

6. 외부 API 연동하여 뉴스 뷰어 만들기

by aeyong-dev 2023. 8. 1.

6.1 비동기 작업의 이해

우리가 만든 웹 앱은 결국 백엔드에서 만들어준 외부 API를 사용해 데이터를 불러옵니다. 

이때, API를 호출하면 응답받을 때 까지 시간이 걸리겠죠. 

Ajax를 사용하여 요청을 보내고, 기다렸다가, 응답을 받으면 데이터를 처리합니다. 

이 과정은 비동기적으로 일어납니다. 

 

쉽게 설명해볼게요. 

동기적이란 말은, 작업이 끝나면 다른 작업을 하는 것을 말합니다. 

비동기적이란 말은, 작업이 동시에 병렬적으로 이루어진다는 것을 말합니다. 

 

동기적(Synchronous)이란 것은 위의 그림처럼 순서대로 작업이 이루어지고요. 

비동기적(Asynchronous)이란 것은 위의 그림처럼 동시에 작업이 이루어집니다. 

 

우리의 웹 앱을 동기적으로 처리한다면, 요청에 대한 응답을 받을 때 까지 아무것도 못해요. 

하지만 비동기적으로 처리한다면, 요청에 대한 응답을 기다리며 다른 일들을 할 수 있습니다. 

 

이렇게 비동기적으로 작업을 실행하기 위해서 가장 흔히 사용되는 것이 콜백함수 입니다. 

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

setTimeout(printMe, 3000);
console.log("Wait...");

여기 간단한 코드를 실행하게 되면, Wait... 문구가 먼저 출력되고 3초 뒤에 printMe함수가 실행됩니다. 

여기서 사용되는 setTimeout 또한 콜백함수입니다. 

3초 기다렸다가 printMe가 출력되는 것이 아니라, Wait...이 먼저 출력되고 3초 후에 printMe가 출력됐잖아요. 

비동기적으로 두 함수가 실행되었기에 가능한 일입니다. 

 

6. 1. 1 콜백 함수

비동기적으로 처리하는 과정에서, 여러 함수가 동시에 실행됩니다. 

그런데, 1번 함수가 끝나면 8번 함수가 바로 실행되기를 바라는 상황에서

비동기적으로 1번과 8번이 동시에 실행되면 곤란하겠죠. 

이런 상황에서 콜백함수를 사용합니다. 

콜백함수는 이렇게 사용해요. 

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

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

콜백함수를 중첩해서 사용할 수도 있습니다.

하지만 복잡해지는 경우에는 정말 보기 싫은 코드가 생기게 됩니다. 

이럴때 우리는 Promise를 사용합니다. 

 

6. 1. 2 Promise

콜백지옥을 벗어나기 위해 ES6에서 만들어진 문법입니다. 

아래와 같이 사용하여, callback을 중첩해서 사용하는 것이 아니라, .then으로 다음의 작업을 계속 이어줍니다. 

그리고 마지막에는 .catch로 실행도중 발생하는 에러를 처리합니다. 

function increase(number){
	const promise = new Promise((resolve, reject) => {
    	setTimeout(()=>{
        	const result = numebr+10;
            if(result>50){
            	const e = new Error('NumberTooBig');
                return reject(e);
            }
            resolve(result);
        }, 1000);
     });
     return promise;
}

increase(0)
	.then(number => {
    	console.log(numebr);
        return increase(number);
    })
    .then(number => {
    console.log(numebr);
    return increase(number);
	})
	.then(number => {
    	console.log(numebr);
        return increase(number);
    })
	.then(number => {
    	console.log(numebr);
        return increase(number);
    })
	.then(number => {
    	console.log(numebr);
        return increase(number);
    })
    .catch(e=>{
    	console.log(e);
    });

.then으로 다음 작업을 계속 설정해줌으로써 콜백지옥을 벗어납니다. 

 

6. 1. 3 async/await

async/await은 Promise를 더 쉽게 쓸 수 있게 해줍니다. 

함수 앞에 async 키워드를 쓰고, 함수 내부 Promise 앞부분에 await 키워드를 사용합니다. 

이렇게 해서 Promise가 끝날 때 까지 기다리고, 결과 값을 변수에 담을 수 있습니다. 

 

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = number + 10;
      if (result > 50) {
        const e = new Error("NumberTooBig");
        return reject(e);
      }
      resolve(result);
    }, 1000);
  });
  return promise;
}

async function runTasks() {
  try {
    let result = await increase(0);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
  } catch (e) {
    console.log(e);
  }
}

 

6. 2 axios로 API 호출해서 데이터 받아오기

http 클라이언트란, http 프로토콜을 사용하는 소프트웨어 입니다.

axios는 자바스크립트 에서의 http 클라이언트에요.

http 요청을 Promise 기반으로 처리한다는 것이 특징입니다.

 

설치를 위해 다음 명령어를 작성합시다.

yarn add axios

그리고 코드포맷을 위해 프로젝트 최상단 디렉토리에 .prettierrc 파일을 생성하고 다음과 같이 작성합니다.

{
  "sinngleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

vscode 파일 자동 불러오기 기능을 쓰기 위해 jsonconfig.json 파일도 만들고 다음과 같이 작성할게요.

{
  "compilerOptions": {
    "target": "es6"
  }
}

이제 본격적으로 App.js를 작성해봅시다.

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

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

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

export default App;

이 코드를 간단히 설명해볼게요.

불러오기 버튼을 누른다면, jsonplaceholder에서 제공하는 가짜 API를 get 요청으로 호출하고, response 결과를 .then을 통해 비동기적으로 컴포넌트 상태에 넣어 보여줍니다.

그러면 async를 적용해볼게요.

const onClick = async() => {
    try{
      const response = await axios.get("https://jsonplaceholder.typicode.com/todos/1");
      setData(response.data);  
    }
    catch(e){
      console.log(e);
    }
  };

try, catch로 에러핸들링을 해주었습니다. 

마찬가지로 잘 작동하는 모습을 볼 수 있습니다.

 

6. 3 newsAPI api 키 발급받기

무슨 api든, 사용하기 위해서는 기본적으로 api key를 발급받아야 합니다. 

https://newspai.org/register 로 들어가서 가입 후 api key를 발급받아주세요.

다른 api도 마찬가지로, api key를 발급받고, 공식 문서에서 사용 설명을 읽은 후 시키는 대로 하면 됩니다. 

위의 api같은 경우에는 https://newsapi.org/s/south-korea-news-api 에서 가이드를 읽을 수 있습니다.

문서를 읽어보니 탑 뉴스 헤드라인을 얻고싶으면 get 요청을

https://newsapi.org/v2/top-headlines?country=kr&apiKey=[YOUR API KEY]
로 보내면 된다고 하네요. 
대괄호 부분을 지우고(괄호포함 지워야함) API key를 넣어서 쓰면 됩니다. 
이 요청을 보냈을 때 response로 받을 수 있는 객체의 형태 또한 확인할 수 있습니다. 

 

이제 우리의 코드에서 get요청을 위의 주소로 고치고 실행시켜보면 탑 뉴스 헤드라인이 잘 표기됨을 확인할 수 있습니다. 

이 json 데이터를 잘 가공해서 예쁘게, 사용자가 보기 쉽게 만들어 주는 것이 우리 프론트엔드의 일이라고 할 수 있겠죠?

6. 4 뉴스 뷰어 UI 만들기

먼저 styled-components를 사용하기 위해 설치할게요. 

yarn add styled-components

src/components 디렉터리 생성 후 NewsItem.js, NewsList.js 파일을 생성합니다. 

NewsItem은 뉴스 정보를 보여주고, NewsList는 API 요청하고 데이터 배열을 컴포넌트 배열로 변환하여 렌더링해줍니다. 

6. 4. 1 NewsItem 만들기

json 데이터를 가공하여 보여주기 위해, 어떤 필드가 있는지 살펴봅시다. 

{
      "source": {
        "id": null,
        "name": "Starnewskorea.com"
      },
      "author": "김나연",
      "title": "윤박♥김수빈, 결혼식 현장 공개 \"축하 감사..잘 살게요\" - 스타뉴스",
      "description": "모델 김수빈이 배우 윤박과 결혼 소감을 전했다. 2일 윤박과 김수빈은 서울 모처에서 결혼식을 올렸다. 결혼식은 양가 가족들과 가까운 지인들이 참석한 가운데 비공개로 진행됐다. 결혼식 사회는 배우 곽동연이 맡았으며, 2AM과 존박이 축가를 맡았다.이날 한 웨딩 플래너 업체는 윤박, 김수빈의 웨딩 화보를 공개했고, 볼에 입을 맞추고, 서로를 마주보며 다정한 포...",
      "url": "https://www.starnewskorea.com/stview.php?no=2023090314461929418",
      "urlToImage": "https://thumb.mtstarnews.com/21/2023/09/2023090314461929418_1.jpg",
      "publishedAt": "2023-09-03T06:12:34Z",
      "content": ". \r\n 2 . . , 2AM ., , , . \r\n\" , . \" . \r\n , . . . H&amp; \" \" \" \" .\r\nSNS \" \" \" , . \" . \r\n2012 MBC ' ' , ' ', '', '', ' ', '', ' ' . 1993 6 . YG ."
    }

title이 제목, description이 내용, url이 링크, urlToImage가 뉴스 이미지네요.

우리가 만들 컴포넌트는, 이 객체를 props로 받아 사용할것입니다.  

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 }) => {
  const { title, description, url, urlToImage } = article;
  return (
    <NewsItemBlock>
      {urlToImage && (
        <div className="thumbnail">
          <a href="{url}" target="blank" rel="noopener noneferrer">
            <img src={urlToImage} alt="thumbnail" />
          </a>
        </div>
      )}
      <div className="contents">
        <h2>
          <a href="{url}" target="_blank" rel="noopener noneferrer">
            {title}
          </a>
        </h2>
        <p>{description}</p>
      </div>
    </NewsItemBlock>
  );
};

export default NewsItem;

article 객체를 props로 받아옵니다. 

props로 가져온 article 객체를  title, desciption, url, urlToImage 각각에 나누어줍니다. 

그리고 예쁘게 보여주면 끝이에요. 

쉬워요. 생각보다.

6. 4. 3 NewsList 만들기

나중에 여기서 API 요청을 하게 될 것입니다. 

일단 sample article 이라는 더미 객체를 만들어서 대체할게요.

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} />
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
    </NewsListBlock>
  );
};

export default NewsList;

이걸 App 컴포넌트에서 확인해볼게요.

이렇게 잘 표시되는 것을 볼 수 있습니다. 

 

6. 5 데이터 연동하기 

NewsList에서 이전의 API를 호출해보겠습니다. 

우리가 API를 사용하는(API 요청을 보내는) 시점은 언제일까요?

화면에 컴포넌트가 보이기 시작하는 시점이겠죠? 

그럴때는 useEffect 훅을 사용하면 되는 것을 우리는 알고있습니다. 

useEffect를 사용해서 컴포넌트가 화면에 렌더링 될 때 API를 요청하면 되는 것입니다. 

 

그런데 주의해야 할 점이 있습니다. 

useEffect에 등록하는 함수에는 async를 사용하면 안됩니다. 

왜냐하면, useEffect에서 return해야 하는 값은 뒷정리 함수이기 때문입니다. 

그래서 우리는 함수 내부에 async 키워드를 사용한 다른 함수를 사용해야 합니다. 

 

추가적으로 loading이라는 상태도 관리해서, API 요청이 대기 중인 상태도 만들것입니다. 

요청이 대기중일 때는 loading이 true, 요청이 끝나면 false가 될것입니다. 

 

NewsList 컴포넌트를 다음과 같이 수정합니다. 

import styled from "styled-components";
import { useState, useEffect } from "react";
import axios from "axios";
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 NewsList = () => {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(
          [YOUR API KEY],
        );
        setArticles(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>
  );
};

export default NewsList;

useEffect 내부에 fetchData 라는 함수를 따로 선언해주고, 이 컴포넌트가 로딩 될 때에 실행되도록 하였습니다. 

async로 비동기적인 함수임을 선언하고, 

로딩중이므로(아직 api 요청을 하지 않았으므로) loading을 true로 바꿉니다. 

그리고 axios.get으로 response를 받아 이것을 articles로 만들어줍니다. 

에러가 나면 console.log로 기록합니다. 

 

만약에 로딩중이라면 '대기중...' 문구가 뜨게 하였고, 

만약에 articles가 아직 설정되지 않았다면 null을 return합니다. 

 

최종적으로 articles에 온전히 값을 받아왔다면, 

articles 내부 각각에 NewsItem을 만들어줍니다. 

articles.map() 을 사용하였는데, .map()을 사용하기 위해서는 꼭 articles가 null인지 확인해줘야 합니다. 

null이라면 오류가 생겨 아무 것도 보이지 않게 될 것입니다.

잘 보이는 것을 확인할 수 있습니다. 

 

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

우리가 사용한 api에는 카테고리 기능이 있습니다. 

이는 buisness, science, entertainment, sports, health, technology 총 여섯개입니다. 

필자는 어느정도 사용법을 알았다고 스스로 판단하여.. 

책을 보지 않고 직접 만들어보았습니다. 

 

먼저 Categoris.js를 만들고 다음과 같이 작성했습니다. 

import useState from "react";

const Categories = () => {
  const [selected, setSelected] = useState("all");
    const move = ({props}) => {
        setSelected(props);
    }

  return (
    <div>
      <div>
        <ul>
          <li onClick={move("all")}>all</li>
          <li>business</li>
          <li>entertainment</li>
          ...

선택한 카테고리(현재 카테고리)를 나타내기 위해 selected, setSelected를 만들었습니다. 

그리고 onclick 이벤트를 만들어주기 위해 move 함수를 만들고, props로 '어느 카테고리로 이동할지'를 줬습니다. 

여기서 문제가 생겼습니다. selected를 props로 준 것은 괜찮은데, 어떻게 이동하지? 

 

fileter()를 활용해서 selected에 맞는 기사들을 필터링해서 보여주면 될 것 같습니다. 

그러기 위해서는 NewsList 컴포넌트에 selected를 넘겨줘야 할 것 같습니다. 

NewsList 컴포넌트에서 articles.filter().map()으로 보여주면 될 것 같아요. 

---

작성중인 게시글입니다.