리엑트 JS로 만들기 - 탭 구현, 추천 검색어 구현

조 은길·2022년 2월 12일
0

React

목록 보기
5/12
post-thumbnail

추천 검색어, 최근 검색어 탭이 검색폼 아래 위치한다

바닐라 JS로 만들때는 Views폴더에서 TabView.js에서 해당 요구사항을 관리하였다.

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>에서 온 keywordstate값에 반영되도록 해줬다.


해당 github 링크

profile
좋은 길로만 가는 "조은길"입니다😁

0개의 댓글