14. 외부 API 연동

히치키치·2022년 1월 4일
0

React_Advance

목록 보기
2/9
post-thumbnail

✔ 비동기 작업 이해

1. 동기 vs 비동기


동기적 처리

  • 요청이 끝날 때까지 대기하는 중지됨
  • 요청 처리로 인한 중지가 끝나야만 다음 작업 처리 가능

비동기적 처리

  • 어플리케이션 멈추지 않음
  • 여러가지 요청 처리 가능
  • 요청 처리 기다리며 다른 함수 호출 가능
  • 콜백 함수 사용, Promise (async/await)

2. 콜백 함수


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


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

console.log("시작!!")

setTimeout이 사용되는 시점에서 중지 X
1. 우선 코드 전체가 호출됨
2. 1초 뒤에 주어진 인자 값에 10 더해 반환하는 함수가 실행됨
3. 더해진 값이 result에 담기자마자 출력됨

3. Promise

  • 콜백 안에 콜백을 넣어 구현 가능하지만 가독성 저하됨
  • Promise와 async/await 사용
  • 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("Number Too Big");
                return reject(e)
            }
            resolve(result); // number 값에 10 더하고 성공 처리
        },100) 
    });
    return promise;
}

async function runTask(){
    try{
        let result = await increase(0);
        console.log(result);        
        let result = await increase(result);
        console.log(result);
        let result = await increase(result);
        console.log(result);
        let result = await increase(result);
        console.log(result);
    }
    catch(e){
        console.log(e);
    }
}
  • 함수 앞에 async 추가
  • 함수 내부에서 Promise 앞에 await 사용
  • Promise가 끝날 때까지 대기하고 결과 값을 변수에 담음

✔ axios로 API 호출

axios : 자바스크립트 HTTP 클라이언트로 HTTP 요청을 Promise 기반으로 처리

1. axios 설치

npm create-react-app {프로젝트명}
cd {프로젝트명}
yarn add axios

2. axios.get 사용

const App=()=>{

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

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

  };
  1. 버튼 클릭으로 onClick 함수 실행
  2. onClick 내에서 axios.get 사용해 파라미터로 전달된 주소에 GET 요청
  3. .then 으로 결과 비동적으로 확인

3. async 사용

async () => {} 형식 사용

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

4.뉴스 API 발급 받기

전체 뉴스 불러오기
GET https://newsapi.org/v2/top-headlines?country=kr&apiKey=API_KEY
특정 카테고리 뉴스 불러오기
GET https://newsapi.org/v2/top-headlines?country=kr&category=business&apiKey=API_KEY
카테고리 : business, entertainment, health, science, sports, technology

✔ UI 만들기

1. NewsItem

1.NewsItem 컴포넌트 구성

title, description, url, urlToImage 가진 JSON 객체

2.Styled-components 설치

1. 프로젝트 디렉토리 안에 설치
npm install styled-components

2. 프로젝트 폴더에 import 하기
import styled from 'styled-components';

3. NewsItem 컴포넌트 생성

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;

4. NewsList 컴포넌트 생성

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


const sampleArticle={
    title:"제목",
    description:"내용",
    url:"https://google.com",
    urlToImage:"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTWyeaxdZJy6ROj5lGScMD7ou9WmWwzjjWyEdORt8J-672adFtCM0vBF8KSEkoxv10RGsk&usqp=CAU",
};


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

export default NewsList;

✔ 데이터 연동

1. 비동기로 API

  • useEffect 사용해 처음 랜더링시 API 요청
    useEffect에 등록하는 함수에 async 사용 불가능 ‼
  • useEffect 내부에 async/await 사용시 async 키워드의 또 다른 함수 작성해 사용
  • loading 사용해 API 요청 대기 여부 판별
    true : 요청 대기 중, false : 요청 완료

2. 뉴스 데이터 랜더링

  • 받아온 뉴스 데이터 배열 articles가 현재 null이 아닌지 확인!!
    ‼ mapping 함수 사용전 !articles로 조회 ‼
  • mapping 사용해 받아온 뉴스 데이터 배열을 한개씩 렌더링
  • props 사용해 뉴스 데이터 한개 안에 title, descrpition, image 등 한 개씩 적절한 jsx에 배치
import React, { useEffect, useState } 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);

    //loading 사용해 API 요청 대기 여부 판별 - default 요청 없으니 false
    const [loading, setLoading]=useState(false);

    useEffect(()=>{

        //async 사용하는 함수 따로 선언
        const fetchData=async()=>{
            setLoading(true);  //요청 받아 대기 중이니 true
            try{
                //axios, await로 API로 받아오기
                const response=await axios.get("https://newsapi.org/v2/top-headlines?country=kr&apiKey=4f17bd32831b40dfb56ac62971b30743")
                //받아온 article 데이터 세팅
                setArticles(response.data.articles);
            }
            catch(e){
                console.log(e);
            }
            //받은 요청 모두 완료하여 대기 없으니 loading을 false
            setLoading(false);
        };

        fetchData();//article 데이터 받아오는 fetchData 함수 실행
    },[]);

    //대기중 : (loading = true)
    if (loading){
        return <NewsListBlock>...대기중...</NewsListBlock>
    }

    //아직 articles 값이 설정되지 않은 경우
    if (!articles){
        return null;
    }

    //articles 값이 유효함 - 모두 load된 경우
    return (
        <NewsListBlock>
            {articles.map(article=>(
                <NewsItem key={article.url} article={article}/>
            ))}
        </NewsListBlock>
    );
};

