[Project] News Viewer

thisisyjin·2022년 5월 2일
0

Dev Log 🐥

목록 보기
2/23

News Viewer

프로젝트 소개

  • 뉴스 API (newsapi.org)의 데이터를 불러와서 화면에 렌더링.
  • 카테고리별로 분류하여 조회 가능.
  • 클릭시 해당 기사로 넘어감.

🚀 스택

  • react
  • javaScript
  • styled-components
  • axios (API- GET)

📁 구조

├── public
└── src
    ├── lib
    │     └── usePromise
    ├── components
    │   ├── pages
    │   │     └── NewsPage
    │   ├── Categories
    │   ├── NewsItem
    │   └── NewsList
    ├── App
    └── index

❕ 의존성

  • create-react-app
"dependencies": {
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^13.0.0",
    "@testing-library/user-event": "^13.2.1",
    "axios": "^0.27.2",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "react-router-dom": "^6.3.0",
    "react-scripts": "5.0.1",
    "styled-components": "^5.3.5",
    "web-vitals": "^2.1.0"
  },

⚙️ Setting

$ git clone https://github.com/thisisyjin/news-viewer.git

$ npm install

$ npm start || yarn start

🙋‍♀️ 복습

styled-components

내 개발 블로그 에 따로 포스팅해두었다.

axios

  • HTTP 요청을 Promise 기반으로 처리.

Promise

const App = () => {
  const [data, setData] = useState(null);
  const onClickBtn = () => {
    axios.get('https://jsonplaceholder.typicode.com/todos/1').then(response => {
      setData(response.data);
    });
  };
 ...
  • data 라는 state를 정의한 후,
    1) axios.get(url)로 데이터를 가져온다.
    2) .then()으로 후속 처리 -> response.data를 받아온다.

async/await

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);
    }
  };
  • 화살표 함수에서 async를 쓰려면 async () => 와 같이 나타냄.
  • await axois.get을 response라는 변수에 저장한 후,
  • data(state)를 response.data로 변경함.
  • await은 Promise를 기다리기 위해 사용된다.
  • await은 async function 내부에서만 사용할 수 있다.
  • async 함수의 경우 크게 보면 try {요청} catch (e) {에러시} 로 구성된다.

전체 구조 + 데이터 흐름

1. App.js

  • Routes, Route 설정
  • NewsPage 렌더링함.

2. index.js

  • BrowserRouter 안에 App 렌더링함.
  • index.html의 #root에 render()

3. components/NewsList.js

  • useState, useEffect 사용.
  • state는 articlesloading이 있음.
    -> articles는 기사들, 즉 데이터.
  • async함수를 이용하여 API를 불러옴.
    (useEffect 콜백 내부에 정의한 후, 호출해야함.)
  • try-catch문 전,후에 setLoading을 해줌.
  • response = await axios.get(url)
  • setArticles(response.data.articles) 로 articles state에 데이터를 저장함.
    -> response.data는 response(응답) 스키마의 data 필드이다.(=서버가 제공한 데이터)

    -> API에서 articles는 위와 같음. (구조 참조)
  • useEffect의 deps에는 처음엔 빈 배열로 넣어줌. (추후 카테고리 추가시 수정함)
    -> componentDidMount 역할.

if(loading) 이면 '대기중'을 렌더링하고,
if(!articles) 면 아래 return에서 map함수에서 오류가 나지 않도록 필터링 해준다.

articles(state)는 배열이므로, 배열 메서드인 map을 이용하여 각각의 요소들을
NewsItem 컴포넌트로 렌더링해줌.
NewsItem 컴포넌트에 article이라는 Props를 넘겨줌. (각각의 요소 자신)

4. components/NewsItem.js

  • styled-components(NewsItemBlock)
  • NewsList로부터 article이라는 props를 전달받음.
    -> article.title, description, url, urlToImage (구조분해 할당)

5. components/Category.js

  • categories 라는 배열로 실제로 나타낼 text(한글)과 name(영어)를 객체 형태로 나타냄.
  • styled-components(CategoriesBlock)
  • Category 컴포넌트는 Styled(NavLink)로 함.

