상세 컨텐츠

본문 제목

[리액트를 다루는 기술] 14장 외부 API를 연동하여 뉴스 뷰어 만들기

21-22/21-22 리액트 마스터

by 도리에몽 2022. 1. 3. 13:00

본문

728x90

14.1 비동기 작업의 이해

작업 동기적 처리 : 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없음

작업 비동기적 처리 : 동시에 여러 가지 요청 처리 가능. 기다리는 과정에서 다른 함수도 호출 가능

  → 자바스크립트에서 비동기 작업을 할 때 콜백 함수를 사용함

    ex) setTimeout - 특정 작업 예약

function printMe() {
	console.log('Hello World');
}
setTimeout(printMe, 3000); //3초 뒤 printMe 호출
console.log('대기 중...');

콜백 지옥 : 콜백 안에 또 콜백을 넣어서 구현. 코드의 가독성↓

  ⇒ 콜백 지옥 형성되지 않게 하는 방안 : Promise

function increase(number) {
	const promise = new Promise((resolve, reject) => {
	// resolve는 성공, reject는 실패
		setTimeout(() => {
			const result = number + 10;
			if(result>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);
	})
	.then(number => {
		console.log(number);
		return increase(number);
	})
	.then(number => {
		console.log(number);
		return increase(number);
	})
	.then(number => {
		console.log(number);
		return increase(number);
	})
	.then(number => {
		console.log(number);
		return increase(number);
	})
	.catch(e=>{
		console.log(e);
});

▶async/await

더보기
더보기

Promise를 더욱 쉽게 사용할 수 있도록 해 주는 ES8 문법

사용법

  함수의 앞부분에 async 키워드

  해당 함수 내부에서 Promise의 앞부분에 await 키워드 사용

⇒ 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);
	} catch(e) {
		console.log(e);
	}
}
💡 화살표 함수에 async/awiat 적용 : async () ⇒ {} 형식으로 사용

 

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

axios : 자바스크립트 HTTP 클라이언트

  HTTP 요청을 Promise 기반으로 처리

 💡 가짜 API : https://jsonplaceholder.typicode.com/todos/1

14.3 newsapi API 키 발급받기

https://newsapi.org/register → 가입해서 API 키 발급받기

14.4 뉴스 뷰어 UI 만들기

뉴스 데이터 JSON 객체

  • title : 제목
  • description : 내용
  • url : 링크
  • urlToImage : 뉴스 이미지

▶NewsItem.js - 각 뉴스 정보를 보여주는 컴포넌트

더보기
더보기
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 }) => {
  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;

▶NewsList.js - API를 요청하고 뉴스 데이터가 들어 있는 배열을 컴포넌트 배열로 변환하여 렌더링 해주는 컴포넌트

더보기
더보기
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} />
			<NewsItem article={sampleArticle} />
    </NewsListBlock>
  );
};
export default NewsList;

 

14.5 데이터 연동하기

useEffect를 사용하여 컴포넌트가 화면에 보이는 시점에 API를 요청하려고 할 때,

❗ useEffect에 등록하는 함수에 async를 붙이면 안 됨

  → useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문에

  ⇒ 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용해 주어야 함

 

데이터 배열을 map 함수를 사용하여 컴포넌트 배열로 변환할 때,

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

▶NewsList.js

