Etc

장보기 todo-List 만들기2(recoil 학습)

꼬드리 2024. 1. 16. 22:36

🚩Recoil

1) 검색 기능 추가하기 (Recoil)

 

지난번에 스터디원 분께 말씀드린 대로 야심차게 검색 기능을 추가 하였다.

처음엔 그저 평범한 todo-list라는 명목으로 시작했으나 만들다보니 '장 보기 목록(어쨌든 장보기도 todo 아닌가?)'느낌으로 진행되었기 때문에… 만약 장 보기 목록이 아주 길어진다면 한눈에 이 아이템을 적었는지 알기 어려울 것이다. 이런 곤란한 경우를 대비하여 검색 기능을 활용해보기로 하였다.

 

우선, 완성된 레이아웃은 아래와 같다.

function Header() {
    const [searchValue, setSearchValue] = useRecoilState(searchState);
    const setFilteredTodo = useSetRecoilState(filteredListState);
    const todo = useRecoilValue(listState);
    
    const showList = ()=>{
        searchItems();
        setSearchValue('');
    }

    const searchItems = () =>{
        const filteredItems = todo.filter(item=>
          item.content.toLowerCase().includes(searchValue.trim().toLowerCase()))
        setFilteredTodo(filteredItems);        
        if (filteredItems.length > 0) {
            setSearchValue('');
          }
      }

    const handleOnKeyPress = (e) =>{
        if(e.key==='Enter'){
            searchItems()            
        }
      }

    const searchValueChange = () => {
        if(searchValue.trim()!== ''){
            setSearchValue(searchValue.trim());
            searchItems()
        }     
      }
    
  return (
      <header css={header}>
        <button onClick={showList} css={homeBtn}><FaHome /></button>
        <div css={searchContainer}>
            <span>🍒장 봐올 리스트🍎</span>
            <div>
                <input type='text' value={searchValue} onChange={e=> 
                setSearchValue(e.target.value)} placeholder='아이템을 목록에서 검색해보세요' 
                css={searchBoxInput} onKeyDown={handleOnKeyPress}></input>
                <button css={searchBtn} onClick={searchValueChange}><FaSearch/></button>
            </div>
        </div>
        <button css={darkModeBtn}><FaMoon /></button>
      </header>
      )
}
export default Header;

 

아이템 추가용 input 박스가 기존 레이아웃에 존재하다 보니, 검색 input은 어디 배치해야 가장 UI/UX가 좋을지 고심하였다. 결국에는 상단의 header에 배치하기로 결정한 뒤 <Header/>에 함께 넣어주었다.(추후 구조 분리가 필요해 보인다!)

해당 코드에서 완성한 기능은 다음과 같다.

  1. searchItems를 사용하여 해당 함수가 실행될 때마다 input에 들어온 키워드와 기존 todo항목을 일일이 비교한 뒤, 일치되는 검색어들만 보이도록 하였다. 이때 toLowerCase를 사용하여 영문자로 입력된 경우 대소문자에 차이를 두지 않게 하였다. trim은 혹시 들어올 빈칸을 없애기 위해 사용하였다.
  2. handleOnKeyPress로 enter키를 누를 때마다 searchItems가 실행되어 아이템이 검색되도록 조치하였다.
  3. showList를 좌측 홈 버튼에 넣음으로서, 버튼 클릭할 때 전체 todo목록이 보이도록 하였다. 검색어를 입력하다가도 돌아가서 처음의 전체 목록 리스트를 보고 싶은 경우가 있을 것 같아 해당 기능을 추가했다.

본래 Recoil에서는 상태를 읽고 쓰기 위해 useState와 용도와 모양이 비슷한 useRecoilState를 제공한다. 하지만 읽거나 쓰는 기능의 필요-불필요성에 따라 아래와 같이 useSetRecoilState와 useRecoilValue로 기능이 나뉘기도 한다. 기존에 recoil 사용을 공부하면서 이론으로만 접한 부분을 해당 프로젝트에서 리코일을 사용하면서 직접 써보아서 좋았다.

useRecoilState를 쓰지 않자 불필요한 부분이 사라져 훨씬 깔끔해보인다.

 

const setFilteredTodo = useSetRecoilState(filteredListState);

const todo = useRecoilValue(listState);

 

마지막으로 원하는 리스트 목록을 보여줄 App.js 하단에 다음과 같은 조건을 주었다.

 

