TIL 22-05-01

thisisyjin·2022년 5월 1일
0

TIL 📝

목록 보기
35/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 14

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

외부 API 연동 - 뉴스 뷰어

카테고리 기능 구현

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

화면에 렌더링시 영어로 보여주지 않고, 한글로 보여주도록.

1) Categoris.js 생성

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

const Categories = () => {
    return (
        <CategoriesBlock>
            {categories.map(c => (<Category key={c.name}>{c.text}</Category>))}
        </CategoriesBlock>
    );
};


export default Categories;
  • categories 배열 안에 실제 이름 text(영문)와 name(한글)을 저장함.
  • CategoriesBlock이라는 전체를 감싸는 div 스타일 컴포넌트와
    Category라는 각 카테고리를 꾸미는 스타일 컴포넌트를 만듬.

RESULT

  • 카테고리 목록이 상단에 나타났다.
    -> categories 배열의 name필드가 나타나야함. (한글)

  • category의 상태를 useState로 관리 -> App.js 에서.
  • category 값을 업데이트하는 onSelect 함수 추가.
  • category, onSelect를 Categories 컴포넌트로 전달. (props)
  • category 값을 NewsList 컴포넌트에도 전달해줌.

2) App.js (useState, props)

App.js

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

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;
  • category(state)는 현재 클릭한 카테고리로 이동할 수 있게 해주는 상태이고,
  • onSelect는 클릭한 카테고리로 setCategory해주는 함수이다.

3) Categories.js (css, props)

Categoriesjs

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;

  1. styled-components 에서는 props를 불러올 수 있음.
  • props.active가 true면 css함수 적용.
  • css ``는 여러줄의 조건 스타일을 적용하려 할 때 사용함.
 ${props =>
    props.active && css`
        font-weight: 600;
        border-bottom: 2px solid #22b8cf;
        color: #22b8cf;
        &:hover {
            color: #3bc9db;
        }
    `}

-> 이 구문처럼 사용하므로, 형식 외워두자.

&{props => props.active && css 스타일}

  1. props
<Category
    key={c.name}
    active={category === c.name}
    onClick={() => onSelect(c.name)}
>
  • Category 컴포넌트에 props로 active를 줌.
    -> category === c.name이 true면 active도 true.
    -> 위에서 조건절 style이 적용됨.
  • 카테고리 클릭시 onClick->onSelect(c.name)에 의해 category(state)값이 c.name이 됨.
  • 그리고 active props가 true가 됨.
  • 위 조건절 스타일에 의해 css함수 내부 스타일이 적용됨.

API 호출 시 카테고리 지정

이제 클릭시 스타일이 적용되는 것은 구현했으니,
데이터 연동시 카테고리를 지정하는 것을 구현해야 한다.

NewsList.js (수정)

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

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);

          // 🔻 수정된 부분 (query + url 템플릿 리터럴)
            try {
                const query = category === 'all' ? '' : `&category=${category}`;
                const response = await axios.get(
                    `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f6052deb31a34f38b602753e2ddf0daf`
                );
                setArticles(response.data.articles);
            } catch (e) {
                console.log(e);
            }

            setLoading(false);
        };

        fetchData();
    }, [category]);

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

    if (!articles) {
        return null;
    }
    return (
        <NewsListBlock>
            {articles.map(article => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    );
};

export default NewsList;
  • category를 props로 받아왔기 때문에 category 값에 따라 현재 카테고리를 지정하여 API를 요청할 수 있도록.
  • 현재 category값에 따라 요청할 주소가 동적으로 바뀜.
    -> 'all'이면 query를 공백으로 설정함.

추가로 category 값이 바뀔 때마다 뉴스를 새로 불러와야 하므로, useEffect의 deps에 category를 넣어줌.

이 컴포넌트는 componentDidMount와 componentDidUpdate시 요청을 시작하도록 설정해야 함.
-> useEffect에서는 한번에 설정할 수 있음. (두개를 합쳐놓은 기능임)

RESULT


리액트 라우터 적용

  • 기존에는 카테고리 값을 App 컴포넌트에서 state로 관리했지만,
    이번에는 URL 파라미터를 이용하여 관리해보자.

1) 리액트 라우터 설치

$ yarn add react-router-dom

2) index.js 설정

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

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

3) NewsPage 컴포넌트 생성

src/pages/NewsPage.js

import { useParams } from "react-router-dom";
import Categories from "../Categories";
import NewsList from "../NewsList";

const NewsPage = () => {
    const params = useParams();

    const category = params.category || 'all';

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

export default NewsPage;
  • useParams로 만든 params의 category를 NewsList에 props로 보냄.
  • category가 ''이면 기본값은 'all'로.

App.js (Route 설정)

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

const App = () => {

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

export default App;
  • category가 있어도 NewsPage, 없어도 NewsPage를 보여줘야 하므로 두번 작성함.
  • path="/" 일 때는 params.category가 ''이므로 category는 저절로 'all'이 된다.
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';

const categories = [
    ...
];
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}
                    className={({ isActive }) => (isActive ? 'active' : undefined)}
                    to={c.name === 'all' ? '/' : `${c.name}`}
                >
                    {c.text}
                </Category>))}
        </CategoriesBlock>
    );
};;

export default Categories;
const query = category === 'all' ? '' : `&category=${category}`;
	 const response = await axios.get(
	     `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f6052deb31a34f38b602753e2ddf0daf`
	 );
  1. NavLink 컴포넌트 임포트
  2. Category 컴포넌트 -> styled(NavLink)
  3. active라는 props를 이용하지 않고, 조건부로 class를 부여함.
    -> className={({ isActive }) => (isActive ? 'active' : undefined)}
  4. to라는 props를 전달하여 'all'이면 '/'로, 아니면 /c.name으로

RESULT

  • 좌측하단 주소 (URL params) 주목.

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();
    }, deps);

    return [loading, resolved, error];
}
  • 매개변수 deps는 배열의 형태로 받아야 한다.
  • 마지막 부분에 deps에서 esLint 경고가 발생하는데, '빠른 수정...'을 클릭하면
    ESLint 규칙을 비활성화시키는 주석이 입력됨.
    // eslint-disable-next-line react-hooks/exhaustive-deps

커스텀 Hook 사용

  • NewList 컴포넌트에서 API를 불러오므로, 여기서 usePromise를 사용해보자.
  • return [loading, resolved, error] 이므로, 각 값을 구조분해 할당으로 받아온다.
import styled from 'styled-components';
import NewsItem from './NewsItem';
import axios from 'axios';
import usePromise from '../lib/usePromise';

const NewsListBlock = styled.div`
    ...
`;

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

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

    if (!response) {
        return null;
    }

    if (error) {
        return <NewsListBlock>에러 발생!</NewsListBlock>
    }

    const { articles } = response.data;
    return (
        <NewsListBlock>
            {articles.map(article => (
                <NewsItem key={article.url} article={article} />
            ))}
        </NewsListBlock>
    );
};

export default NewsList;

중요

  • useEffect 내부 함수에는 async/await을 직접 작성하면 안된다.
  • 함수 내부에 async함수를 따로 등록한 후, 호출하는 방식으로 사용해야 한다.

styled-components 문서 참조하기.

참고 - API가 많아지면 요청을 위한 상태관리가 번거로움. (리덕스로 해결)

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

0개의 댓글