본문 바로가기

Computer Programming/[리액트를 다루는 기술]

[리액트를 다루는 기술] 14. API 연동 뉴스 뷰어 (NavLink, useParams, usePromise Custom Hook)

Reference: 리액트를 다루는 기술 - 김민준

https://newsapi.org/s/south-korea-news-api

 

여기서 API를 연동하여 뉴스 뷰어를 만들어보자

 

 

 

한국 뉴스를 가져오는 API에 대한 설명서를 볼 수 있는데, axios로 fetch해온 후 보여주는 컴포넌트를 작성했다. 그러면 아래처럼 데이터를 볼 수 있는데, 이제 이 데이터를 가지고 화면에 잘 보여주면 된다.

 

 

이번에는 scss 전처리기 말고 styled-component를 활용할 예정이다.

 

이제 useEffect를 사용해서 컴포넌트가 처음 렌더링 되는 시점에 API를 요청하면 되는데 여기서 useEffect에 async를 붙이면 안된다.

왜냐면 useEffect 의 리턴값은 뒷정리함수이고 async는 Promise 객체를 반환하기 때문이다.

 

 

 

따라서 useEffect 내부에서 async/await을 사용하고 싶다면 또 다른 함수를 그 안에 만들어서 내장해 사용해야한다. 

 

//NewsList.js
useEffect(() => {
    const fetchData = async () => {
        setLoading(true);
        try {
            const response = await axios.get("https://newsapi.org/v2/top-headlines?country=kr&apiKey=213efe280f8443fcabd141b11f4d00d2")
            setArticles(response.data.articles)
        } catch (e) {
            console.log(e)
        }
        setLoading(false);
    }
    fetchData();
},[])

 

아직 데이터가 준비되지 않았다면 보여지지 않게 한다. (로딩일 때 로딩중) 그리고 map을 돌기 전 articles 를 조회하여 해당 값이 null이 아닌지 검사해야한다. 

 

 

 

if (loading) {
    return <NewsListBlock>로딩중...</NewsListBlock>
}

if (!articles) {
    return null;
}

 

 

 

 

map을 사용할 때는 꼭 key를 넣어줘야 DOM 업데이트때 효율성을 높일 수 있다. (업데이트 및 삭제 시)

<NewsListBlock>
    {articles.map(article => (
        <NewsItem key={article.url} article={article}/>
    ))}
</NewsListBlock>

 

 

다음은 카테고리를 선택하면 해당하는 카테고리의 뉴스만 보여주는 기능을 구현해보자

 

선택한 <Category> styled-component 에 스타일을 다르게 적용하는게 좋다. styled-component를 사용하기 때문에 props를 받아와서 active 일 경우 이 스타일을 적용할 수 있다! 

${props => props.active && css`
    font-weight: 600;
    border-bottom: 2px solid #22b8cf;
    color: #22b8cf;
    &:hover {
        color: #3bc9db;
    }
`}

 

props에 active 속성을 넣어주면 된다.

const Categories = ({ category, onSelect }) => {

    return (
        <CategoriesBlock>
            {categories.map(c => (
                <Category
                    key={c.name}
                    //selected(active)일 경우 props 줘서 다르게 적용
                    active={category === c.name}
                    onClick={() => onSelect(c.name)}>
                    {c.text}
                </Category>
            ))}
        </CategoriesBlock>
    )
}

 

 

 

 

이제 데이터를 가져오면 되는데 newsapi의 url 파라미터에는 아래와 같은 옵션들이 있다.

https://newsapi.org/docs/endpoints/sources

여기서 카테고리에 selected category를 적용해주면 원하는 category의 뉴스리스트를 보여준다.

 

 

//NewsList.js

useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            try {
                // 쿼리문 따로 선언하기 (all 카테고리는 없으므로 빈 스트링 주기)
                const query = category === "all" ? "" : `&category=${category}`
                const response = await axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
                setArticles(response.data.articles)
            } catch (e) {
                console.log(e)
            }
            setLoading(false);
        }
        fetchData();
        //카테고리가 바뀔 때 리렌더링 필요 (새로 뉴스 불러오기)
    },[category])

