[React Study] 3주차 - 뉴스 뷰어 API 연동해보고, API가 무엇인지 공부하기

ㅎㅎ·2023년 10월 1일
0

Study

목록 보기
3/4

2주차 태스크:
React Router v6 공부하고, 적용해보기


3주차 - 뉴스 뷰어 API 연동해보고, API가 무엇인지 공부하기

  1. API란?
  • 접근 방식에 따른 API
  • REST API
  • CRUD Operation이란?
  1. 뉴스 뷰어 API 연동해보기

1. API란?

API는 Application Programming Interface의 줄임말로, 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다.

때때로 API는 정보 제공자와 정보 사용자 간의 계약으로 지칭되며 소비자에게 필요한 콘텐츠(호출)와 생산자에게 필요한 콘텐츠(응답)를 구성한다.

API는 사용자 또는 클라이언트, 그리고 사용자와 클라이언트가 얻으려하는 리소스 또는 웹 서비스 사이의 조정자다. API는 조직이 보안, 제어, 인증을 유지관리(누가 무엇에 액세스할 수 있는지 결정)하면서 리소스와 정보를 공유할 수 있는 방법이기도 하다.

또한 API는 리소스 검색 방법 또는 리소스의 출처에 대해 자세히 알 필요가 없다(캐싱).

접근 방식에 따른 API

1. Private API

내부 API로, 기업이나 연구 단체에서 자체 제품과 운영 개선을 위해 사용되고 제 3자에게 노출되지 않는다.

2. Public API

개방형 API로 모두에게 공개되는 API다. 이 중에서도 접속하는 대상에 대한 제약이 없는 경우를 OPEN API라고 한다.

3. Partner API

특정 비즈니스 파트너 간의 데이터 공유를 위한 API다.


REST API

REST를 기반으로 만들어진 API이다.

REST 구성 요소

REST란 REpresentational State Transfer의 약자로, 자원을 이름으로 구분하여 해당 자원의 상태를 주고받는 모든 것을 의미한다.

  1. 자원(Resource): HTTP URI(Uniform Resource Identifier)
  2. 자원에 대한 행위(Verb): HTTP Method
  3. 자원에 대한 행위의 내용(Representations): HTTP Message Pay Load

즉 REST란,
URI를 통해 리소스를 명시하고 Method(POST, GET, PUT, DELETE, PATCH 등)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미한다.


CRUD Operation이란

CRUD는 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 의미한다.

REST에서의 CRUD Operation 동작 예시:

  • Create: 데이터 생성(POST)
  • Read: 데이터 조회(GET)
  • Update: 데이터 수정(PUT, PATCH)
  • Delete: 데이터 삭제(DELETE)

RESTful이란:
REST의 원리를 따르는 시스템을 의미한다.


2. 뉴스 뷰어 API 연동해보기

참고 사이트: REACT 뉴스 뷰어 만들기

1) 프로젝트 준비

  1. 프로젝트 설치 후 외부 API 연동을 위한 axios 라이브러리를 설치한다.
yarn add axios
  1. .prettierrc 파일을 작성한다.

prettier은 코드 포맷(형식)을 설정할 수 있다. 블로그 글의 코드형식과 맞추기 위해 똑같이 작성해준다.

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}
  1. jsonconfig.json 파일을 작성한다.

이건 왜지?

{
  "compilerOption": {
    "target": "es6"
  }
}

2) axios를 사용한 API 테스트

API를 GET으로 가져올 수 있는지 테스트해봤다.

App.jsx

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

function App() {
  // API를 넘겨받을 state 선언
  const [data, setData] = useState(null);

  const onClick = async () => {
    // axios 라이브러리로 apic call
    try {
      const response = await axios.get(
        'https://jsonplaceholder.typicode.com/todos/1',
      );
      // 응답 data state 저장
      setData(response.data);
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <div>
      <div className="h-12">
        <button
          className="p-0.5 border-solid border-4 bg-slate-200 rounded-sm"
          onClick={() => onClick()}
        >
          불러오기
        </button>
      </div>
      {/* JSON 문자열 뿌릴 영역 */}
      <div className="w-64 border-solid border-4">
        {data && (
          <textarea
            className="w-60"
            rows={7}
            value={JSON.stringify(data, null, 2)}
            readOnly={true}
          />
        )}
      </div>
    </div>
  );
}

export default App;


3) newsAPI 키 발급받기

3-1) newsAPI 웹사이트에서 로그인을 해 키를 발급받는다.
https://newsapi.org/account

한국 뉴스에 대한 newsAPI 공식 문서

3-2) App.jsx에서의 API를 발급받은 API로 교체한다.

App.jsx

import React, { useState } from 'react';
import NewsList from './NewsList';

