본문 바로가기

공부일지/Project

비전공자의 프로젝트 만들기) 검색API를 이용한 검색 프로젝트 만들기

728x90
반응형
반응형
728x90

검색API를 이용한 검색 프로젝트 만들기

 

이번에는 Rapidapi의 api를 사용해서 구글 검색기능의 프로젝트를 만들어 보려고 한다.

 

 

작업에 앞서 먼저 이번 프로젝트에서 사용할 라이브러리는 아래의 목록과 같다.

  • react-loader-spinner
  • axios
  • react-paginate
  • react-query
  • react-router-dom
  • autoperfixer
  • postcss
  • tailwind

먼저 프로젝트에 components 폴더를 만들어서 컴포넌트들을 만든다.

 

  • NavigationBar.jsx : 검색의 각 탭을 나타낸다.
  • SearchInput.jsx : 검색어를 입력 후 엔터를 누르면 파라미터로 해당 검색어를 전송한다.
  • SearchTermRequired.jsx : 검색어가 입력되지 않았을 때 표출 될 '검색어를 입력해 주세요.' 컴포넌트 따로 빼지 않아도 되는데 나중에 해당 프로젝트를 좀 더 다양하게 수정하거나 할 때 사용하기 위해 빼놓았다.
  • AllResult.jsx : 통합 검색 리스트가 나오는 컴포넌트
  • ImageResult.jsx : 이미지 검색 리스트 나오는 컴포넌트

 

SearchInput.jsx

import React, { useState, useEffect, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'

function SearchInput() {
    const [searchText, setSearchText] = useState('');
    const [searchParams, setSearchParams] = useSearchParams();

    useEffect(()=>{
        setSearchText(searchParams.get("q") ?? '');
    },[searchParams])

    const onChangeInput = useCallback((e) => {
        setSearchText(e.target.value);
    }, [])
    const onKeyUp = useCallback((e) => {
        if(e.key === 'Enter' && e.target.value.trim().length > 0){
            setSearchParams({q: e.target.value})
        }
    }, [setSearchParams])



    return (
        <input 
            value={searchText}
            type="text"
            className='w-96 h-11 bg-slate-50 border outline-none p-6 text-black mt-10 shadow-md hover:shadow-lg'
            placeholder='검색어를 입력해 주세요'
            onChange={onChangeInput}
            onKeyUp={onKeyUp}
        />
    )
}

export default SearchInput

 

이번에 tailwind 라이브러리를 처음 사용해 보았는데 이게 편하다고 해야될지 불편하다고 해야 될지 애매한 그런 느낌이었다. 어찌 보면 간단한 css를 입력하지 않아도 돼서 좋은 거 같은데 class가 너무 길어져서 개인적으로 나는 class에 해당 엘리먼트들의 네이밍을 해주는 용도로만 사용하는 게 깔끔하고 괜찮은 거 같은데 저렇게 내가 디자인한 css들을 모두 보여준다는 게 조금 안 좋아 보였다.

 

코드들을 하나씩 보자면

const [searchText, setSearchText] = useState('');
const [searchParams, setSearchParams] = useSearchParams();

useEffect(()=>{
    setSearchText(searchParams.get("q") ?? '');
},[searchParams])

const onChangeInput = useCallback((e) => {
    setSearchText(e.target.value);
}, [])
const onKeyUp = useCallback((e) => {
    if(e.key === 'Enter' && e.target.value.trim().length > 0){
        setSearchParams({q: e.target.value})
    }
}, [setSearchParams])

useState는 함수형 리액트에서 항상 사용하는 상태를 관리하는 기능이라 패스 할께요.

 

useSearchParams는 웹페이지에 데이터를 전달하는 가장 간단한 방법이 주소에 ?a=123 이런 식으로 파라미터를 보내주는 방식인데 이 부분을 QueryString이라고 한다. 

 

데이터 저장은 useState와 같은 방식으로 set으로 보내고 set에 문제가 없으면 searchParams에 저장된다. 

하지만 저장된 데이터를 가져올 때는 state와 다르다 state는 저장된 변수만 그대로 가져오면 되는데 useSearchParams는 데이터를 가져올때 .get()을 사용해 해당 키값의 데이터를 가져온다.

나는 작업할 때 q라고 키값을 정하고 엔터키를 눌렀다 땠을때 와 input의 value의 글자수가 0 이상일 때 setSearchParams에 저장하는 방식을 사용했다. 

 

이번에는 onChagne에 e => setSearchText(e)를 사용하지 않은 이유는 useCallback이랑 조금 더 친해지고 싶어서? 익숙해지고 싶어서 사용하지 않았다.

 

개인적으로 input의 value를 수정하는 데는 큰 차이는 없는 거 같다. callback이 말 그대로 요청한 걸 반환해 주는 기능이다 보니 굳이 글자 변경하는 데는 큰 차이를 느끼진 못했다.

 

그리고 혹시라도 주소창에 바로 파라미터를 입력해서 검색할 때도 있을 수 있어서 effect를 사용해서 searchParams의 데이터가 변경되면 setSearchText에도 q의 키값을 가지고 있는 value를 사용하도록 하였다.

 

 

NavigationBar.jsx

 

import React from 'react'
import { NavLink, useLocation } from 'react-router-dom'

function NavigationBar() {

    

    const {search} = useLocation();

    return (
        <div className='flex justify-center items-center mt-10 w-full h-16 border-2 border-t-green-500'>
            <NavLink to={`/all${search}`} className={({isActive}) => isActive ? "text-green-500 m-20" : 'm-20'}>통합</NavLink>
            <NavLink to={`/news${search}`} className={({isActive}) => isActive ? "text-green-500 m-20" : 'm-20'}>뉴스</NavLink>
            <NavLink to={`/photo${search}`} className={({isActive}) => isActive ? "text-green-500 m-20" : 'm-20'}>이미지</NavLink>
        </div>
    )
}

export default NavigationBar

Nav부분은 크게 별 다른 코드들은 없다. 

useLocation의 search 값을 가져와 페이지가 이동해도 파라미터의 정보를 그대로 동일하게 보여주게 하였고. 

NavLink의 속성인 isActive를 사용해 true면 해당 클래스를 아니면 아무런 클래스도 안 나오게 하였다.

 

그리고 중요한 index.js!!! 처음에는 npm start를 했을 때 왜 에러가 나지 했는데 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';
import { QueryClient, QueryClientProvider } from 'react-query';

const root = ReactDOM.createRoot(document.getElementById('root'));

const queryClient = new QueryClient()

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>
);

