프론트엔드/리액트를 다루는 기술

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

Hyeon_E 2023. 7. 11. 16:46

[ 14.1 비동기 작업의 이해 ]

웹 애플리케이션을 만들다 보면 처리할 때 시간이 걸리는 작업이 있음 예를 들어 웹 애플리케이션에서 서버 쪽 데이터가 필요할 때는 Ajax 기법을 사용하여 서버의 API를 호출함으로써 데이터를 수신함 서버의 API를 사용해야 할 때는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 즉시 처리되는 것이 아니라 응답을 받을때까지 기다렸다가 전달받은 응답 데이터를 처리함 이과정에서 해당 작업을 비동기적으로 처리하게 됨

 

만약 작업을 동기적으로 처리한다면 요청을 끝날때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없음

요청이 끝나야 비로소 그 다음 예정된 작업을 할 수 있음 하지만 이를 비동기적으로 처리하며 웹 애플리케이션이 멈추지 않고 동시에 여러 가지 요청을 처리할 수도 있고 기다리는 과정에서 다른 함수도 호출할 수 있음

서버 API를 호출할 때 외에도 작업을 비동기적으로 처리할 때가 있는데 바로 setTimeout함수를 사용하여 특정 작업을 예약할 때임

 

JS에서 비동기 작업을 할 때 가장 흔히 사용하는 방법은 콜백 함수를 사용하는 것 함수자체를 함수의 인자로 들어간 함수를 콜백함수라고 함

 

▶ 콜백함수

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

 

해당 함수가 처리된 직후 어떤한 작업을 하고 싶다면 콜백 함수를 활용해서 작업함 여러개를 순차적으로 처리하고 싶다면 콜백함수를 중첩하여 구현할 수 있음

 

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

 

하지만 이렇게 콜백을 여러번 중첩하면 코드의 가독성이 나빠짐 이러한 형태의 코드를 '콜백지옥'이라고 부름

웬만하면 지양해야 할 형태의 코드

 

▶ 14.1.2 Promise

Promise는 콜백 지옥 같은 코드가 형성되지 않게 하는 방안으로 ES6에 도입된 기능

 

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); // Promise를 리턴
  })
  .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);
  })
  .then((number) => {
    console.log(number);
    return increase(number);
  })
  .catch((e) => {
    //도중에 에러가 발생한다면 .catch를 통해 알 수 있음
    console.log(e);
  });

 

여러 작업을 연달아 처리한다고 해서 함수를 여러 번 감싸는 것이 아니라 .then을 사용하여 그다음 작업을 설정하기 때문에 콜백 지옥이 형성되지 않음

 

▶ 14.1.3 async/await

async/await는 Promise를 더욱 쉽게 사용할 수 있도록 해주는 ES2017(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);
  }
}

 

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

axios는 현재 가장 많이 사용되고 있는 JS HTTP 클라이언트

이 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 처리한다는 점 

 

import React, { useState } from 'react';
import axios from 'axios';
const 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를 호출하고 이에 대한 응답을 컴포넌트 상태에 넣어서 보여줌 onClick 함수에서 axios.get 함수를 사용 이 함수는 파라미터로 전달된 주소에 GET 요청을 해줌

이에 대한 결과는 .then을 통해 비동기적으로 확인할 수 있음

 

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

 

위에 예시에 async를 적용하면 이런 코드가 됨

화살표 함수에 async/await를 적용할 때는 async () => {}와 같은 형식으로 적용됨

 

[ 14.3 newsapi API 키 발급받기 ]

사전에 newsapi에서 API를 키를 발급받음 API 키는 https://newsapi.org/register에 가입하면 발급 받을 수 있음

https://newsapi.org/s/south-korea-news-api로 들어가면 API 설명서가 있음

 

사용할 API 주소는 두가지 형태임

여기 카테고리는 business, entertainment, health, science, sports, technology 중에 골라서 사용할 수 있음

카테고리를 생략하면 모든 카테고리의 뉴스를 불러옴 apiKey 값에는 발급받은 API 키를 입력하면 됨

 

import React, { useState } from 'react';
import axios from 'axios';
const App = () => {
  const [data, setData] = useState(null);
  const apiKey = process.env.REACT_APP_APIKEY;
  console.log(apiKey);
  const onClick = async () => {
    try {
      const response = await axios.get(
        `https://newsapi.org/v2/top-headlines?country=kr&apiKey=${apiKey}`,
      );
      setData(response.data);
    } catch (e) {
      console.log(e);
    }
  };
  return (
    <div>
      <div>
        <button onClick={onClick}>불러오기</button>
      </div>
      {data && (
        <textarea
          rows={7}
          value={JSON.stringify(data, null, 2)}
          readOnly={true}
        />
      )}
    </div>
  );
};
export default App;

 

