리엑트 JS로 만들기 - 최근 검색어 구현
최근 검색어 이름, 검색일자, 삭제 버튼이 목록 형태로 탭 아래 위치한다
최근 검색어 데이터를 저장하고 있는 historyData
를 받아오는 getter와 가장 최근 시간대가 최상위 리스트로 올라가게 하기 위한 sort()를 하나 만들어주자!!
getHistoryList() {
// TODO
return this.storage.historyData.sort(this._sortHistory);
}
_sortHistory(history1, history2) {
// 날짜를 역순으로 정렬해주기 위해서 만듬
return history2.date > history1.date;
}
state
에 최근 검색어를 다룰 state값을 추가해주고, componentDidMount()
에서도 최근 검색어 데이터를 받아오도록 하자!!
constructor() {
super();
this.state = {
searchKeyword: "",
searchResult: [],
submitted: false,
selectedTab: TabType.KEYWORD,
keywordList: [],
// TODO
historyList: [],
};
}
componentDidMount() {
const keywordList = store.getKeywordList();
const historyList = store.getHistoryList();
this.setState({ keywordList, historyList });
}
이제 render()
에 첨부해줄 historyList
라는 엘리먼트를 생성한다.
formatRelativeDate(date)
은 helper.js
에서 만들어놓은 시간을 계산하는 메소드이다.
나중에 다시 사용할 때가 많은 것같으니, 첨부해두겠다.
// 1
export function formatRelativeDate(date = new Date()) {
const TEN_SECOND = 10 * 1000;
const A_MINUTE = 60 * 1000;
const A_HOUR = 60 * A_MINUTE;
const A_DAY = 24 * A_HOUR;
const diff = new Date() - date;
if (diff < TEN_SECOND) return `방금 전`;
if (diff < A_MINUTE) return `${Math.floor(diff / 1000)}초 전`;
if (diff < A_HOUR) return `${Math.floor(diff / 1000 / 60)}분 전`;
if (diff < A_DAY) return `${Math.floor(diff / 1000 / 60 / 24)}시간 전`;
return date.toLocaleString("ko-KR", {
hour12: false,
dateStyle: "medium",
});
}
// 2
export function createPastDate(date = 1, now = new Date()) {
if (date < 1) throw "date는 1 이상입니다";
const yesterday = new Date(now.setDate(now.getDate() - 1));
if (date === 1) return yesterday;
return createPastDate(date - 1, yesterday);
}
const historyList = (
<ul className="list">
{this.state.historyList.map(({ id, keyword, date }) => (
<li key={id}>
<span>{keyword}</span>
{/* <span>{date}</span> */}
<span> {formatRelativeDate(date)} </span>
<button className="btn-remove"></button>
</li>
))}
</ul>
- 진행시 발생했던 에러 사항
- 처음에는
<span>{date}</span>
을 짚어넣으려고 했다. 그러나, 무지막지한 에러가 터졌는데, 이유는new Date()
가 객체를 반환하기 때문에, 객체는 태그 안에 문자열처럼 짚어넣을 수가 없기 때문이다.
- 처음에는
이후에, tabs
에서 추천 검색어를 추가했듯이, 추가해주면 된다.
const tabs = (
<>
<ul className="tabs">
{Object.values(TabType).map((tabType) => (
<li
key={tabType}
className={this.state.selectedTab === tabType ? "active" : ""}
onClick={() => this.setState({ selectedTab: tabType })}
>
{TabLabel[tabType]}
</li>
))}
</ul>
{this.state.selectedTab === TabType.KEYWORD && keywordList}
// 추가된 부분
{this.state.selectedTab === TabType.HISTORY && historyList}
</>
);
목록에서 검색어를 클릭하면 선택된 검색어로 검색 결과 화면으로 이동한다
const historyList = (
<ul className="list">
{this.state.historyList.map(({ id, keyword, date }) => (
// 추가된 부분
<li key={id} onClick={() => this.search(keyword)}>
<span>{keyword}</span>
{/* <span>{date}</span> */}
<span> {formatRelativeDate(date)} </span>
<button className="btn-remove"></button>
</li>
))}
</ul>
onClick={() => this.search(keyword)}
을 달아주고, search()
에서 state값을 초기화 해주면, 간단히 해결된다.
search(searchKeyword) {
const searchResult = store.search(searchKeyword);
this.setState({
searchKeyword,
searchResult,
submitted: true,
});
목록에서 x 버튼을 클릭하면 선택된 검색어가 목록에서 삭제된다
storage
객체를 관리하는 Store
class 안에 선택된 검색어를 삭제해줄 메소드를 만들었다.
=> 처음에는 왜 구지 Class
로 만들어서 Data를 관리해야 하나 했는데, storage
객체만을 관리해줄 메소드들을 따로 모아놓을 수있어서 가독성 면에서도 사용성 면에서도 좋은 것같다.
removeHistory(keyword) {
// TODO
// 강제로 지우는 게 아니라, 필터를 통해서 얘만 빼주고
// 렌더링을 하는 방법도 있구나
// storage 값을 새로 세팅함
this.storage.historyData = this.storage.historyData.filter(
(history) => history.keyword !== keyword
);
}
removeHistory(keyword)
라는 메소드를 Store
class 안에 새로 만들어줬다.
handleClickRemoveHistory(event, keyword) {
// 실제 history 데이터는 우리 store가 관리하고 있다.
// store에 삭제하는 removeHistory(keyword)를 호출하면 되겠다.
store.removeHistory(keyword);
const historyList = store.getHistoryList();
this.setState({ historyList });
}
🙅 이벤트 전파 차단하기 🙅
- 발생한 문제
=> 검색 기록이 삭제는 됐지만, 리셋 버튼이 아닌, 검색기록 버튼을 누른 것처럼 작동이 됐다.
이유를 분석해보자!!
<button>
에도 이벤트 핸들러가 달려있고, 그 부모 태그인 <li>
에도 이벤트 핸들러가 달려있다.
즉, 버튼의 이벤트가 위로 계속 버블링 되어서, <li>
에 있는 이벤트 핸들러와 동작하게 되어있다.
- 버블링에 관한 더 자세한 내용은 이벤트 버블링, 이벤트 캡처 그리고 이벤트 위임까지 written by 캡틴판교
그리고 <li>
에 달린 이벤트는 클릭시, 해당 검색결과를 보여주는 이벤트이다.
그렇기 때문에, <button>
에서는 이벤트가 위로 버블링 되지 않도록, 이벤트 전파를 차단해야 한다.
즉, event.stopPropagation();
을 넣어주자!!
handleClickRemoveHistory(event, keyword) {
// TODO
// 실제 history 데이터는 우리 store가 관리하고 있죠.
// store에 삭제하는 removeHistory(keyword)를 호출하면 되겠다.
// 이벤트 전파를 막는 메소드
event.stopPropagation();
store.removeHistory(keyword);
const historyList = store.getHistoryList();
this.setState({ historyList });
}
=> "추천 검색어"로 갖다가 돌아와서 삭제된 검색어 리스트는 유지되있다.
검색시마다 최근 검색어 목록에 추가된다
- 추천 검색어에서 특정 검색어를 하나 검색하고 나면, 그 검색어가 최근 검색어로 들어가야 된다는 요구사항이다.
- 검색 시마다, 최근 검색어에 해당 검색어가 등록 되어야한다.
Store.js
에서
search(keyword) {
// TODO
this.addHistory(keyword);
return this.storage.productData.filter((product) =>
product.name.includes(keyword)
);
}
검색할 때마다, 검색이력이 추가되도록 this.addHistory(keyword);
를 추가했다.
// 에러 방지를 위해, keyword가 없는 경우는 빈문자열로 초기화한다.
addHistory(keyword = "") {
// 좌우의 공백을 trim()으로 제거한다.
keyword = keyword.trim();
if (!keyword) {
return;
}
// 동일한 최근 검색어가 있으면, 옛날 기록은 지우고,
// 새로 삽입한다.
const hasHistory = this.storage.historyData.some(
(history) => history.keyword === keyword
);
if (hasHistory) this.removeHistory(keyword);
// 바로 다음 숫자의 ID를 생성하는 메소드임
// helper.js에서 생성해놓은 것
const id = createNextId(this.storage.historyData);
const date = new Date();
this.storage.historyData.push({ id, keyword, date });
// 가장 최근 검색어가 최상단으로 올라가게 정렬해준다.
this.storage.historyData = this.storage.historyData.sort(this._sortHistory);
}
addHistory()
는 단순히 검색어를 추가하는 것이 아니라, 동일한 검색어를 과거에 검색했다면, 시간대를 최신 시간대로 변경해준다.
또한, 검색어 리스트를 가장 최신 검색어는 최상단에 위치하도록 sort해줬다.
// helper.js
export function createNextId(list = []) {
// 가장 큰 아이디 값을 찾고, 거기에서 하나 더 증가한 값을 배출한다.
// 이렇게 되면, id가 낮은 순서대로 오지 않아도, 가장 큰 숫자를 구할 수 있다.
return Math.max(...list.map((item) => item.id)) + 1;
}
createNextId(list = [])
방식이나 (Array.length)+1
으로 id
를 추가하는 것이나 큰 차이는 없다.
_sortHistory(history1, history2) {
// 날짜의 역순으로 정렬한다.
// 가장 최근 날짜가 최상단에 위치한다.
return history2.date - history1.date;
}
⚠ sort()
구현시, 주의사항 ⚠
강사님은
return history2.date > history1.date;
으로 구현했다.
그러나, 내 로컬 환경에서는 부등호를 이용하는 방식은 동작하지 않았다.
그래서 부등호(비교 연산) 대신, 산술 연산식(뺄셈식)으로 변경하니 올바르게 정렬이 되었다.
이유를 검색해보니, 브라우저에서 javascript를 해석하는 엔진의 차이였다.
크롬에서는 부등호 연산자로 sort가 되지 않았지만,
강사님께서 사용하신 파이어폭스로 해보니, sort 함수에 비교 연산, 산술 연산 둘 다 올바르게 정렬이 되었다.
=> 결론은, 크롬이나 크로미움 기반의 브라우저에서는 sort 함수 구현시, 산술 연산 방식으로 해야 올바르게 나온다.
// main.js
search(searchKeyword) {
const searchResult = store.search(searchKeyword);
// TODO
const historyList = store.getHistoryList();
this.setState({
searchKeyword,
searchResult,
// TODO
// 새로운 최근 검색어 내역을 반영하기 위해
historyList,
submitted: true,
});
}
바닐라 JS vs React
- 바닐라 JS
MVC
모델에서Model
과View
는 서로 독립적으로 각자의 역할을 수행한다.Model
은 모든 데이터를 관리하고View
는 DOM과 밀접하게 움직이면서 화면을 그린다.Controller
는 이 둘을 관리하면서 어플리케이션 상태와 이에 따를 UI를 수시로 변화시킨다.
- React
Model
은 스토리지만 갖고 있고 다른 어플리케이션 상태는 컴포넌트로 위임한다. 컴포넌트의state
로 관리되는 상태는render()
함수가 반환하는 리액트 엘리먼트와 유기적으로 연결되어 화면에 출력된다. 즉state
의 변화가 UI까지 자동으로 영향을 끼치는 것이다.
어플리케이션 상태가 변할 때마다 수시로 돔을 조작했던View
와 달리 리액트는 DOM 앞단에 가상돔 계층을 추가해 가상돔을 수정하게끔 한다. 수시로 변경 요청을 받은 가상돔은 최소한의 DOM API만을 사용해 화면을 효율적으로 렌더링한다.
=> 리액트를 사용해 리액티브 프로그래밍과 가상돔을 이용한 성능 개선을 할 수 있었다.
해당 github 링크
Author And Source
이 문제에 관하여(리엑트 JS로 만들기 - 최근 검색어 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@gil0127/리엑트-JS로-만들기-최근-검색어-구현저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)