바닐라 JS로 만들때는 Views
폴더에서 TabView.js
에서 해당 요구사항을 관리하였다.
object.values( ) 사용법 => 배열 형태로 모든 value값들을 return 한다.
main.js
에서 두가지 객체를 추가해주었다.
// 얘는 <li>의 key 값으로 쓰려고 만들었다.
const TabType = {
KEYWORD: "KEYWORD",
HISTORY: "HISTORY",
};
const TabLabel = {
[TabType.KEYWORD]: "추천 검색어",
[TabType.HISTORY]: "최근 검색어",
};
render()
안에 새로운 엘리먼트를 만들어줬다.
=> render()
가독성을 위해서, 가능한 엘리먼트로 분류해주겠다.
const tabs = (
<ul className="tabs">
{Object.values(TabType).map((tabType) => {
return <li key={tabType}>{TabLabel[tabType]}</li>;
})}
</ul>
);
추천 검색어와 최근 검색어를 <ul>
안에 <li>
로 묶었다.
Object.values(TabType)
은 탭타입의 value 값들을 배열형태로 반환하기 때문에 map()
을 사용할 수 있다. map()
안에서는 해당 value 값들이 <li>
태그들의 key
역할을 한다.
또한, 탭라벨 객체의 value들을 찍어줘야하는데 [TabType.KEYWORD]
, [TabType.HISTORY]
이렇게 따로 따로 부를 수가 없다. 그래서 TabLabel[TabType[TabType.KEYWORD] ]
= TabLabel["KEYWORD"]
= 맵 안에서는 TabLabel[tabType]
이기 때문에, 이런 식으로 TabLabel
의 value값들을 호출한다.
=> 이렇게 해서 tabs
라는 엘리먼트 변수를 만들었다.
이제 이 tabs를 출력해주는 작업을 하자!!
return (
<>
<header>
<h2 className="container">검색</h2>
</header>
<div className="container">
{searchForm}
<div className="content">
{/* TODO */}
{this.state.submitted ? searchResult : tabs}
</div>
</div>
</>
);
submitted
상태에서 따라서 검색결과를 보여주거나, 탭을 보여줘야 한다.
현재 선택된 탭이 어떤 탭인가 하는 상태에 따라서 tabs
를 계산해주자!
className="active"
를 지정해주면, 해당 검색어가 선택되어진 걸로 CSS
처리가 되있다.
const TabType = {
KEYWORD: "KEYWORD",
HISTORY: "HISTORY",
};
const TabLabel = {
[TabType.KEYWORD]: "추천 검색어",
[TabType.HISTORY]: "최근 검색어",
};
this.state = {
searchKeyword: "",
searchResult: [],
submitted: false,
// TODO
// 추천 검색어를 디폴트 값으로 해놨다
selectedTab: TabType.KEYWORD,
};
tabs
에 삼항 연산자를 이용해서, state
값과 map()
안에서 돌리는 tabType
값과 일치하면, active
클래스를 넣어주도록 만들었다.
const tabs = (
<ul className="tabs">
{Object.values(TabType).map((tabType) => (
// TODO
<li
key={tabType}
// tabs 에 추가한 부분
className={this.state.selectedTab === tabType ? "active" : ""}
>
{TabLabel[tabType]}
</li>
))}
</ul>
);
<ul className="tabs">
{Object.values(TabType).map((tabType) => (
// TODO
<li
key={tabType}
className={this.state.selectedTab === tabType ? "active" : ""}
// 추가된 내용
onClick={ () => this.setState({ selectedTab: tabType }) }
///////
>
{TabLabel[tabType]}
</li>
))}
</ul>
onClick
에서 곧바로 setState
를 통해, tabType
을 변경해준다.
이러면, 추천 검색어
나 최근 검색어
클릭시, 배경색이 변경된다.
const tabs = (
<>
<ul className="tabs">
{Object.values(TabType).map((tabType) => (
// TODO
<li
key={tabType}
className={this.state.selectedTab === tabType ? "active" : ""}
onClick={() => this.setState({ selectedTab: tabType })}
>
{TabLabel[tabType]}
</li>
))}
</ul>
// 추가해준 부분
{this.state.selectedTab === TabType.KEYWORD && <> TODO: 추천 검색어 </>}
{this.state.selectedTab === TabType.HISTORY && <> TODO: 최근 검색어 </>}
</>
);
선택된 탭에 따라서 해당 "TODO:"
가 출력되도록 만들어놓았다.
실제 데이터 삽입은 추천 검색어 구현에서 다루겠다.
혹자는 탭 UI를 만드는데 왜 굳이
TabType
이나TabLabel
만들어 사용했는지 궁금할지도 모르겠다. 항상 같은 모습의 정적인 탭일 경우는 직접 UI에 탭 이름을 표시하면 되겠지만 지금은 선택한 탭을 저장하고 동적으로 표시해야하는 경우다. 선택된 탭을 식별해야 하는데 변경 가능성이 있는 이름보다는 고유한 키를 정의하는 것이 코드 유지보수면에서 더 낫다고 생각한다.
추천 검색어를 출력하려면, 추천 검색어를 출력하기 위한 state
가 필요하다.
this.state = {
searchKeyword: "",
searchResult: [],
submitted: false,
selectedTab: TabType.KEYWORD,
// TODO
// 추천검색어를 가져오기 위해
keywordList: [],
};
keywordList
에는 Store.js
에서 데이터를 받아와서, 할당할 예정이다.
그렇다면, 그 다음으로 생각해야할 것은 "어느 타이밍에 데이터를 할당할까?" 하는 문제가 생긴다.
DOM
이라면, 'submit'
이벤트가 발생하는 순간에 데이터를 넣어줄 수도 있겠다.
그러나, 리엑트에서 더 좋은 방법은 컴포넌트의 라이프 싸이클을 이용하는 방법이다.
리액트 컴포넌트는 생성부터 소멸까지 일련의 생명 주기를 갖는다.
- 컴포넌트 상태 등 초기화 작업을 완료하면 컴포넌트 객체가 생성된다(constructor)
- 그리고 리액트 앨리먼트를 이용해 가상돔을 그리고 이걸 실제 돔에 반영한다(render)
- 돔에 반영되는 것을 마운트된다라고 표현하는데 마운트가 완료되면(componentDidMount) 이벤트를 바인딩하거나 외부 데이터를 가져오는 등의 작업을 수행한다
- 컴포넌트가 사라지기 전에 즉 마운트 직전에는(compoentWillUnmount) 이벤트 핸들러를 제거하는 등 리소스 정리 작업을 한다
- 마지막으로 컴포넌트는 본인의 삶을 마감하는 순서를 따른다
각 시점별로 컴포넌트의 메서드가 호출되는데 괄호안의 함수명을 사용한다.
자료출처 : 김정환 블로그
이 내용을 참고하면, componentDidMount()
에서 외부 데이터를 가져오는 작업을 할 수있다.
componentDidMount() {
// DOM 이 마운트 되면, 호출 되는데,
// store 객체에서 최근 검색어를 가져와서
// 최근 검색어를 갱신하는 역할을 한다.
// 그러면, 리엑트 컴포넌트는 state가 변경됐다는 걸 알고
// render()를 다시 호출한다.
const keywordList = store.getKeywordList();
this.setState({ keywordList });
}
이렇게 해서 키워드목록을 담았다. 이제 실제 UI에 반영해보자!
render()
에 넣어줄 엘리먼트를 생성했다. 데이터를 받아와서, 순서가 상관없는 리스트인<ul>
형식으로 뿌려준다.
const keywordList = (
<ul className="list">
// keywordList가 객체들을 요소로 가진 배열이기 때문에,
// 구조분해할당으로 객체에서 필요한 부분들을 빼올 수있다.
{this.state.keywordList.map((item, index) => {
return (
<li key={item.id}>
<span className="number">{index + 1}</span>
<span>{item.keyword}</span>
</li>
);
})}
</ul>
);
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>
/////////////// 추가 해준 부분!!
// 조건부가 참이라면, keywordList를 넣어줘라
{this.state.selectedTab === TabType.KEYWORD && keywordList}
</>
);
const keywordList = (
<ul className="list">
{this.state.keywordList.map(({ id, keyword }, index) => (
// TODO => 추가 해준 onClick()
<li key={id} onClick={(e) => this.search(keyword)}>
<span className="number">{index + 1}</span>
<span>{keyword}</span>
</li>
))}
</ul>
);
클릭 이벤트를 처리해주기 위해, onClick을 달아주었다. 또한, 클릭 후, 검색어에 해당하는 데이터를 뿌려주기 위해서, 비슷한 동작을 하는 search()
를 재활용했다.
이렇게만 하면, 추천 검색어 클릭시, 해당 검색어에 따른 데이터가 나온다.
search(searchKeyword) {
const searchResult = store.search(searchKeyword);
this.setState({
// TODO
searchResult,
submitted: true,
searchKeyword,
});
}
또한, 검색어창에도 클릭한 추천 검색어가 들어가도록 조치해주기 위해서, searchKeyword
즉 해당 <li>
에서 온 keyword
가 state
값에 반영되도록 해줬다.