NavLink는 react-router의 내장 컴포넌트 중 하나로,
현재 라우터와 일치하면 스타일 or 클래스를 부여하는 컴포넌트이다.

  • 전체를 CategoriesBlock으로 감싸주고,
    categories 배열을 map 함수로 각각을 Category 컴포넌트로 렌더링함.
  • Category 컴포넌트는 NavLink 컴포넌트이므로, to 프로퍼티를 가지며 (=path)
    className은 isActive시 'active'라는 클래스를 갖도록 함.
    -> 참고로, isActive는 현재 route가 일치할 때 true임.

참고 - NavLink의 isActive

// 1️⃣ 클래스 적용시
className={isActive =>
    "nav-link" + (!isActive ? " unselected" : "")
  }
// 또는 
	activeClassName="selected"
// 2️⃣ 스타일 적용시
style={isActive => ({
    color: isActive ? "green" : "blue"
  })
}

Category 컴포넌트가 active 라는 클래스를 가지면,

&.active {
        font-weight: 600;
        border-bottom: 2px solid #22b8cf;
        color: #22b8cf;
        &:hover {
            color: #3bc9db;
        }
    }

위처럼 클래스 선택자에 의해 스타일이 적용된다.

 to={c.name === 'all' ? '/' : `/${c.name}`

또한, to 프로퍼티에는 c.name이 'all'이라면 경로는 /가 되게 하고,
아니라면 /c.name이 되도록 설정한다.

6. pages/NewsPage.js

  • react-router-dom의 내장 Hooks인useParams 사용.
  • const params = useParams;를 한 후에
    params.category를 category라는 변수에 저장한다.
    -> NewsList 컴포넌트에 props로 category를 전달한다.
    (만약, params가 없다면? - 'all'로 기본값을 설정.)
const params = useParams();
const category = params.category || 'all';
  • return값은 Category 컴포넌트와 NewsList 컴포넌트.
    +) React.Fragment <></>

7. NewsList.js 수정

const query = category === 'all' ? '' : `&category=${category}`;
  • NewsPage로부터 props로 받은 category로 query를 결정함.

  • category가 'all'이라면 ''로, 아니라면 category로 결정.
    -> 참고로, category는 useParams로 받아온 params의 값이다.
    (params=''인 경우에는 category='all'로 기본값.)

  • await axios.get(url)에서 url 부분을 수정해준다.
    -> ES6 태그드 템플릿 리터럴로 작성함.

`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=(인증키)`

country=kr 뒤부분에 바로 query를 넣어줌.
-> 이 url은 동적으로 바뀌는 url이 됨.

1) params 값에 따라서 -> 2) category 값이 설정 -> props로 넘겨줌
-> 3) query값이 설정 -> 4) axios.get을 요청할 url이 동적 설정.


Custom Hook

  • 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];
}
  1. state는 loading, resolved, error 총 세가지를 가짐.
    -> usePromise 함수가 return 하는 값은 모두 state들임.

  2. process 라는 함수를 useEffect 내부에서 정의함.
    -> async 함수이므로 선언한 후 사용해야 함.

  3. try(fetch부분) catch(에러시) 구조로 이루어지며,
    try-catch문 이전에는 setLoading(true)
    try-catch문 이후에는 setLoading(false)를 해줌.

  4. resolved = await promiseCreator()을 해줌.
    여기서 promiseCreator는 usePromise의 첫번째 인자로 받는 함수이다.
    -> axios.get(url)을 하면 된다.

  • 사용 예>
const [loading, response, error] = usePromise(() => {
        const query = category === 'all' ? '' : `${category}`;
        return axios.get(
            `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=(인증키)`
        );
    }, [category]);
  • return 값이 [loading, resolved, error]이므로 구조분해할당으로 받는다.
  • 첫번째 인자로는 query변수 선언과 return axios.get(url)을 넣어준다. (여러줄이므로 () => {}로 작성)
    -> resolved = await axios.get(url)이 되고, setResolved(resolved)를 해준다.
  • 참고로, const resolved에서 resolved는 state가 아닌 변수이다.
    -> setResolved로 바꾼 값이 state임.
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글