[ 14.4 뉴스 뷰어 UI 만들기 ]

▶ 14.4.1 NewsItem 만들기

각 뉴스 데이터에 필드를 확인해보면

 

    {
      "source": {
        "id": null,
        "name": "Khan.co.kr"
      },
      "author": "경향신문",
      "title": "'금태섭 신당', 원희룡에 “맘에 안 든다고 '올스톱' 할 거면 정치가 왜 필요한가” - 경향신문",
      "description": null,
      "url": "https://www.khan.co.kr/politics/politics-general/article/202307101450011",
      "urlToImage": null,
      "publishedAt": "2023-07-10T05:50:00Z",
      "content": null
    }

 

// src/components/NewsItem.js
import React from 'react';
import { styled } from 'styled-components';

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>
  );
};
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;
  }
`;
export default NewsItem;

 

 

// App.js
import React from 'react';
import NewsList from './components/NewsList';

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

export default App;

 

[ 14.5 데이터 연동하기 ]

useEffect를 사용하여 컴포넌트가 처음 렌더링되는 시점에 API 요청

주의할점은 useEffect에 등록하는 함수에 async를 붙이면 안됨 useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문

따라서 useEffect 내부에서 async/await를 사용하고 싶다면 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어사 사용해 주어야함 또한 loading이라는 상태를 관리하여 API 요청이 대기 중인지 판별 요청이 대기중일때는 loading값이 true가 되고 요청이 끝나면 loading 값이 false가 되어야 함

 

// src/components/NewsList.js

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import axios from 'axios';
import NewsItem from './NewsItem';

const NewsList = () => {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(false);
  const apiKey = process.env.REACT_APP_APIKEY;

  useEffect(() => {
    //async를 사용하는 함수 따로 선언
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(
          `https://newsapi.org/v2/top-headlines?country=kr&apiKey=${apiKey}`,
        );
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
      setLoading(false);
    };
    fetchData();
  }, []);

  // 대기중일때
  if (loading) {
    return <NewsListBlock>대기중...</NewsListBlock>;
  }
  if (!articles) {
    return null;
  }
  // articles 값이 유효할때
  return (
    <NewsListBlock>
      {articles.map((article) => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
};
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;
  }
`;
export default NewsList;

 

데이터를 불러와서 배열을 map 함수를 사용하여 컴포넌트 배열로 변환할 때 신경써야 할 부분이 있음

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

이 작업을 하지 않으면 아직 데이터가 없을 때 null에는 map 함수가 없기 때문에 렌더링 과정에서 오류가 발생

그래서 애플리케이션이 제대로 나타나지 않고 흰 페이지만 보이게 됨

 

[ 14.6 카테고리 기능 구현하기 ]

// src/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 Categories = () => {
  return (
    <CategoriesBlock>
      {categories.map((item) => (
        <Category key={item.name}>{item.text}</Category>
      ))}
    </CategoriesBlock>
  );
};
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;
  }
`;
export default Categories;

 

categories라는 배열 안에 name과 text 값이 들어가 있는 객체들을 넣어 주어서 한글로 된 카테고리와 실제 카테고리 값을 연결시켜 줌 여기서 name은 카테고리 값을 가리키고 text값은 레더링할 때 사용할 한글 카테고리를 가리킴

 

// App.js
import React, { useCallback, useState } 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} />
    </>
  );
};

export default App;

 

App에서 category 상태를 useState로 관리함 추가로 category 값을 업데이트하는 onSelect라는 함수를 만들어줌

category와 onSelect 함수를 Categories 컴포넌트에 props로 전달하고 category값을 NewsList 컴포넌트에게도 전달

 

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

export default App;

 

