TIL 22-04-30 (2)

thisisyjin·2022년 4월 30일
0

TIL 📝

목록 보기
34/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 14

  • 외부 API 연동 - 뉴스 뷰어 제작

외부 API 연동 - 뉴스 뷰어

비동기 작업

웹에서 서버 쪽 데이터가 필요할 때에는 - AJAX 기법을 사용하여 서버의 API를 호출하여 데이터 수신 가능.
서버의 API를 사용해야 하는 경우, 네트워크 송수신 과정에서 시간이 걸려서 작업이 즉시 처리되지 않고,
응답을 받을 때까지 기다렸다가 전달받은 응답(response)을 처리함.
-> 비동기적으로 처리함.

동기적비동기적
순서대로 처리. 다른 작업 할 수 X.순서대로 처리하지 않음. 여러 요청 처리 가능.
  • 서버 API 호출, setTimeout(타이머 함수) 등의 작업시 비동기적으로 처리함.
  • 자바스크립트에서 비동기 작업시 콜백 함수를 사용함.
    -> 함수를 함수의 인자로 전달해줌.

콜백 함수

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

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

< (1초 후) 10


만약, 1초마다 10, 20, 30, 40과 같이 여러번 순차적으로 처리하고 싶으면
콜백함수를 중첩하면 된다.

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

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

  • 원하던 결과는 얻었지만, 여러 번 중첩되다 보니 가독성이 떨어졌다. = 콜백 지옥
  • 이런 코드는 지향해야 한다.

🙋‍♂️ 콜백 지옥(callback hell)

콜백 함수를 익명 함수로 전달하는 과정에서 또 다시 콜백 안에 함수 호출이 반복되어
코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상.


Promise

  • 콜백 지옥 (Callback Hell)을 방지하기 위해 ES6에 도입됨.
  • Promise는 new 연산자와 함께 호출하고 인자로 콜백을 받는다.
    ->Promise는 Promise 객체를 반환한다.
  • Promise는 호출될 때 바로 실행되지만 그 안에 콜백은 resolve, reject 둘 중 하나가 호출 되기 전에 then 또는 catch로 넘어가지 않는다.
  • resolve, reject 로 성공 혹은 실패로 결과 값을 나타내어 다음 작업을 체이닝할 수 있다. (즉, 비동기 제어가 가능하다.)
    -> 계속해서 Promise 객체를 리턴하므로.
  • then으로 작업을 이어가기 위해서는 resolve() 함수를 호출한다.
  • 작업을 중단하거나 err 처리를 위해서는 reject() 함수를 호출한다.

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

increase(0)
    .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);
    });
  • 여러 작업을 연달아 처리할 때, 함수의 중첩 대신 .then 메서드를 사용하면 되므로 콜백 지옥이 생성되지 않음.

async/await

  • Promise를 더욱 쉽게 사용하게 해주는 ES8 문법.
  • 함수 앞부분에 async 키워드를 추가하고, 함수 내부에서 Promise 앞부분에 await 키워드 사용.
    -> Promise가 끝날 때까지 기다리고, 결과값을 특정 변수에 담을 수 있음.
function increase(number) {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            const result = number + 1;
            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);
    } catch (e) {
        console.log(e);
    }
}
  • 위에서 increase()의 리턴값이 promise 이므로 그 앞부분에 await 키워드를 붙여줌.
  • try ~ catch 문을 이용함.

axios로 API 호출하여 데이터 수신

✅ axios

App.js

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

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

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

export default App;

  • get()안에 URL은 JSON 형식의 파일임.
    axios.get 메서드를 이용하여 데이터를 불러오고, then 메서드로 후속처리.

  • setData(response.data)로 서버가 응답한 데이터를 가져올 수 있음.

참고 - 응답 스키마