function App() {
  // API를 넘겨받을 state 선언
  const [data, setData] = useState(null);

  const onClick = async () => {
    // axios 라이브러리로 apic call
    try {
      const response = await axios.get(
        'https://newsapi.org/v2/top-headlines?country=kr&apiKey=756cb0def47e492787f3e66f5dfb4af5',
      );
      // 응답 data state 저장
      setData(response.data);
    } catch (e) {
      console.log(e);
    }
  };

  return <NewsList />;
}

export default App;


4) UI 만들기

참고한 블로그글에선 스타일 컴포넌트 라이브러리를 사용하길래 이번 기회에 경험해보기로 함.

스타일 컴포넌트 라이브러리 사용 방법

  1. 프로젝트 폴더에 스타일 컴포넌트를 추가한다.
yarn add styled-components
  1. 적용할 프로젝트 파일에 임포트하여 사용한다.
import styled from 'styled-components';

newsAPI의 파라미터 구성

newsAPI에서 불러온 애용을 보면 각 아이템은 다음 파라미터로 구성 되어있다.

  • author
  • content
  • description: 내용
  • publishedAt
  • source
  • id
  • name
  • title: 제목
  • url: 링크
  • urlTolmage: 뉴스 이미지

중 아래의 4가지 파라미터를 사용한다.

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

NewsItem.jsx

import React from 'react';
import styled from 'styled-components';

const NewsItemBlock = styled.div`
  display: flex;
  .thumbnail {
    img {
      margin-right: 1rem;
      width: 160px;
      height: 160px;
      object-fit: cover;
    }
  }
  .contents {
    h2 {
      margin: 0;
      a {
        color: block;
      }
    }
    p {
      margin: 0;
      line-height: 1.5;
      margin-top: 0.5rem;
      white-space: normal;
    }
  }
  & + & {
    margin-top: 3ream;
  }
`;

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

NewsList.jsx

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

const NewsItemBlock = 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',
};

export default function NewsList() {
  return (
    <NewsItemBlock>
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
      <NewsItem article={sampleArticle} />
    </NewsItemBlock>
  );
}

App.jsx

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

function App() {
  return <NewsList />;
}

export default App;

결과


5) 데이터 연동하기

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

주의할 점:
async를 useEffect 사용 시 붙여 사용하면 안 된다. useEffect의 반환값이 뒷정리 목적의 함수이기 때문이다.
=> async/await를 사용하려면 함수 내부에서 처리를 해줘야 한다.

NewsList.jsx

API를 호출하여 map으로 컴포넌트에 API 배열 데이터를 props로 넘겨준다.

  • articles: API 배열을 담을 state
  • loading: API 호출 시간 동안 화면에 보여줄 로딩 state

try catch 문을 사용하여 API 호출 실패 예외처리를 선언하고 호출시간 동안 보여줄 로딩 영역을 설정한다.

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

