본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

이제 이번 프로젝트의 마지막 과정입니다. 지난 포스팅까지 해서 목록을 구성하고 탭 메뉴로 각자의 탭에 맞는 Naver API를 호출하여 목록을 변경하는 부분까지 해보았습니다. 컴포넌트 간 상태 및 데이터를 공유하기 위해서는 Recoil을 설치하고 사용해 보았습니다. 이제 이번 프로젝트의 마지막으로 키워드 검색 상자를 만들고 키워드에 맞는 검색 결과를 조회해 보겠습니다.

 

먼저 이제까지 진행한 프로젝트의 폴더 및 파일 구조를 한 번 확인하겠습니다.

혹시나 빠진 파일이 있으신 분들은 한 번 체크해 보시면 좋을 것 같습니다. 먼저 목표 UI를 확인해 보겠습니다. 저번 TypeScript를 사용하지 않고 React만 사용해서 개발했을 때 아래와 같이 UI를 확인했었고, 이번에도 동일합니다.

 

 

UI는 위와 같이 구성하고, 스크롤 영역에는 포함시키지 않고 상단에 고정합니다. 키워드를 입력하고 검색 버튼을 터치하면 현재 탭과 검색 키워드에 맞는 검색 결과를 표시합니다.

 

/src/component/ 경로에 searchbar.component.tsx 파일을 생성하고 아래와 같이 UI Rendering 부분을 작성합니다. UI를 구성하는 부분만 있어서 딱히 JavaScript 버전과 다른 내용이 없습니다.

import React from "react";