{
  // `data`는 서버가 제공한 응답(데이터)입니다.
  data: {},

  // `status`는 서버 응답의 HTTP 상태 코드입니다.
  status: 200,

  // `statusText`는 서버 응답으로 부터의 HTTP 상태 메시지입니다.
  statusText: 'OK',

  // `headers` 서버가 응답 한 헤더는 모든 헤더 이름이 소문자로 제공됩니다.
  headers: {},

  // `config`는 요청에 대해 `axios`에 설정된 구성(config)입니다.
  config: {},

  // `request`는 응답을 생성한 요청입니다.
  // 브라우저: XMLHttpRequest 인스턴스
  // Node.js: ClientRequest 인스턴스(리디렉션)
  request: {}
}

  • response.data는 서버가 제공한 응답(=data)을 의미함.
  • useState Hook의 setState함수를 이용하여 해당 데이터를 'data' state에 저장.
  • onClickBtn 함수에서는 axios.get() 메서드 사용.
    -> 인자로 전달된 주소에 GET 요청을 해줌. (HTTP Request)
    -> 결과는 .then을 통해 비동기적으로 확인 가능

⚡️ async/await 적용시

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

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

export default App;

newsapi 불러오기

  • newsAPI - 회원가입 후 API 키 얻을 수 있음.
  • App.js에 axios.get()의 인자로
    https://newsapi.org/v2/top-headlines?country=kr&apiKey=API인증키 를 넣어줌.

불러오기를 클릭하면 해당 API의 데이터가 불러와짐.


뉴스 뷰어 UI 제작

styled-components 설치

$ yarn add styled-components

우선 NewsItem.jsNewsList.js 파일을 생성한 후,
뉴스 데이터에는 어떤 필드가 있는지 확인해보자.

"source": {
        "id": null,
        "name": "Sbs.co.kr"
      },
      "author": "서대원 기자",
      "title": "고진영, LPGA 투어 팔로스 버디스 챔피언십 2R 공동 2위 - SBS 뉴스",
      "description": "여자골프 세계랭킹 1위 고진영 선수가 미국여자프로골프 투어 신설대회인 팔로스 버디스 챔피언십 2라운드에서 공동 2위에 올랐습니다. 고진영은 미국 캘리포니아주 팔로스 버디스 골프클럽에서 열린 대회 이틀째 경기에서 버디 1개와 더블 보기 1개로 1오버파를 쳤습니다.",
      "url": "https://news.sbs.co.kr/news/endPage.do?news_id=N1006733776",
      "urlToImage": "https://img.sbs.co.kr/newimg/news/20220425/201658611_1280.jpg",
      "publishedAt": "2022-04-30T02:23:00Z",
      "content": null
    },
  1. title = 제목
  2. description = 기사 내용
  3. url = 링크
  4. urlToImage = 뉴스 이미지

NewsItem 컴포넌트 생성

🙋‍♂️ styled-component 사용.

주로 컴포넌트를 감싸는 ___Block (=styled.div)를 위에 별도로 생성함.

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;

NewList 컴포넌트 생성

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;
  • 여기서 나중에 API 요청을 할 것임.
  • 아직은 안했으므로, 샘플 데이터인 sampleArticle을 같이 생성해줌.
  • 하위 컴포넌트인 NewsItem에 props로 article을 넘겨줌.
    -> 여기서 article은 각 뉴스 데이터임.

App.js

import NewsList from './components/NewsList';

const App = () => {
  return <NewsList />;
}

export default App;

Result


데이터 연동

  • 이제 newsapi의 데이터를 연동시키면 된다.
  • 컴포넌트가 렌더링 되는 순간 API를 요청한다.
    -> useEffect를 이용. 두번째 인자를 [ ] 로 하여 맨 처음에만 실행되도록.
  • loading 이라는 state로 로딩중인지 판별할 수 있게 함.

❗️ 주의

  • useEffect의 콜백 안에 async를 붙여선 안된다.
  • useEffect는 뒷정리 함수를 return 하기 때문.
    -> 따라서, 콜백에서 async를 쓰고 싶다면 내부에서 함수를 만들어서 호출해줘야 함.

NewsList.js

// 여기서 API 요청
import { 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=f6052deb31a34f38b602753e2ddf0daf'
                );
                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;
  • 로딩중 일때는 대기중... 이라는 문구가 뜨게 하고,
  • ⭐️ !articles 일때, 즉 articles가 null 일때를 검사해야 한다.
    -> 아직 데이터가 없을 때 map함수를 거치면 렌더링 과정에서 오류가 발생하기 때문.

Result

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글