export default NewsList;

요청 대기 중 (loading = true)

요청 완료 (loading = false) && article 올바르게 설정됨

✔ 카테고리 기능

1. 카테고리 UI 추가

  • categories 배열 내 name=(API 내) 실체 카테고리 명, text=랜더링용 한글 카테고리
import React from 'react';
import styled from 'styled-components';

const categories=[
    {
        name:"all",
        text:'전체보기'
    },
    {
        name : "business",
        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;
    font-weight : 1000;
    cursor: pointer;
    white-space:pre;
    text-decoration: none;
    color:inherit;
    paddig-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;
  • APP 에서 NewsList 컴포넌트 상단에 랜더링
const App=()=>{
    return(
      <>
        <Categories/>
        <NewsList/>
      </>
    );
  };

카테고리 목록 상단 랜더링 결과

2. 카테고리 상태관리

  • useState로 category 상태 관리
  • onselect 함수를 통해 category 값 업데이트
  • Categories 컴포넌트에 category와 onSelect 전달
  • NewsList 컴포넌트에 category 전달
import React, {useState,useCallback} from "react";
import axios from "axios";
import NewsList from './components/NewsList';
import Categories from './components/Categories';
/*API_KEY="4f17bd32831b40dfb56ac62971b30743" */

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;
  • 전달받은 onSelect를 onClick으로 설정 후 선택된 카테고리에 다른 스타일 적용
import React from 'react';
import styled,{css} from 'styled-components';

const categories=[...]
const CategoriesBlock=styled.div`...`
            
                  
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 스타일 적용

3. 카테고리 지정해 API 호출

  • query 이용해 카테고리가 all이면 {공백} , 특정 카테고리 선택 시 &category=ㅋ{카테고리} 요청 주소에 포함
  • category 값이 변경될 때마다 재랜더링하기 위해 useEffect의 두번째 인자 배열 (의존배열)에 넣기

함수형 컴포넌트 (useEffect) 사용
1. 컴포넌트 맨 처음 랜더링되는 경우
2. category 값 변경되는 경우
요청 시작되도록 함

import React, { useEffect, useState } 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=({category})=>{

    const [articles,setArticles]=useState(null);

    //loading 사용해 API 요청 대기 여부 판별 - default 요청 없으니 false
    const [loading, setLoading]=useState(false);

    useEffect(()=>{

        //async 사용하는 함수 따로 선언
        const fetchData=async()=>{
            setLoading(true);  //요청 받아 대기 중이니 true
            try{
                const query=category==="all"?"":`&category=${category}`;
                console.log(query);
                //axios, await로 API로 받아오기
                const response=await axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=4f17bd32831b40dfb56ac62971b30743`)
                //받아온 article 데이터 세팅
                //console.log(response.data.articles);
                setArticles(response.data.articles);
            }
            catch(e){
                console.log(e);
            }
            //받은 요청 모두 완료하여 대기 없으니 loading을 false
            setLoading(false);
        };

        fetchData();//article 데이터 받아오는 fetchData 함수 실행
    },[category]);

    //대기중 : (loading = true)
    if (loading){
        return <NewsListBlock>...대기중...</NewsListBlock>
    }

    //아직 articles 값이 설정되지 않은 경우
    if (!articles){
        return null;
    }

    //articles 값이 유효함 - 모두 load된 경우
    return (
        <NewsListBlock>
            {articles.map(article=>(
                <NewsItem key={article.url} article={article}/>
            ))}
        </NewsListBlock>
    );
};

export default NewsList;
전체건강기술

✔ 라우터 적용

1. 리액트 라우터 설치

npm add react-router-dom@5.3.0
버전 지정 한하면 라우팅 문법에 차이 발생함

2. index에서 라우터 적용

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")

);

3. NewsPage 생성

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

const NewPages=({match})=>{

    //카테고리가 선택되지 않으면 기본값 all로 사용
    const category = match.params.category||"all";

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

};

export default NewPages;

4. App에 Route 내용 정의

  • 현재 선택된 category 값을 URL 파라미터 통해 사용
  • category 값이 선택적임 (없으면 전체 카테고리를 선택한 거임)
import React from "react";
import {Route} from "react-router-dom";
import NewsPage from "./pages/NewsPage";

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

}

export default App;
  • 특정 component에 styled-components 사용 시 styled(컴포넌트이름)`` 형식
  • Category 컴포넌트에 to 값에 /카테고리이름 설정
  • 전체보기(all) 경우
    1. to/으로 설정 (/all 아님)
    1. exact 값을 true로 설정 (미설정 시 다른 카테고리 선택 시 전체보기에 active 스타일 적용됨)

✔ usePromise 커스텀 Hook 만들기

0개의 댓글