// 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.js의 코드도 첨부드려요.

 

이제 검색 리스트를 나오게 할 api를 소개해드릴게요. 처음에는 Nav에도 작업했더니 News의 정보도 나오게 하려고 했는데 해당 Api는 이제 없어져서 찾은 게 검색과 이미지 리스트였어요.

 

https://rapidapi.com/neoscrap-net/api/google-search72

 

Google Search API Documentation (neoscrap-net) | RapidAPI

 

rapidapi.com

제가 사용한 api에요 여기서 무료를 사용하시면 한 달에 50번의 요청은 무료할 수 있어서 해당 api를 선택했어요.

위의 api 말고도 다양한 api들이 많으니 본인에게 맞는 api를 찾아서 사용하시면 될 거 같아요.

 

AllResult.jsx

function AllResult() {
    const location = useLocation();
    const search = new URLSearchParams(location.search).get('q');
    const itemsPerPage = 10;
    const [currentItems, setCurrentItems] = useState([])
    const [pageCount, setPageCount] = useState(0)
    const [itemOffset, setItemOffset] = useState(0)

    const {data, isLoading} = useQuery(
        ['allResult', search],
        () => 
            axios.get(
                `https://google-search72.p.rapidapi.com/search?q=${search}&num=20`,
                {headers: {
                    'X-RapidAPI-Key': '개인키',
                    'X-RapidAPI-Host': '개인호스트'
                }}
            ).then(data => data?.data),
            {
                refetchOnWindowFocus : false,
                enabled: !!search,
                cacheTime:0
            }
    );

    useEffect(() => {
        const endOffset = itemOffset + itemsPerPage
        setCurrentItems(data?.items.slice(itemOffset, endOffset))
        setPageCount(Math.ceil((data?.items?.length ?? 0)/itemsPerPage))

    }, [itemOffset, data?.items])
    
    const handlePageClick = useCallback((e)=>{
        setItemOffset(e.selected * itemsPerPage % data?.items?.length);
    }, [data?.items])

    if(!search) return <SearchTermRequired />
    if(isLoading) 
    return (
        <Oval
            ariaLabel='loading-indicator'
            height={100}
            width={100}
            strokeWidth={5}
            color="#22C55E"
            secondaryColor='white'
            wrapperClass='flex justify-center items-center mt-52'
        />
    )

    return (
        <>
            <div className='flex flex-col justify-cnet align-middle m-auto w-[700px]'>
                { 
                    data.items.length > 0 
                        ? currentItems?.map(({title, link, htmlSnippet}, idx) => (
                            <div key={idx} className='mt-10'>
                                <a href={link} target='_blank' title="새창열림" rel='noreferrer'>
                                    <p className='text-sm'>
                                        {link.length > 40 ? link.substring(0,40)+'...' : link}
                                    </p>
                                    <p className='text-lg text-blue-700 hover:underline'>
                                        {title}
                                    </p>
                                    <p className='text-xs'>
                                        {htmlSnippet}
                                    </p>
                                </a>
                            </div>
                            ))
                        :   <div className='flex justify-center items-center m-auto h-96'>
                                <p className='text-3xl text-gray-400'>
                                    검색결과가 없습니다.
                                </p>
                            </div>
                }
            </div>
            <ReactPaginate
                breakLabel='...'
                nextLabel='>>'
                previousLabel='<<'
                pageRangeDisplayed={10}
                pageLinkClassName="-ml-[1] text-[#22C55E] bg-slate-50 block border-solid border border-[#dee2e6] px-[0.75rem] py-[0.375rem] hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                previousLinkClassName="text-[#22C55E] bg-slate-50 block border-solid border border-[#dee2e6] px-[0.75rem] py-[0.375rem] hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                nextLinkClassName="-ml-[1] text-[#22C55E] bg-slate-50 block border-solid border border-[#dee2e6] px-[0.75rem] py-[0.375rem] hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                breakLinkClassName="-ml-[1] text-[#22C55E] bg-slate-50 block border-solid border border-[#dee2e6] px-[0.75rem] py-[0.375rem] hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                containerClassName="flex ml-auto mr-auto w-fit mt-10 pb-10 select-none"
                activeLinkClassName="z-[3] text-slate-50 bg-[#22C55E] border-[#22C55E] focus:text-[#e9ecef] focus:z-[3] focus:bg-[#22C55E] focus:outline-0 hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                disabledLinkClassName="text-[#6c757d] pointer-events-none bg-slate-50 border-[#dee2e6] hover:z-[2] hover:text-[#22C55E] hover:bg-[#e9ecef] hover:border-[#dee2e6]"
                renderOnZeroPageCount={null}
                onPageChange={handlePageClick}
                pageCount={pageCount}
            />
        </>
    )
}

해당 컴포넌트의 데이터가 엄청 길어요. 이 부분을 만들고 나면 image도 동일하게 사용하거라 image는 따로 코드는 올리지 않을게요.

 

const location = useLocation();
const search = new URLSearchParams(location.search).get('q');

Location으로 해당 페이지의 url을 들고 와서 URLSearchParams에 넣어줘요. 그리고 넣어주실 때는 search의 속성만 지정해서 넣으면 됩니다.

 

그럼 쿼리스트링의 정보를 전달받은 URLSearchParams에서 get 함수를 사용해 q의 키값의 데이터를 가져와요.

 

const {data, isLoading} = useQuery(
    ['allResult', search],
    () => 
        axios.get(
            `https://google-search72.p.rapidapi.com/search?q=${search}&num=20`,
            {headers: {
                'X-RapidAPI-Key': '개인키',
                'X-RapidAPI-Host': '개인호스트'
            }}
        ).then(data => data?.data),
        {
            refetchOnWindowFocus : false,
            enabled: !!search,
            cacheTime:0
        }
);

 

그럼 저희가 작성한 쿼리스트링의 데이터 값이 나오겠죠? 그걸 useQuery를 사용해서 데이터를 저장해요.

useQuery은 서버로부터 데이터를 조회해 올 때 사용합니다. 배열의 queryKey에 allResult, search라는 키를 배정합니다.

여기서 위에 파라미터의 데이터가 search라고 했는데 왜 queryKey에도 선언을 했냐면 쿼리키와 같은 배열 안에 있는 state가 변경될 때마다 refetching 하게 된다고 한다.

search값이 바뀔 때마다 refetching 되어 데이터를 다시 가져오기 때문에 search를 키값으로 함께 넣었습니다.

 

api사이트에 받은 api의 정보를 이제 api호출할 수 있게 axios를 사용해서 데이터를 가져올 거예요.

 

api호출 정보는 api 구매하시면 나와요.

return에서 map을 사용해 데이터를 뿌려주는 건 따로 설명하지 않을게요 그것도 api 홈페이지에 어떤 데이터들의 속성과 이름으로 뿌려지는지 api마다 달라서요 구매하신 api의 정보를 보시면 나올 거 같아요.

 

페이징 부분인데

 

const itemsPerPage = 10;
const [currentItems, setCurrentItems] = useState([])
const [pageCount, setPageCount] = useState(0)
const [itemOffset, setItemOffset] = useState(0)

useEffect(() => {
    const endOffset = itemOffset + itemsPerPage
    setCurrentItems(data?.items.slice(itemOffset, endOffset))
    setPageCount(Math.ceil((data?.items?.length ?? 0)/itemsPerPage))

}, [itemOffset, data?.items])

const handlePageClick = useCallback((e)=>{
    setItemOffset(e.selected * itemsPerPage % data?.items?.length);
}, [data?.items])

 

제가 사용한 api는 데이터를 최대 20개 밖에 가져오지 못하기 때문에 2페이지 밖에 안 떠서 조금 아쉽더라고요.

 

itemsPerPage에 기본적으로 페이지에 몇 개의 리스트를 보여줄 건지 정하고 전체 나온 data에서 리스트만큼 slice 해서 새로운 배열을 만들고 그 배열을 뿌려줘요.

 

2페이지를 눌렀을 때는 11번부터 20번까지 보여주는 그런 방식이에요. 

page 같은 경우에는 디자인하는데 시간이 오래 걸려서 그렇지 코드는 시작, 끝의 번호를 정하고 그거에 맞게 데이터를 배열로 나누고 보여주고 하는 방식이라 한번 해보시면 바로 이해하실 거예요.

 

오늘은 간단하게 구글 api를 사용해 검색을 하는 기능을 만들었어요. 

state는 이제 어느 정도 이해를 하는데 callback이나 effect 같은 거는 아직 100프로 이해가 어렵네요.

 

그래도 하나씩 더 만들어 가다 보면 이해하는 날이 오겠죠?

 

728x90
반응형