더보기
더보기
import React, { useState, useEffect } 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 NewsList = () => {
	const [articles, setArticles] = useState(null);
	const [loading, setLoading] = useState(false);
	
	useEffect(() => {
		const fetchData = async () => {
			setLoading(true);
			try {
				const response = await axios.get(
					'https://newsapi.org/v2/top-headlines?country=kr&apiKey=발급받은 API 키',
				);
			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;

14.6 카테고리 기능 구현하기

▶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 screen and (max-width:768px){
    width:100%;
    overflow-x:auto;
  }
`;

const Category = styled(NavLink)`
  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;

name : 실제 카테고리 값

text : 렌더링 할 때 사용할 한글 카테고리

category 상태를 useState로 관리

▶App.js

더보기
더보기
import React, {useState, useCallback} 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} />
		</>
	);
};

▶components/Categories.js

더보기
더보기
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 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;
  }
`;

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;

카테고리 별로 API 요청

▶components/NewsList.js

더보기
더보기
...

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 = await axios.get(
					'https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=발급받은 API 키',
				);
			setArticles(response.data.articles);
		} catch(e) {
				console.log(e);
			}
			setLoading(false);
		};
		fetchData();
	}, [category]);

...

14.7 리액트 라우터 적용하기

카테고리 값을 리액트 라우터의 URL 파라미터를 사용해 관리해 보기

▶리액트 라우터의 설치 및 적용

더보기
더보기
$ yarn add react-router-dom
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

 

▶NewsPage 생성

더보기
더보기
// pages/NewsPage.js
import React from 'react';
import Categories from '../components/Categories';
import NewsList from '../components/NewsList';

const NewsPage = ({ match }) => {
  const category = match.params.category || 'all';

  return (
    <>
      <Categories />
      <NewsList category={category} />
    </>
  );
};
export default NewsPage;
// App.js
import React from "react";
import { Route } from 'react-router-dom';
import NewsPage from './pages/NewsPage';

//API key : ec1b127d878f4e23b281eac02db4510e
const App = () => {
  return <Route path="/:category?" component={NewsPage} />;
};

export default App;

path에 /:category? 형태 : category 값이 선택적이라는 의미.

  파라미터 값이 없다면 전체 카테고리를 선택한 것으로 간주

▶Categories에서 NavLink 사용하기

더보기
더보기
//components/Categories.js
import React from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';

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(NavLink)`
  font-size:1.125rem;
  cursor:pointer;
  white-space:pre;
  text-decoration:none;
  color:inherit;
  padding-bottom:0.25rem;

  &:hover { color:#495057; }

  &.active {
    font-weight:600;
    border-bottom:2px solid #22b8cf;
    color:#22b8cf;
    &:hover{
      color:#3bc9db;
    }
  }

  & + & {
    margin-left:1rem;
  }
`;

const Categories = () => {
  return (
    <CategoriesBlock>
      {categories.map(c => (
        <Category
          key={c.name}
          activeClassName="active"
          exact={c.name === 'all'}
          to={c.name === 'all' ? '/' : `/${c.name}`}
        >{c.text}</Category>
      ))}
    </CategoriesBlock>
  );
};

export default Categories;

14.8 usePromise 커스텀 Hook 만들기

컴포넌트에서 API 호출처럼 Promise를 사용해야 하는 경우, 간결하게 코드 작성 위해 커스텀 Hook을 만들어 줌

▶lib/usePromise.js

더보기
더보기
import { useState, useEffect } from "react";

export default function usePromise(promiseCreator, deps) {
  const [loading, setLoading] = useState(false);
  const [resolved, setResolved] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const process = async () => {
      setLoading(true);
      try {
        const resolved = await promiseCreator();
        setResolved(resolved);
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    };
    process();
    //eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
  return [loading, resolved, error];
}
  • Promise의 대기 중, 완료 결과, 실패 결과에 대한 상태 관리
  • usePromise의 의존 배열 deps를 파라미터로 받음
  • 파라미터로 받아 온 deps 배열은 usePromise의 내부에서 사용한 useEffect의 의존 배열로 설정됨.

▶components/NewsList.js

더보기
더보기
import React from 'react';
import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
import usePromise from '../lib/usePromise';

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 = ({ category }) => {
  const [loading, response, error] = usePromise(() => {
    const query = category === 'all' ? '' : `&category=${category}`;
    return axios.get(
      `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=ec1b127d878f4e23b281eac02db4510e`,
    );
  }, [category]);

  //대기 중일 때
  if (loading) {
    return <NewsListBlock>대기 중...</NewsListBlock>
  }
  //아직 response 값이 설정되지 않았을 때
  if (!response) {
    return null;
  }
	//에러가 발생했을 때
	if(error) {
		return <NewsListBlock>에러 발생!</NewsListBlock>;
	}
  //response 값이 유효할 때
  const { articles } = response.data;
  return (
    <NewsListBlock>
      {articles.map(article => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
};
export default NewsList;

퀴즈

▶Q1. 자바스크립트에서 비동기 작업을 할 때는 (__________)를 사용한다.

더보기
더보기

답 : 콜백함수👍

▶Q2. Promise를 더욱 쉽게 사용할 수 있도록 해주는 ES8문법으로, 함수의 앞부분에 () 키워드를 추가하고, 해당 함수 내부에서 Promise의 앞부분에 (_) 키워드를 사용한다.

더보기
더보기

답 : async / await👍

▶Q3. useEffect에 등록하는 함수는 async로 작성하면 안 됩니다. 이를 해결하기 위해서는 어떻게 해야 할까요?

더보기
더보기

답 : 함수 내부에 async 키워드가 붙은 다른 함수를 만들어 사용한다!👍

728x90

관련글 더보기