filteredTodo의 length에 따라 다른 조건이 주어진다. 우선 검색어가 존재할 경우 map 함수를 사용해서 filteredTodo의 리스트를 보여준다. 검색어가 하나도 기존 목록과 일치하지 않을 경우 '일치하는 항목이 없다'는 메세지를 보여준다. 마지막으로 filteredTodo의 length가 0일 경우(검색어가 존재하지 않을 경우) 무조건 처음 전체 리스트 todo를 보여준다.

 

하나의 section 내에 보여주고자 하는게 많아 당연히 고려할 조건도 많아졌고…

일단 이렇게 해 두었으나… 좀더 개선된 방법이 있을 것 같다.

//App.js
<section css={listSection} className='list-section'>
          <div css={itemListContainer}>
            {filteredTodo.length>0 ? (
              filteredTodo.map(item => (
                <TodoItem key={item.id} item={item}></TodoItem>
              )) 
              ):
                searchValue.trim() !== ''?(
                  <div css={emptyItem}>검색 결과와 일치하는 항목이 없습니다.</div>
                ):(
                  todo.map(item => ( <TodoItem key={item.id} item={item}/>)
            ))
            }
            
            </div>
</section>

 

 

  • 검색어와 일치하는 목록이 존재하지 않는 경우

  • 검색어와 일치하는 목록이 존재하는 상태('사'로 검색함)


 

2) Modal 상태 관리(Recoil)

이전 스터디 프로젝트에서는 각자 redux-toolkit를 공부하면서 모달창의 상태 관리를 구현해보았다. 그러나 이번 투두 리스트에서는 recoil 하나로 상태를 관리하기로 결심하였다. 사실 나는 redux toolkit을 사용해서 기존의 방법대로 한번 모달을 구현해본 뒤 recoil로 다시 만들었는데 압도적으로 리코일이 사용이 간단한 편이었다. redux toolkit가 아무리 redux보다 편리해도 action이나 store, reducers등 고려해야 할 요소가 많다.....

 

즉 가벼운 구조의 프로젝트는, 상태를 리코일로 관리하는 것도 좋아 보인다.

//modalState.js
import { atom } from "recoil";

const modalState = atom({
    key: 'modalState',    
    default: false,
  });
  
  export { modalState };

 

recoil의 아톰으로 일단 modalState를 하나 만들어주고 기본상태를 false로 설정한다. 이후 App.js로 이동하여,

 

const [modalDataState, setModalDataState] = useRecoilState(modalState);

 

로 불러온 다음 console.log(modalDataState); 콘솔을 하나 찍어 보았다. 이때 콘솔창에는 기본으로 설정한 false가 나와야 정상이다. 이후 하단과 같이 실행될 때마다 modalDataState가 false와 true를 오가도록 간단한 함수를 하나 만들었다.

const setModalStateDefault = () =>{
    setModalDataState(!modalDataState)
  }

 

이제 해당 함수가 실행될 때마다, modalDataState는 아래처럼 바뀔 것이다. 미리 콘솔로 잘 작동하는지 확인한다.

{modalDataState&&(
        <section css={css({height:"50%", width:"100%", position:"absolute", top:"50%"})}>
        <div css={overlay} onClick={setModalStateDefault}></div>
        <Modal setModalStateDefault={setModalStateDefault}/>
      </section>)}

 

마지막으로 Modal과 모달의 overlay(모달 뒤에 깔리는 흐린 배경)이 modalDataState의 false, true 유무에 따라 보이거나 사라지게 만들면 끝이 난다.

또 Modal.js에서도 해당 함수를 쓸 수 있도록 props를 하나 내려주었다.

//Modal.js
<section css={css({display:"flex",justifyContent:"center", alignItems:"center"})}>
        <div css={modalContainer}>
            <button css={modalCloseBtn} onClick={setModalStateDefault}>x</button>
            <p>정말 전체를 삭제하시겠습니까?</p>
            <div css={modalBtnsContainer}>
                <button onClick={deleteItemList} css={[modalBtn, yesBtnStyle]}>예</button>
                <button css={[modalBtn, noBtnStyle]} onClick={setModalStateDefault}>아니오</button>
            </div>
        </div>        
    </section>

 

이제 overlay를 클릭하거나, 모달의 ‘아니오’, x(나가기)버튼을 클릭할 때마다 함수가 실행되어 모달을 닫는다.

 

해당 모달은 '리스트 전체 삭제' 버튼을 클릭했을 때 true가 되며 모달이 뜨도록 하였다.

따라서 모달창에서 예를 클릭하면 전체 리스트를 담은 배열이 초기화되는 deleteItemList 함수가 먼저 실행된 뒤, setModalStateDefault()가 한번 더 실행되어 modal이 닫힐 것이다.

const deleteItemList = () => {
    setTodo([])
    setModalStateDefault();
  }