const NewsItemBlock = 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 function NewsList() {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(null);

  useEffect(() => {
    // async 비동기 함수호출
    const fetchData = async () => {
      // API 호출 시간 동안 보여줄 로딩바
      setLoading(true);
      // try catch문 에러 처리
      try {
        const response = await axios.get(
          'https://newsapi.org/v2/top-headlines?country=kr&apiKey=756cb0def47e492787f3e66f5dfb4af5',
        );
        // API 데이터 state 저장
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
      setLoading(false);
    };
    fetchData();
  }, []);

  // 대기 중
  if (loading) {
    return <NewsItemBlock>대기 중입니다...</NewsItemBlock>;
  }
  // articles 값이 설정 안될 경우
  if (!articles) {
    return null;
  }

  // articles 값이 유효할 때
  return (
    <NewsItemBlock>
      {articles.map((v) => (
        <NewsItem key={v.url} article={v} />
      ))}
    </NewsItemBlock>
  );
}


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

  • business
  • science
  • entertainment
  • sports
  • health
  • technology

App.jsx에서 선택한 카테고리를 관리할 state를 추가한다.

App.jsx

import React, { useCallback, useState } from 'react';
import NewsList from './components/NewsList';
import Categories from './components/Categories';

function App() {
  // 기본 카테고리 state 선언
  const [category, setCategory] = useState('all');
  // 콜백으로 사용할 카테고리 함수
  const onSelect = useCallback((Category) => setCategory(Category), []);

  return (
    <>
      {/* props로 카테고리 state와 함수를 넘겨준다 */}
      <Categories category={category} onSelect={onSelect} />
      <NewsList category={category} />
    </>
  );
}

export default App;

Categories.jsx

카테고리 데이터는 배열로 만들어 선언한다. props로 넘겨받은 함수와 state를 사용해 카테고리 클릭시 active로 컴포넌트를 변경해주도록 한다.

import React from 'react';
import styled, { css } from 'styled-components';

// 카테고리 배열 생성
const categories = [
  {
    name: 'all',
    text: '전체보기',
  },
  {
    name: 'business',
    text: '비즈니스',
  },
  {
    name: 'science',
    text: '과학',
  },
  {
    name: 'entertainment',
    text: '연예',
  },
  {
    name: 'sports',
    text: '스포츠',
  },
  {
    name: 'health',
    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;
  }
`;

export default function Categories({ onSelect, category }) {
  return (
    <CategoriesBlock>
      {categories.map((v) => (
        <Category
          key={v.name}
          active={category === v.name}
          onClick={() => onSelect(v.name)}
        >
          {v.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
}

카테고리가 활성화 되었다.


7) 활성화된 카테고리 API 지정하기

카테고리를 변경하면 뉴스 API도 변경되도록 한다.

App.jsx의 NewsList 컴포넌트에 props로 category state를 넘겨줬었다. 그 state로 API를 호출하면 API 변경시 NewList가 자동으로 랜더링 된다.

NewsList.jsx

props로 전달해준 catgory state를 선언하고 그 값에 따라 API를 호출할 수 있도록 query 파라미터를 만들어서 API에 넣어준다.

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

const NewsItemBlock = 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 function NewsList({ category }) {
  const [articles, setArticles] = useState(null);
  const [loading, setLoading] = useState(null);

  useEffect(() => {
    // async 비동기 함수호출
    const fetchData = async () => {
      // API 호출 시간 동안 보여줄 로딩바
      setLoading(true);
      // try catch문 에러 처리
      try {
        // props로 넘어온 state로
        const query = category === 'all' ? '' : `&category=${category}`; // all은 빈값
        const response = await axios.get(
          `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=756cb0def47e492787f3e66f5dfb4af5`,
        );
        // API 데이터 state 저장
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
      setLoading(false);
    };
    fetchData();
  }, [category]);

  // 대기 중
  if (loading) {
    return <NewsItemBlock>대기 중입니다...</NewsItemBlock>;
  }

  // articles 값이 설정 안될 경우
  if (!articles) {
    return null;
  }

  // articles 값이 유효할 때
  return (
    <NewsItemBlock>
      {articles.map((v) => (
        <NewsItem key={v.url} article={v} />
      ))}
    </NewsItemBlock>
  );
}


8) 리액트 라우터 적용하기

이전에 state로 관리했던 카테고리 값을 라우터로 변경한다.

React Router v6 업데이트
이번 프로젝트에서는 리액트 라우터 v5를 사용한다고 함.

잉 v6 설치했는데... 이전에 v6를 짧게 공부했었으니까 블로그 글 참고해서 해봐야겠다. 어떻게 바꿔야 할지 모르겠어서 우선은 v5.3.0을 설치했음

index.jsx

라우터를 적용한다.

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>,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

사담 및 메모:
index.jsx에 브라우저라우터를 적용하고 App.jsx에서 route path를 설정해주는 거구나

스트릭모드라는 태그로 감싸져있었는데 그건 뭐지?
<React.StrictMode> 는 애플리케이션 내의 잠재적인 문제를 알아내기 위한 도구로 Fragment와 같이 UI를 랜더링 하지 않고 자손들에 대한 부가적인 검사와 경고를 활성화한다. 개발 모드에서만 활성화되고 프로덕션 빌드에는 영향을 끼치지 않는다.

NewsPage.jsx

match에서 parms를 접근하면 undefined 에러가 발생해서 useParams hook을 대체 사용했다고 함.

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

export default function NewPage() {
  // 카테고리가 선택되지 않았을 경우 기본값 'all'
  const parms = useParams();
  const category = parms.category || 'all'; // 이게 무슨 문법이지?

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

App.jsx

라우트 경로를 설정해준다.

path에서 ?의 의미는 category값이 선택적으로 들어간다는 의미로, 있을 수도 없을 수도 있는 or 같은 방식의 문법이다. (값이 없으면 all)

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

function App() {
  // // 기본 카테고리 state 선언
  // const [category, setCategory] = useState('all');
  // // 콜백으로 사용할 카테고리 함수
  // const onSelect = useCallback((Category) => setCategory(Category), []);

  return <Route path="/:category?" component={NewsPage} />;
}

export default App;

Categories.jsx

props가 없어졌으므로 선택한 카테고리에 css를 적용 가능하도록 NavLink로 대체한다.

import React from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';

// 카테고리 배열 생성
const categories = [
  {
    name: 'all',
    text: '전체보기',
  },
  {
    name: 'business',
    text: '비즈니스',
  },
  {
    name: 'science',
    text: '과학',
  },
  {
    name: 'entertainment',
    text: '연예',
  },
  {
    name: 'sports',
    text: '스포츠',
  },
  {
    name: 'health',
    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;
  }
`;

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

결과

url에 카테고리 이름이 추가되는 것을 확인할 수 있다.


피드백 및 질의응답

스터디 이후 추가

4주차 태스크:
.

profile
Backend

0개의 댓글