default가 all 이고 all은 옵션에 없어서 쿼리문에 넣으면 안되기 때문에 따로 빼서 삼항연산자로 쿼리를 할당한다.

 

 

 

 

 

 

잘 보여지고 있지만 

이번에는 리액트 라우터를 써서 URL 파라미터를 이용하는것으로 바꿔보자

 

 

👇

 

 

Categories안의 Category 는 NavLink로 바꿔서 styled-component 를 다시 적용해줬다. active 일 때 스타일 다르게 하려면 props로 전달했는데, NavLink는 자체적으로 isActive 라는 boolean 값을 가지고 있으므로 아래와 같이 사용하면 된다.

 

style = {({isActive}) => (isActive ? 'active' : undefined )}

 

그러면 동적으로 클래스네임에 active 일경우 active 가 추가된다. (selected 된 경우)

 

 

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 React.memo(Categories)

 

속성명도 className으로 바꿔야한다.

 

 

그리고 Newspage.js 를 만들어 "/"으로 사용한다.

const Newspage = () => {
    const params = useParams();
    const category = params.category || 'all' //없으면 기본값 all 적용
    return (
        <div>
            <NewsList category={category}/>
        </div>
    )
}

선택된 카테고리가 없으면 'all'로 기본값을 적용하고 그렇지 않으면 params 에 들어있는 값을 NewsList로 전달해 category를 보내준다.

이렇게 하면 페이지 전환이 되는데, 만약 프로젝트의 크기가 더 커질 경우 useState을 이용해 상태를 관리하는 것 보다 ux를 향상시킬 수 있다.

 

 

 

마지막으로, API 호출처럼 비동기적 처리를 위해 Promise를 사용하는 경우 더욱 간결하게 코드를 작성할 수 있도록 custom hook을 만들어보자.

 

 

src 디렉토리 안에 lib 디렉토리를 만들고 그 안에 커스텀 훅을 넣으면 된다.

 

lib 디렉토리는 주로 프로젝트에서 재사용 가능한 코드를 관리하는 용도로 사용되며 API 요청을 처리하거나 데이터를 가공하는 함수, 유틸리티 함수나 상수 등을 넣어 관리한다.

 

=> 즉 공통적으로 사용하는 코드를 모듈화해서 중복을 피하고 유지보수성을 개선하기 위해 사용함!

 

 

usePromise.js

import { useEffect } from "react";
import { useState } from "react"

const 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) //dependencies eslint 경고 무시
    return [loading, resolved, error] //상태 return
}

export default usePromise

 

 

주의할 점!

usePromise의 return

    return [loading, resolved, error] //상태 return

 

 

NewsList에서 사용할 때 구조분해할당

const [loading, response, error] =usePromise(()=>{
        const query = category === 'all' ? '' : `/${category}`
        //promise 객체를 건네줘야 함
        return axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
    },[category])

 

loading, response, error로 받아서 resolved 일때의 response를 전달받음. 이후 articles는 response.data로 접근해서 사용하기!

dependencies도 두번째 인자로 잘 넘겨주고

 

 

 

 

가독성있게 articles로 할당해 추가해주면 좋다.

const { articles } = response.data

 

 

const NewsList = ({ category }) => {
    const [loading, response, error] =usePromise(()=>{
        const query = category === 'all' ? '' : `&category=${category}`
        //promise 객체를 건네줘야 함
        return axios.get(`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=213efe280f8443fcabd141b11f4d00d2`)
    },[category])


    if (loading) {
        return <NewsListBlock active={loading === true}>로딩중...</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 React.memo(NewsList)

 

 

custom hook 만들어서 사용하는 부분이 조금  어려웠지만 어떤 값이 리턴되고, 어떤 값을 받아와서 사용하는지에 대한 큰 그림을 그려서 이해해야하는 것 같다. 이렇게 함수를 모듈화시켜서 사용하는것이 훨씬 가독성있고 간결하다 👻

 

 

 

 

 

 


[리액트를 다루는 기술] 책을 바탕으로 만들었습니다.