const SearchBar = () => {
    return (
        <div className="header">
            <input type="text" className="iptSearch" id="keyword" />
            <button type="button" className="search">
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

 

생성한 SearchBar 컴포넌트를 사용하기 위해 app.tsx에 SearchBar 컴포넌트를 추가합니다.

import { atom, RecoilRoot, RecoilState } from "recoil";
import ListView from "./component/listview.component";
import SearchBar from "./component/searchbar.component";
import TabList from "./component/tablist.component";

export const selectedTabId: RecoilState<string> = atom({
    key: 'tabId',
    default: 'news'
});

const App = () => (
    <RecoilRoot>
        <div>
            {/* ADD :: Start */}
            <SearchBar />
            {/* ADD :: End */}
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

상단에 고정할 수 있도록 stylesheet를 아래와 같이 대강 추가해 줍니다.

.header{padding:10px;border-bottom:1px solid #DDD;position:fixed;top:0;display:inline-block;z-index:100;width:100%;background-color:#FFF;}
.iptSearch{width:calc(100% - 110px);border:1px solid #EEE;line-height:27px;padding:0px 10px;margin-right:10px;}
button.search{padding:5px 10px;}

 

Header 영역에 검색 상자를 추가하면서 목록 영역은 상단 부분의 자리를 좀 비켜줘야 합니다. 목록 영역의 class인 listArea class에 상단 여백을 추가합니다.

.listArea{width:100%;margin-bottom:50px;margin-top:52px;}

 

 

여기까지 진행을 하면 검색 상자에 대한 UI는 구성을 마쳤습니다. 컴파일과 번들링을 진행해서 UI를 확인하고 넘어가겠습니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

 

 

기능 구현은 아직 하지 않아, 검색이 되지는 않습니다. UI를 완성했으니 이제 검색 기능을 구현해 보겠습니다. 먼저 app.tsx 에 검색 키워드를 관리할 atom을 하나 추가로 생성합니다. 기본 검색어는 "코로나"로 지정했습니다.

import { atom, RecoilRoot, RecoilState } from "recoil";
import ListView from "./component/listview.component";
import SearchBar from "./component/searchbar.component";
import TabList from "./component/tablist.component";

export const selectedTabId: RecoilState<string> = atom({
    key: 'tabId',
    default: 'news'
});

//ADD :: Start
export const searchKeyword: RecoilState<string> = atom({
    key: 'keyword',
    default: '코로나'
});
//ADD :: End

const App = () => (
    <RecoilRoot>
        <div>
            <SearchBar />
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

다음은 SearchBar 컴포넌트에서 검색어를 입력하고 [검색] 버튼을 터치하면 입력한 검색어를 상태 값에 저장하는 기능을 구현합니다. 아래와 같이 먼저 RecoilState를 선언한 후 [검색] 버튼을 터치하면 setter를 호출하여 상태 값에 저장합니다.

import { useRecoilState } from "recoil";
import { searchKeyword } from "../app";

const SearchBar = () => {
	//ADD :: Start
    const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

    const search = () => {
        const searchKeyword: string = (document.querySelector('#keyword') as HTMLInputElement).value;
        setKeyword(searchKeyword);
    };
    //ADD :: End

    return (
        <div className="header">
            <input type="text" className="iptSearch" id="keyword" />
            {/* onClick 추가 */}
            <button type="button" className="search" onClick={search}>
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

 

SearchBar 컴포넌트에서 searchKeyword atom에 값을 저장하는 부분을 구현하였으니 이제 ListView 컴포넌트에서 searchKeyword atom에서 검색 키워드를 추출해서 해당 검색어로 검색하도록 수정합니다.

 

먼저 SearchBar 컴포넌트와 동일하게 useRecoilState를 선언합니다.

const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

 

useEffect를 구현한 부분에 keyword를 하드코딩 값이 아닌 keyword 값으로 변경해주고, 탭 아이디나 키워드가 변경될 때마다 인터페이스를 수행하도록 변경합니다.

useEffect(() => {
    apiGet(selTabId, keyword);
}, [selTabId, keyword]);

 

ListView 컴포넌트의 전체 소스코드는 아래와 같습니다.

import moment from "moment";
import { useEffect, useState } from "react";
import { useRecoilState } from "recoil";
import { searchKeyword, selectedTabId } from "../app";
import { IBookData, IHttpResp, INewsData } from "../interface/apidata.interface";

const NewsRow = ({ title, pubDate, description }: INewsData) => {  
    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}} />
            </div>
            <div className="cont">
                <span className="date">{moment(pubDate).format('YYYY.MM.DD HH:mm')}</span>
                <span dangerouslySetInnerHTML={{__html: description}} />
            </div>
        </li>
    )
};

const BookRow = ({ image, title, author, description }: IBookData) => {
    return (
        <li>
            <a href="#" className="bookRow">
                <div className="bookImg">
                    <img src={image} />
                </div>
                <div className="bookDesc">
                    <div className="title" dangerouslySetInnerHTML={{__html: title}} />
                    <div className="cont">
                        <span className="author">{author}</span>
                        <span dangerouslySetInnerHTML={{__html: description}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

const ListView = () => {
    const [articles, setArticles] = useState<IHttpResp | null>(null);
    const [selTabId, setSelTabId] = useRecoilState<string>(selectedTabId);
    const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

    const search = () => {
        const searchKeyword: string = (document.querySelector('#keyword') as HTMLInputElement).value;
        setKeyword(searchKeyword);
    };

    const apiGet = async(type: string, param: string) => {
        const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        })
        .then((resp: Response) => resp.json())
        .then((resp: IHttpResp) => {
            const typedResp: IHttpResp = {
                ...resp,
                type: type
            }
            setArticles(typedResp);
        });
    };

    useEffect(() => {
        apiGet(selTabId, keyword);
    }, [selTabId, keyword]);

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                articles.items &&
                (
                    (articles.type === 'news') ?
                        (articles.items as INewsData[]).map((v: INewsData, inx: number) => {
                            return <NewsRow key={inx} {...v} />
                        })
                    : (articles.type === 'book') ?
                        (articles.items as IBookData[]).map((v: IBookData, inx: number) => {
                            return <BookRow key={inx} {...v} />
                        })
                    : ''
                )
            }
            </ul>
        </div>
    );
};

export default ListView;

 

여기까지가 마무리입니다. 컴파일과 번들링을 수행하고 실행하면 결과를 확인할 수 있습니다. 검색과 탭 기능 모두 정상적으로 동작하네요.

 

[검색] 기능 확인

 

[탭 이동] 확인

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

지난 포스트에서 탭을 선택하여 토글하는 부분까지 확인을 했습니다. 이번 포스트에서는 선택한 탭에 따라 뉴스, 도서 인터페이스를 하고 결과를 Rendering 하는 부분까지 작성해 보겠습니다.

 

탭을 선택할 때 선택한 탭에 맞는 데이터를 인터페이스 하려면 ListView 컴포넌트가 현재 선택한 탭이 무엇인지를 알아야 합니다. 이렇게 여러 컴포넌트끼리 데이터나 상태를 공유할 때 React에서는 React Redux, Context API, Recoil 등의 상태 관리를 사용합니다.

 

대표적으로 사용하는 라이브러리에 대한 설명은 아래 링크를 참고하면 됩니다. 

 

[React Redux]

Quick Start | React Redux (react-redux.js.org)

 

Quick Start | React Redux

 

react-redux.js.org

 

[Context API]

Context – React (reactjs.org)

 

Context – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

[Recoil]

Recoil (recoiljs.org)

 

Recoil

A state management library for React.

recoiljs.org

 

Context API 같은 경우에는 React에 포함된 기능으로 추가적인 라이브러리 설치는 필요 없습니다. 하지만 여기서는 보일러 플레이트 코드가 가장 짧은 Recoil을 설치하여 사용해 보도록 하겠습니다.

 

아래 명령으로 Recoil을 설치합니다.

D:\workspace\searchNaverApi> yarn add recoil@0.7.3

 

설치 후 package.json의 dependencies 하위를 확인하면 recoil이 정상적으로 포함되어 있음을 알 수 있습니다.

"dependencies": {
    "moment": "2.29.1",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "recoil": "0.7.3"
  },

 

Recoil을 적용하기 위해서 app.tsx 소스코드를 열어 가장 상위 레벨을 <RecoilRoot>로 감싸 줍니다. <RecoilRoot> 하위에 포함된 컴포넌트 끼리는 애플리케이션의 데이터나 상태를 공유할 수 있습니다. app.tsx 소스코드를 아래와 같이 <RecoilRoot>를 최상단에 위치하도록 추가합니다.

import { RecoilRoot } from "recoil";
import ListView from "./component/listview.component";
import TabList from "./component/tablist.component";

const App = () => (
    <RecoilRoot>
        <div>
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

공유해야 할 상태 값을 atom으로 저장합니다. 관리해야 할 상태나 데이터들이 많다면 atom들을 별도의 파일로 분할하여 관리하겠지만 지금은 선택한 탭이나 다음 포스트에서 다룰 검색어 정도가 전부이므로 그냥 app.tsx에 추가하겠습니다. 이번 포스트에서는 선택한 탭 아이디를 추가합니다.

import { atom, RecoilRoot, RecoilState } from "recoil";
import ListView from "./component/listview.component";
import TabList from "./component/tablist.component";

//ADD :: Start
export const selectedTab: RecoilState<string> = atom({
    key: 'tabId',
    default: 'news'
});
//ADD :: End

const App = () => (
    <RecoilRoot>
        <div>
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

이제 Tab 컴포넌트에서 selectedTabId atom을 사용하여 상태를 변경하고, 변경된 상태를 참조하여 탭의 UI를 변경하겠습니다. 먼저 탭의 상태를 변경할 수 있도록 useRecoilState Hook을 선언합니다. useRecoilState Hook은 useState Hook과 유사한 방법으로 사용할 수 있습니다.

const [setTabId, setSelTabId] = useRecoilState<string>(selectedTabId);

 

다음은 탭을 Rendering 하는 부분에서 이 상태 변경을 이용해서 탭의 현재 상태를 변경합니다. 변경 전은 컴포넌트 생성 시에 props를 전달 받아 UI를 변경하거나, 탭을 선택하는 사용자의 click action을 받아 해당 Element의 class 속성을 변경하는 방법을 사용했는데, 변경 후에는 현재 atom에 저장된 tabId가 일치하는 탭에만 on class를 할당하는 방법으로 변경하겠습니다.

const changeTab = (id: string) => {
    setSelTabId(id);
};

return (
    <li>
        <a href="#"
            id={tabId}
            className={setTabId === tabId ? 'on' : ''}
            onClick={() => changeTab(tabId)}>
            <span>{tabName}</span>
        </a>
    </li>
)

 

changeTab 함수에서는 RecoilState에 저장한 tabId를 변경하도록 setSelTabId 함수만 호출합니다. UI를 Rendering 할 때 해당 탭의 아이디와 저장한 selTabId를 비교하여 동일한 탭일 경우 "on" class를 부여하고, 다른 탭일 경우에는 부여하지 않습니다.

 

TabList 컴포넌트에서는 탭 목록 선언 시에 on 항목을 삭제합니다. (Tab 컴포넌트에서 상태 값을 참조하는 것으로 변경하였으므로 해당 값은 더 이상 사용하지 않습니다.) TabList 및 Tab 컴포넌트에서 on 항목을 삭제하기 위해서는 ITabInfo 타입에서 먼저 on 항목을 삭제합니다.

interface ITabInfo {
    tabId: string;
    tabName: string;
}

export {
    ITabInfo
}

 

ITabInfo 타입을 변경한 후, 맞추어 변경한 tablist.component.tsx 프로그램의 소스 코드는 아래와 같습니다.

import { useRecoilState } from "recoil";
import { selectedTabId } from "../app";
import { ITabInfo } from "../interface/tabinto.interface";

const Tab = ({ tabId, tabName }: ITabInfo) => {

    const [setTabId, setSelTabId] = useRecoilState<string>(selectedTabId);

    const changeTab = (id: string) => {
        setSelTabId(id);
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={setTabId === tabId ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    )
};

const TabList = () => {
    const tabList: ITabInfo[] = [
        {tabName: '뉴스', tabId: 'news'},
        {tabName: '도서', tabId: 'book'}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList">
            {
                tabList.map((v: ITabInfo, inx: number) => {
                    return <Tab key={inx} {...v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

 

 

이제 ListView 컴포넌트에서 상태 값을 이용해 탭 변경 시 뉴스와 도서를 조회하는 부분을 작성해 보겠습니다. 상태 값을 참조해야 하므로 먼저 useRecoilState를 선언합니다.

const [setTabId, setSelTabId] = useRecoilState<string>(selectedTabId);

 

다음은 Naver API를 호출할 때 현재 선택한 탭 아이디를 참조하여 뉴스나 도서 API를 사용하도록 변경합니다. useEffect 부분을 변경하여 탭을 변경할 때마다 API를 다시 호출하도록 변경합니다. (아직 검색어에 대한 처리는 하드코딩을 유지합니다.)

useEffect(() => {
    apiGet(setTabId, '타입스크립트');
}, [setTabId]);

 

마지막으로 UI를 Rendering 부분은 이미 데이터에 따른 분기가 적용되어 있으므로 변경할 내용은 없습니다. 여기까지 작성한 listview.component.tsx 파일의 소스코드는 아래와 같습니다.

import moment from "moment";
import { useEffect, useState } from "react";
import { useRecoilState } from "recoil";
import { selectedTabId } from "../app";
import { IBookData, IHttpResp, INewsData } from "../interface/apidata.interface";

const NewsRow = ({ title, pubDate, description }: INewsData) => {  
    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}} />
            </div>
            <div className="cont">
                <span className="date">{moment(pubDate).format('YYYY.MM.DD HH:mm')}</span>
                <span dangerouslySetInnerHTML={{__html: description}} />
            </div>
        </li>
    )
};

const BookRow = ({ image, title, author, description }: IBookData) => {
    return (
        <li>
            <a href="#" className="bookRow">
                <div className="bookImg">
                    <img src={image} />
                </div>
                <div className="bookDesc">
                    <div className="title" dangerouslySetInnerHTML={{__html: title}} />
                    <div className="cont">
                        <span className="author">{author}</span>
                        <span dangerouslySetInnerHTML={{__html: description}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

const ListView = () => {
    const [articles, setArticles] = useState<IHttpResp | null>(null);
    const [setTabId, setSelTabId] = useRecoilState<string>(selectedTabId);

    const apiGet = async(type: string, param: string) => {
        const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': 'z73CVZ80v0SYgrwfwbfz',
                'X-Naver-Client-Secret': 'dFoN8oBtKB'
            }
        })
        .then((resp: Response) => resp.json())
        .then((resp: IHttpResp) => {
            const typedResp: IHttpResp = {
                ...resp,
                type: type
            }
            setArticles(typedResp);
        });
    };

    useEffect(() => {
        apiGet(setTabId, '타입스크립트');
    }, [setTabId]);

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                articles.items &&
                (
                    (articles.type === 'news') ?
                        (articles.items as INewsData[]).map((v: INewsData, inx: number) => {
                            return <NewsRow key={inx} {...v} />
                        })
                    : (articles.type === 'book') ?
                        (articles.items as IBookData[]).map((v: IBookData, inx: number) => {
                            return <BookRow key={inx} {...v} />
                        })
                    : ''
                )
            }
            </ul>
        </div>
    );
};

export default ListView;

 

컴파일 및 번들링을 진행한 후 결과를 보면 아래와 같습니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

 

[뉴스] 탭을 선택했을 경우

 

[도서] 탭을 선택했을 경우

 

이번 포스트에서는 탭 버튼 동작을 마무리 해보았습니다. Recoil을 사용해서 컴포넌트 간 상태를 공유해서 Tab 컴포넌트에서 설정한 값을 ListView 컴포넌트에서 읽어서 API 호출을 진행해 보았습니다. 다음 포스트에서는 키워드 검색 부분까지 진행해서 프로젝트를 완성해 보겠습니다.

300x250
본 포스트에서는 리액트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

저번 포스팅까지 해서 목록을 구성하고 탭 메뉴를 만들어서 탭을 전환할 때마다 탭에 맞는 Naver API를 호출하도록 해보았습니다. 현재까지 작업한 파일 및 폴더 구조는 아래와 같습니다.

먼저 만들어 볼 UI를 확인해 보겠습니다. 01. Concept 포스팅에서 보면 아래와 같습니다.

UI는 위와 같이 구성하고, 스크롤 영역에는 포함시키지 않고 상단에 고정합니다. 그리고 키워드를 입력하고 검색 버튼을 터치하면 현재 탭에 맞는 검색 결과를 표시합니다.

 

/src/component/ 경로에 searchbar.component.jsx 파일을 생성하고 아래와 같이 UI Rendering 부분을 작성합니다.

import React from "react";

const SearchBar = () => {
    return (
        <div className="header">
            <input type="text" className="iptSearch" />
            <button type="button" className="search">
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

생성한 SearchBar 컴포넌트를 사용하기 위해 app.js 에 SearchBar 컴포넌트를 추가합니다.

import React from "react";
import { atom, RecoilRoot } from "recoil";
import ListView from "./component/listview.component.jsx";
import TabList from "./component/tablist.component.jsx";
import SearchBar from "./component/searchbar.component.jsx";

export const selectedTabId = atom({
    key: 'tabId',
    default: 'news'
});

const App = () => {
    return (
        <RecoilRoot>
            <div>
                {/* ADD :: Start */}
                <SearchBar />
                {/* ADD :: End */}
                <ListView />
                <TabList />
            </div>
        </RecoilRoot>
    )
};

export default App;

상단에 고정할 수 있도록 stylesheet를 아래와 같이 대강 추가해 줍니다.

.header{padding:10px;border-bottom:1px solid #DDD;position:fixed;top:0;display:inline-block;z-index:100;width:100%;background-color:#FFF;}
.iptSearch{width:calc(100% - 110px);border:1px solid #EEE;line-height:27px;padding:0px 10px;margin-right:10px;}
button.search{padding:5px 10px;}

Header 영역이 추가되면서 목록 영역은 상단 부분의 자리를 좀 비켜줘야 합니다. 목록 영역의 class인 listArea class에 상단 여백을 추가합니다.

.listArea{width:100%;margin-bottom:50px;margin-top:52px;}

여기까지 하면 검색에 사용할 컴포넌트를 생성하고 페이지에 컴포넌트를 추가했고, 컴포넌트의 모양을 잡아주는 stylesheet도 만들었습니다. 실행시켜 보면 아래와 같은 화면을 확인할 수 있습니다.

 

UI를 어느 정도 만들었으니 이제 검색 기능을 구현해 보겠습니다. 먼저 app.js 에 검색 키워드 상태 값 지정에 필요한 atom을 하나 추가로 생성합니다. 기본 검색어는 "코로나"로 지정했습니다.

import React from "react";
import { atom, RecoilRoot } from "recoil";
import ListView from "./component/listview.component.jsx";
import TabList from "./component/tablist.component.jsx";
import SearchBar from "./component/searchbar.component.jsx";

export const selectedTabId = atom({
    key: 'tabId',
    default: 'news'
});

//ADD :: Start
export const searchKeyword = atom({
    key: 'keyword',
    default: '코로나'
});
//ADD :: End

const App = () => {
    return (
        <RecoilRoot>
            <div>
                <SearchBar />
                <ListView />
                <TabList />
            </div>
        </RecoilRoot>
    )
};

export default App;

다음은 SearchBar 컴포넌트에서 검색어를 입력하고 [검색] 버튼을 터치하면 입력한 검색어를 상태 값에 저장하는 기능을 아래와 같이 구현합니다. 먼저 RecoilState를 선언한 후 [검색] 버튼을 터치하면 setter를 호출하여 저장합니다.

import React from "react";
import { useRecoilState } from "recoil";
import { searchKeyword } from "../app";

const SearchBar = () => {
    //ADD :: Start
    const [keyword, setKeyword] = useRecoilState(searchKeyword);

    const search = () => {
        const searchKeyword = document.querySelector('#keyword').value;
        setKeyword(searchKeyword);
    };
    //ADD :: End

    return (
        <div className="header">
            <input type="text" className="iptSearch" id="keyword" />
            {/* onClick 추가 */}
            <button type="button" className="search" onClick={search}>
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

값을 저장하는 부분까지 진행했으니 이제 ListView 컴포넌트에서 저장한 값을 추출해서 사용하도록 구현합니다. 먼저 위와 동일하게 RecoilState를 하나 더 선언합니다.

const [keyword, setKeyword] = useRecoilState(searchKeyword);

useEffect를 구현한 부분에 keyword를 하드코딩 값이 아닌 keyword 값으로 변경해주고, 탭 아이디나 키워드가 변경되면 다시 인터페이스를 수행하도록 변경합니다.

useEffect(() => {
    apiGet(selTabId, keyword);
}, [selTabId, keyword]);

ListView 컴포넌트의 전체 소스코드는 아래와 같습니다.

const ListView = () => {

    const [articles, setArticles] = useState(null);
    const [selTabId, setSelTabId] = useRecoilState(selectedTabId);
    const [keyword, setKeyword] = useRecoilState(searchKeyword);

    const apiGet = async (type, param) => {
        const apiUrl = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        const resp = await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        });
        resp.json().then(data => {
            setArticles(data.items);
        });

    };

    useEffect(() => {
        apiGet(selTabId, keyword);
    }, [selTabId, keyword]);

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                (selTabId === 'news') ?
                    articles.map((v, inx) => {
                        return <NewsRow key={inx} row={v} />
                    })
                : (selTabId === 'book') ?
                    articles.map((v, inx) => {
                        return <BookRow key={inx} row={v} />
                    })
                : ''
            }
            </ul>
        </div>
    );
}

여기까지가 끝인 것 같습니다. 검색을 해보고 탭 이동을 해보면 검색 결과가 정상적으로 잘 표시됨을 확인할 수 있습니다.

 

300x250
본 포스트에서는 리액트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

저번 포스팅까지 해서 뉴스와 도서 목록을 각각 구성해서 UI까지 만들어 보았습니다. 지금은 뉴스와 도서를 번갈아가며 조회하려면 소스코드를 수정해가면서 조회해야 합니다. 이번 포스팅에서는 [뉴스]와 [도서] 탭을 만들어서 각 탭을 선택하면 원하는 목록이 조회되도록 해보겠습니다.

 

01. Concept 포스트에서 이야기한대로 탭은 화면 하단에 아래와 같은 모양으로 만들어 보겠습니다.

/src/component/ 하위에 tablist.component.jsx 파일을 하나 생성합니다. 이 파일에 탭을 구성하여 보겠습니다. 먼저 탭 한 칸에 대한 컴포넌트를 만들어 보겠습니다.

const Tab = (props) => {
    const tabName = props.tab.tabName;
    const on = props.tab.on;
    const tabId = props.tab.id;

    const changeTab = (id) => {
        document.querySelector('.tabList li a.on').classList.remove('on');
        document.querySelector('.tabList li a#' + id).classList.add('on');
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={on ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    );
};

Tab 이라는 이름의 class를 생성하고 props를 전달 받습니다. return 하는 UI는 단순합니다. <li> 아래 실제 연결 동작을 담당할 <anchor> 태그를 하나 생성하고 id 속성을 부여합니다. 탭의 초기 구성을 위해 props로 전달 받은 on 속성에 따라 className 속성을 부여합니다.

 

그리고 탭을 선택했을 경우에 동작을 담당하는 changeTab 이라는 함수를 만들고 onClick 속성에 할당합니다. UI 에 표시되는 텍스트는 props로 전달 받은 tabName을 표시합니다.

 

changeTab 함수에서 현재 하는 동작은 on/off 에 대한 class만 변경합니다. 현재 on class가 붙어 있는 <a> 태그에서 on class를 제거하고 현재 입력 받은 탭에 on class를 추가합니다.

 

다음엔 Tab 컴포넌트를 사용해 전체 탭을 표시하는 TabList 컴포넌트를 아래와 같이 작성합니다.

const TabList = () => {
    const tabList = [
        {tabName: '뉴스', id: 'news', on: true},
        {tabName: '도서', id: 'book'}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList" role="tablist">
            {
                tabList.map((v, inx) => {
                    return <Tab key={inx} tab={v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

먼저 tabList 배열을 하나 생성하고 표시할 탭 목록을 작성합니다. 우리는 "뉴스"와 "도서" 두 개의 탭을 사용할 것이므로 두 개를 만들고 초기에는 뉴스 탭을 활성화 시키기위해 뉴스 탭에는 on: true 로 세팅합니다. UI에 Rendering 할 부분은 <ul> 태그 아래 <Tab> 컴포넌트를 넣어 줍니다.

 

tablist.component.jsx의 전체 소스코드는 아래와 같습니다.

import React from "react";

const Tab = (props) => {
    const tabName = props.tab.tabName;
    const on = props.tab.on;
    const tabId = props.tab.id;

    const changeTab = (id) => {
        document.querySelector('.tabList li a.on').classList.remove('on');
        document.querySelector('.tabList li a#' + id).classList.add('on');
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={on ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    );
};

const TabList = () => {
    const tabList = [
        {tabName: '뉴스', id: 'news', on: true},
        {tabName: '도서', id: 'book'}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList" role="tablist">
            {
                tabList.map((v, inx) => {
                    return <Tab key={inx} tab={v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

이제 전에 만든 ListView 컴포넌트와 TabList 컴포넌트를 같이 사용하기 위해 별도의 프로그램을 하나 생성합니다.

/src/app.js 를 하나 만들고 아래와 같이 작성합니다. 이제부터는 main.js 에서는 app.js의 <App /> 컴포넌트를 사용하고 다른 컴포넌트들의 시작점은 app.js 에서 관리하겠습니다.

import React from "react";
import ListView from "./component/listview.component.jsx";
import TabList from "./component/tablist.component.jsx";

const App = () => {
    return (
        <div>
            <ListView />
            <TabList />
        </div>
    )
};

export default App;

main.js는 아래와 같이 수정합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app.js';

ReactDOM.render(<App />, document.getElementById('app'));

이제 TabList UI에 사용할 stylesheet를 "대강" 추가합니다. main.css 파일에 추가하면 됩니다.

.tabBox{position:fixed;bottom:0px;width:100%;height:50px;border-top:1px solid #DDD;background-color:#FFF;}
.tabList{display:table;width:100%;table-layout:fixed;}
.tabList li{display:inline-block;text-align:center;width:49%;}
.tabList li:first-child{border-right:1px solid #DDD;}
.tabList li a{display:block;color:#000;font-size:1.6rem;line-height:48px;}
.tabList li a span{display:inline-block;padding:0px 8px;width:100%;}
.tabList li a.on span{background-color:#999;color:#FFF;font-weight:700;}

여기까지 작성했으면 일단 동작은 되지 않겠지만 UI는 확인할 수 있습니다. UI를 확인해보면 아래와 같습니다.

[뉴스]와 [도서] 탭을 선택하면 아래와 같이 토글이 되는 것을 확인할 수 있습니다.

탭 토글까지 확인되었으면 이제 탭 선택에 따라 뉴스, 도서가 인터페이스 되도록 개발해 보겠습니다. 탭을 선택할 때 선택한 탭에 맞는 데이터를 인터페이스 하려면 ListView 컴포넌트가 현재 선택한 탭이 무엇인지를 알아야 합니다. 이렇게 컴포넌트에서 다른 컴포넌트 등과 데이터나 상태를 공유할 때 React에서는 React Redux, Context API, Recoil 등의 상태 관리를 사용합니다.

 

대표적으로 사용하는 라이브러리에 대한 설명은 아래 링크로 대신합니다.

 

[React Redux]

https://react-redux.js.org/tutorials/quick-start

 

Quick Start | React Redux

 

react-redux.js.org

[Context API]

https://ko.reactjs.org/docs/context.html

 

Context – React

A JavaScript library for building user interfaces

ko.reactjs.org

[Recoil]

https://recoiljs.org/ko/

 

Recoil

A state management library for React.

recoiljs.org

Context API 같은 경우에는 React에서 자체적으로 제공하는 기능으로 별도의 라이브러리 설치가 필요 없습니다. 하지만 여기서는 보일러 플레이트 코드가 가장 짧은 Recoil을 설치하여 사용해 보도록 하겠습니다.

 

아래 명령으로 Recoil을 설치합니다.

D:\workspace\searchNaverApi> yarn add recoil@0.7.3

설치 후 package.json 파일을 보면 dependencies 하위에 recoil이 정상적으로 포함되어 있음을 확인할 수 있습니다.

"dependencies": {
    "moment": "2.29.1",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "recoil": "0.7.3"
},

Recoil을 적용하기 위해 app.js 소스코드를 열어 가장 상위 레벨을 <RecoilRoot>로 감싸 줍니다. <RecoilRoot> 하위에 포함된 컴포넌트끼리는 데이터나 상태를 공유할 수 있습니다. app.js 소스코드는 아래와 같이 됩니다.

import React from "react";
import { RecoilRoot } from "recoil";
import ListView from "./component/listview.component.jsx";
import TabList from "./component/tablist.component.jsx";

const App = () => {
    return (
        <RecoilRoot>
            <div>
                <ListView />
                <TabList />
            </div>
        </RecoilRoot>
    )
};

export default App;

관리해야 할 상태나 데이터들이 많다면 별도로 파일을 분할해서 관리하겠지만 지금은 선택된 탭이나 검색 키워드 정도만 관리할 예정이므로 그냥 app.js 에 추가하겠습니다.

import React from "react";
//MODIFY :: Start
import { atom, RecoilRoot } from "recoil";
//MODIFY :: End
import ListView from "./component/listview.component.jsx";
import TabList from "./component/tablist.component.jsx";

//ADD :: Start
export const selectedTabId = atom({
    key: 'tabId',
    default: 'news'
});
//ADD :: End

const App = () => {
    return (
        <RecoilRoot>
            <div>
                <ListView />
                <TabList />
            </div>
        </RecoilRoot>
    )
};

export default App;

이제 Tab 컴포넌트에서 selectedTabId atom을 사용하여 상태를 변경하고, 변경된 상태를 참조하여 탭의 UI를 변경하겠습니다. 먼저 탭의 상태를 변경할 수 있도록 useRecoilState Hook을 선언합니다. useRecoilState Hook은 useState Hook과 유사한 방법으로 사용할 수 있습니다.

const [selTabId, setSelTabId] = useRecoilState(selectedTabId);

그 다음 탭을 Rendering 하는 부분에서 이 상태 변경을 이용해서 탭의 현재 상태를 변경하도록 합니다. 변경 전은 컴포넌트 생성 시에 props를 전달 받아 UI를 변경하거나, 탭을 선택할 때 JavaScript로 해당 Element의 class 속성을 변경하는 방법을 사용했었는데 변경 후에는 현재 atom에 저장된 값과 각 탭 아이디가 일치하는지를 확인해서 on/off 처리해 보겠습니다.

const changeTab = (id) => {
    setSelTabId(id);
};

return (
    <li>
        <a href="#"
            id={tabId}
            className={selTabId === tabId ? 'on' : ''}
            onClick={() => changeTab(tabId)}>
            <span>{tabName}</span>
        </a>
    </li>
);

changeTab 함수에서는 RecoilState에 저장한 tabId를 변경하도록 setSelTabId 함수만 호출합니다. UI를 Rendering 할 때 해당 탭의 아이디와 저장한 selTabId를 비교하여 동일한 탭일 경우 "on" class를 부여하고, 다른 탭일 경우에는 부여하지 않습니다.

 

TabList 컴포넌트에서는 탭 목록 선언 시에 on 항목을 삭제합니다. (상태 값을 참조하는 것으로 변경하였으므로 해당 값은 더 이상 사용하지 않습니다.) 변경한 tablist.component.jsx 파일의 소스코드는 아래와 같습니다.

import React from "react";
import { useRecoilState } from "recoil";
import { selectedTabId } from "../app";

const Tab = (props) => {
    const tabName = props.tab.tabName;
    const tabId = props.tab.id;

    const [selTabId, setSelTabId] = useRecoilState(selectedTabId);

    const changeTab = (id) => {
        setSelTabId(id);
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={selTabId === tabId ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    );
};

const TabList = () => {
    const tabList = [
        {tabName: '뉴스', id: 'news'},
        {tabName: '도서', id: 'book'}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList" role="tablist">
            {
                tabList.map((v, inx) => {
                    return <Tab key={inx} tab={v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

이제 ListView 컴포넌트에서 상태 값을 이용해서 탭 변경 시 뉴스와 도서를 조회하는 부분을 작성해 보겠습니다. 상태 값을 참조해야 하므로 먼저 useRecoilState를 선언합니다.

const [selTabId, setSelTabId] = useRecoilState(selectedTabId);

다음은 Naver API를 호출할 때 해당 탭 아이디를 참조하여 뉴스나 도서 API를 사용하도록 합니다. 그리고 useEffect 부분을 변경하여 탭을 변경할 경우마다 API를 조회하도록 변경합니다. (아직 검색어에 대한 처리는 하드코딩입니다.)

useEffect(() => {
    apiGet(selTabId, 'React');
}, [selTabId]);

마지막으로 UI를 Rendering 하는 부분도 저장된 탭 아이디를 참조해서 뉴스를 조회할 경우 NewsRow를, 도서를 조회할 경우 BookRow 컴포넌트를 사용하여 목록을 표시하도록 변경합니다.

return (
    <div className="listArea">
        <ul className="listView">
        {
            articles &&
            (selTabId === 'news') ?
                articles.map((v, inx) => {
                    return <NewsRow key={inx} row={v} />
                })
            : (selTabId === 'book') ?
                articles.map((v, inx) => {
                    return <BookRow key={inx} row={v} />
                })
            : ''
        }
        </ul>
    </div>
);

아래 그림과 같은 결과를 보실 수 있습니다.

 

[뉴스] 탭을 선택했을 경우

[도서] 탭을 선택했을 경우

여기까지 작성한 listview.component.jsx 파일의 소스코드는 아래와 같습니다.

import React from "react";
import { useEffect, useState } from "react"
import moment from "moment";
import { useRecoilState } from "recoil";
import { selectedTabId } from "../app";

const NewsRow = (props) => {
    const title = props.row.title;
    const pubDate = moment(props.row.pubDate).format('YYYY.MM.DD HH:mm');
    const desc = props.row.description;

    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}}></a>
            </div>
            <div className="cont">
                <span className="date">{pubDate}</span>
                <span dangerouslySetInnerHTML={{__html: desc}} />
            </div>
        </li>
    );
};

const BookRow = (props) => {
    const image = props.row.image;
    const title = props.row.title;
    const author = props.row.author;
    const desc = props.row.description;

    return (
        <li>
            <a href="#" className="bookRow">
                <div className="bookImg">
                    <img src={image} />
                </div>
                <div className="bookDesc">
                    <div className="title" dangerouslySetInnerHTML={{__html: title}} />
                    <div className="cont">
                        <span className="author">{author}</span>
                        <span dangerouslySetInnerHTML={{__html: desc}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

const ListView = () => {

    const [articles, setArticles] = useState(null);
    const [selTabId, setSelTabId] = useRecoilState(selectedTabId);

    const apiGet = async (type, param) => {
        const apiUrl = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        const resp = await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        });
        resp.json().then(data => {
            setArticles(data.items);
        });

    };

    useEffect(() => {
        apiGet(selTabId, 'React');
    }, [selTabId]);

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                (selTabId === 'news') ?
                    articles.map((v, inx) => {
                        return <NewsRow key={inx} row={v} />
                    })
                : (selTabId === 'book') ?
                    articles.map((v, inx) => {
                        return <BookRow key={inx} row={v} />
                    })
                : ''
            }
            </ul>
        </div>
    );
}

export default ListView;
300x250

+ Recent posts