책임을 하나씩 관리하기

지역 상태로 관리하던 필터를 search param으로 옮기고, hook의 책임 나누기

Table Of Contents

검색 필터링을 구현하자

지금 진행하고 있는 프로젝트에서는 암호화폐 관련 정보를 모아 볼 수 있다.

검색 편의성을 위해 텍스트 검색 외에도 기간, 암호화폐, 거래소, 중요도 등을 필터링하는 기능도 제공한다!

1. search filters

(시작 날짜, 중요도 필터를 적용한 모습)

 

하지만 기존 필터는 지역 상태로 관리되고 있었다.

// SearchArea.tsx export default function SearchArea() { const [option, setOption] = useState(defaultOption); function onChangeOption() { /* ... */ } return ( <> {/* 옵션 렌더링 & 변경을 위해 option, onChangeOption을 넘깁니다 */} <SearchOption optioin={option} onChangeOption={onChangeOption} /> {/* 옵션애 따라 데이터를 받아오기 위해 option을 넘깁니다 */} <SearchResult option={option} /> </> ); }
// SearchOption.tsx export default function SearchOption({ option, onChangeOption }) { return ( <> <DateSelector value={option.from} onChange={(newValue) => onChangeOption("from", newValue)} /> <DateSelector value={option.to} onChange={(newValue) => onChangeOption("to", newValue)} /> <ImportanceSelector value={option.importance} onChange={(newValue) => onChangeOption("importance", newValue)} /> {/* ... */} </> ); }

 

부모 컴포넌트에서 option라는 state를 만들고, 이를 자식 컴포넌트로 내려주게 된다.

 

이렇게 필터를 지역 상태로 구현하면, 이런 문제가 생긴다!

Search Params를 이용하자

우선 필터를 지역 상태에서 꺼내서 URL의 Search Params에 저장하기로 했다.

Search params를 활용하면 위에서 언급한 문제점들을 모두 해결할 수 있다.

Custom Hook으로 params 관리하기

그러던 중, Toss Frontend Fundamentals에서 책임을 하나씩 관리하기라는 글을 읽게 되었다.

딱 내가 겪고 있는 상황아잖아...?!🤔

지역 상태에서 URL로 옮기는 것 까지는 좋은데, 이 로직을 전부 usePageState같은 하나의 hook에 담게 되면 한 hook에서 다루는 맥락이 너무 커진다.

💡 이 문제를 해결하기 위해서 쿼리 파라미터별로 Hook을 분리해볼 수 있다.

Hook의 책임 분리하기

날짜 관리하기: useDateParam

검색 필터에서 숫자, 문자열과 같은 간단한 데이터는 useQueryParam같은 Hook을 하나 만든 다음에 돌려 쓸 수 있다.

하지만 날짜(Date)는 직접 URL에 저장할 수 없고, 값을 파싱/포맷할 때 유효성 검증이 필요하기 때문에 별도의 Hook을 마련해야 한다.

function formatDate(value: Date): string { ... } function parseDate(value: string): Date | null { ... } export default function useDateParam(key: string) { const searchParams = useSearchParams(); const router = useRouter(); const value = parseDate(searchParams.get(key) || ""); function setValue(newValue: Date) { if (typeof window === "undefined") return; const newParams = new URLSearchParams(window.location.search); newParams.set(key, formatDate(newValue)); router.replace(`?${newParams.toString()}`, { scroll: false }); } return [value, setValue] as const; }

 

이제 useDateParam을 사용하여 DateSelector에 적용할 수 있다.

export default function SearchOption() { const [fromDate, setFromDate] = useDateParam("from"); const [toDate, setToDate] = useDateParam("to"); return ( <> <DateSelector value={fromDate} onChange={setFromDate} /> <DateSelector value={toDate} onChange={setToDate} /> </> ); }

seDateParam("from");처럼 사용하면 URL의 search params 중에서 "from" 을 key로 가지는 값을 관리할 수 있다.

이렇게 SearchOption이 props를 받을 필요 없이 쿼리 문자열에서 직접 값을 가져오고, 변경할 수 있다!

한 key에 여러 value 관리하기: useMultipleParam

일부 검색 필터는 하나의 값만 가지는 것이 아니라, 여러 개의 값을 가질 수도 있다.

예를 들어, 중요도(importance) 필터에서 low, mid 이벤트만 검색하고 싶다면 아래처럼 URL을 구성할 수 있다.

?importance=LOW&importance=MID

 

하지만 기존의 useDateParam 같은 방식으로 값을 저장하면, 값이 하나만 저장되는 문제가 발생할 수 있다.

이를 방지하기 위해, 하나의 key에 여러 개의 값을 저장할 수 있도록 getAll()append()를 사용한 Hook을 만든다.

export default function useMultipleParam(key: string = "option") { const searchParams = useSearchParams(); const router = useRouter(); // 현재 URL에서 해당 key의 모든 값을 가져온다. const getCurrentQuery = () => searchParams.getAll(key); const value = getCurrentQuery(); function setValue(newValue: OptionValue[]) { if (typeof window === "undefined") return; const newParams = new URLSearchParams(window.location.search); newParams.delete(key); newValue.forEach((v) => newParams.append(key, v as string)); router.replace(`?${newParams.toString()}`, { scroll: false }); } return [value, setValue] as const; }

 

이제 useMultipleParam을 활용해 MultiSelector가 검색 필터를 설정할 수 있도록 변경할 수 있다.

아래 코드에서는 useMultipleParam("importance");처럼 사용해서 URL의 search params 중에서 "importance"를 key로 가지는 값을 관리한다.

export default function SearchOption() { const [importance, setImportance] = useMultipleParam("importance"); return ( <> <ImportanceSelector value={importance} onChange={setImportance} /> </> ); }

아쉬운 점

window.location.search?

지금은 Hook 안에서 URL의 search param을 받아올 때, window.location.search를 이용하고 있다.

원래 Next.js에서 제공해주는 useSearchParams hook을 사용했지만, 문제가 발생했다...

화면을 열고 from 값을 변경한 뒤, to 값을 변경하면 from값이 초기값으로 되돌아가버렸다. 다시 from 값을 변경하려 하면 to 값이 다시 초기값으로 돌아온다...!😂

 

처음에는 setValue 함수를 useCallback으로 감싸고, searchParams의 값을 의존성 배열에 넣어서 setValue가 항상 최신 searchParams 값을 참조하도록 해봤다. 하지만 실패했다...

searchParams라는 객체 레퍼런스가 동일해서 문제가 생기지 않는 것일까 싶어 의존성 배열에 searchParams.toString()를 대신 넣어줬는데, 이 방법도 문제를 해결해주지는 못했다...🥹

임시방편으로 window.location.search를 활용해 작동은 하게 만들었지만, window.location은 Next.js의 route 객체와 완전히 동기화되지 않는다는 점에서 사용을 권장하지 않는다고 한다.

 

Next.js docs에 따르면 useSearchParams는 URL이 변경되면 자동으로 업데이트된다고 하는데, 왜 여기서는 자동으로 업데이트 되지 않는지...?

Next.js부터 React까지 문서를 계속 읽고있는데 아무도... 이에 대한 언급을 해 놓지 않는다진짜서럽다 원인을 알게 되면 좋겠다...

결론

최종적으로 폴더 구조는 아래처럼 구성했다.

SearchPage ├── _components │ ├── SearchOption │ │ ├── DateSelector │ │ ├── ImportanceSelector │ │ └── ... │ └── SearchResult └── _utils ├── useDateParam └── useMultiParam

SearchParam을 관리하는 Hook들을 따로 _utils 폴더를 만들어서 모아놨다.

참고