
이제 이번 프로젝트의 마지막 과정입니다. 지난 포스팅까지 해서 목록을 구성하고 탭 메뉴로 각자의 탭에 맞는 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;
여기까지가 마무리입니다. 컴파일과 번들링을 수행하고 실행하면 결과를 확인할 수 있습니다. 검색과 탭 기능 모두 정상적으로 동작하네요.
[검색] 기능 확인

[탭 이동] 확인

'React > React 기초 (List - TypeScript)' 카테고리의 다른 글
React 기초 (목록 - TypeScript) 10 - Github (0) | 2022.07.12 |
---|---|
React 기초 (목록 - TypeScript) 08 - Tab 버튼 구현(#2) (0) | 2022.07.10 |
React 기초 (목록 - TypeScript) 07 - Tab 버튼 구현(#1) (0) | 2022.07.08 |
React 기초 (목록 - TypeScript) 06 - ListView UI 구성 (0) | 2022.07.08 |
React 기초 (목록 - TypeScript) 05 - API 호출 (0) | 2022.07.07 |