본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 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

+ Recent posts