// src/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 Categories = ({ onSelect, category }) => {
  return (
    <CategoriesBlock>
      {categories.map((item) => (
        <Category
          key={item.name}
          active={category === item.name}
          onClick={() => onSelect(item.name)}
        >
          {item.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
};
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;
  }
`;
export default Categories;

 

props로 전달받은 onSelect를 각 Category컴포넌트의 onClick으로 설정해주고 현재 선택된 카테고리 값에 따라 다른 스타일 적용

 

▶ 14.6.2 API를 호출할 때 카테고리 지정하기

props로 받아 온 category에 따라 카테고리를 지정하여 API를 요청

 

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import axios from 'axios';
import NewsItem from './NewsItem';

const NewsList = ({ category }) => {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(false);
  const apiKey = process.env.REACT_APP_APIKEY;

  useEffect(() => {
    //async를 사용하는 함수 따로 선언
    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=${apiKey}`,
        );
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
      setLoading(false);
    };
    fetchData();
  }, [category]);

  // 대기중일때
  if (loading) {
    return <NewsListBlock>대기중...</NewsListBlock>;
  }
  if (!articles) {
    return null;
  }
  // articles 값이 유효할때
  return (
    <NewsListBlock>
      {articles.map((article) => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
};
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;
  }
`;
export default NewsList;

 

category값이 바뀔때 마다 요청할 주소가 동적으로 바뀜

category값이 바뀔때마다 뉴스를 새로 불러와야 하기 때문에 useEffect의 의존배열에 category를 넣어 주어야 함

 

[ 14.7 리액트 라우터 적용하기 ]

값을 리액트 라우터의 URL 파라미터를 사용하여 관리

 

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

터미널로 리액트 라우터 설치

 

yarn add react-router-dom

 

index.js에서 리액트 라우터를 적용

 

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';

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

 

▶ 14.7.2 NewsPage 생성

import React from 'react';
import { useParams } from 'react-router-dom';
import Categories from '../components/Categories';
import NewsList from '../components/NewsList';

const NewsPage = ({ match }) => {
  const params = useParams();
  // 카테고리가 선택되지 않았으면 기본값 all로 사용
  const category = params.category || 'all';

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

export default NewsPage;

 

현재 선택된 category 값을 URL 파라미터를 통해 사용할 것이므로 Categories 컴포넌트에서 현재 선택된 카테고리 값을 알려 줄 필요도 없고 onSelect 함수를 따로 전달해 줄 필요도 없음

 

import React from 'react';
import { Route, Routes } from 'react-router-dom';
import NewsPage from './pages/NewsPage';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<NewsPage />} />
      <Route path="/:category" element={<NewsPage />} />
    </Routes>
  );
};

export default App;

 

// src/components/NewsList.js
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import axios from 'axios';
import NewsItem from './NewsItem';

const NewsList = ({ category }) => {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(false);
  const apiKey = process.env.REACT_APP_APIKEY;

  useEffect(() => {
    //async를 사용하는 함수 따로 선언
    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=${apiKey}`,
        );
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
      setLoading(false);
    };
    fetchData();
  }, [category]);

  // 대기중일때
  if (loading) {
    return <NewsListBlock>대기중...</NewsListBlock>;
  }
  if (!articles) {
    return null;
  }
  // articles 값이 유효할때
  return (
    <NewsListBlock>
      {articles.map((article) => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
};
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;
  }
`;
export default NewsList;

 

[ 14.8 usePromise 커스텀 Hook 만들기 ]

컴포넌트에서 API 호출처럼 Promise를 사용해야 하는 경우 더욱 간결하게 코드를 작성할 수 있도록 해 주는 커스텀 Hook

 

// src/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];
}

 

프로젝트의 다양한 곳에서 사용될 수 있는 유틸 함수들은 보통 src 디렉터리에 lib 디렉터리를 만든 후 작성

usePromise Hook은 Promise의 대기중, 완료결과, 실패 결과에 대한 상태를 관리하며 usePromise의 의존 배열 deps를 파라미터로 받아 옴 파라미터로 받아 온 deps 배열은 usePromise 내부에서 사용한 useEffect의 의존 배열로 설정되는데 이 배열을 설정하는 부분에서 ESLint 경고가 나타남

이 경고를 무시하려면 특정 줄에서만 ESLint 규칙을 무시하도록 주석을 작성해야함

에디터에 초록색 경고 줄이 그어졌을 때 그 위에 커서를 올리면 빠른 수정... 이라는 문구가 나타나는데 이를 클릭하면 자동으로 ESLint 규칙을 비활성화시키는 주석을 입력할 수 있음

 

// src/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=0a8c4202385d4ec1bb93b7e277b3c51f`,
    );
  }, [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;

 

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

 

usePromise를 사용하면 NewsList에서 대기 중 상태 관리와 useEffect 설정을 직접 하지 않아도되므로 코드가 훨씬 간편해짐 요청 상태를 관리할 때 무조건 커스텀 Hook을 만들어사 사용해야하는 것은 아니지만 상황에 따라 적절히 사용하면 좋은 코드를 만들어 갈 수 있음

 

[ 14.9 정리 ]

리액트 컴포넌트에서 API를 연동하여 개발할 때 절대 잊지 말아야 할 유의 사항은 useEffect에 등록하는 함수는 async로 작성하면 안된다는 점 함수 내부에 async 함수를 따로 만들어 주어야 함

커스텀 Hook을 만들어 사용함으로써 코드가 간결해지기는 했지만 사용해야 할 API의 종류가 많아지면 요청을 위한 상태 관리를 하는 것이 번거로워질 수 있음 이런 경우 리덕스와 리덕스 미들웨어를 이용하면 좀 더 쉽게 요청에 대한 상태를 관리할 수 있음