기존 문제점
URLSearchParams
: URL의 쿼리 부분을 수비게 파싱하고, 조작하고, 반복할 수 있는 방법 제공
- append, delete, entries, forEach, get, getAll, has, keys, set, sort, toString, values
❓ 갑자기 궁금해지는 searchParams와 searchParam의 차이
- searchParams : URL의 쿼리 문자열을 다루기 위한 인터페이스를 나타내는데 사용
- searchParam : 보다 일반적이고 추상적인 용어로, URL의 쿼리 부분에 포함된 개별적인 파라미터나 키-값 쌍을 가리킬 때 사용할 수도 있다
URLSearchParams 객체 사용해보기
-
searchParams 선언
const searchParams = new URLSearchParams('http://localhost:3000/home?a=1&b=2&c=3'); console.log(searchParams.toString());
-
append
cconst searchParams = new URLSearchParams('http://localhost:3000/home?a=1&b=2&c=3'); searchParams.append('c','4'); searchParams.append('c','5') console.log(searchParams.toString());
-
delete
cconst searchParams = new URLSearchParams('http://localhost:3000/home?a=1&b=2&c=3'); searchParams.append('c','4'); searchParams.append('c','5'); searchParams.delete('c'); console.log(searchParams.toString());
URLSearchParams의 한계
delete메서드를 사용하면, 해당 key에 대한 모든 searchParams를 제거한다.
즉, 원하는 key & value쌍을 제거하지 못한다.
해결 방안
URLSearchParams객체를 우선 다루기 쉬운 형태로 바꾸어야 한다. 기존 searchParams는 key-value형태이므로 객체 리터럴 혹은 Map이 적절하다. 둘 중에 순회를 용이하게 하기 위해서는 Map이 객체 리터럴보다 더 적절하다고 판단하였다. 또한 has, get, delete를 이용하여 보다 용이하고 명시적인 데이터 조작이 가능하다는 장점이 있다.
Map으로 변환을 하고 해당 객체를 계속 사용할 것이기에 class로 추상화하는 것이 적절하다고 판단하였다. 함수로 추상화를 할 경우, Map으로 변환한 객체를 함수를 사용하는 측에서 하거나, 혹은 함수를 사용할 때마다 Map으로 변환하여 해당 객체 변수를 가지고 있어야 한다.
이러한 Map으로 변환한 객체를 class의 프로퍼티로 가지고 있으면 적절하다고 판단하였다.
1. searchParamsMap 프로퍼티
export class SearchParamsManager {
searchParamsMap: SearchParamsType
constructor(searchParams: ReadonlyURLSearchParams) {
this.searchParamsMap = new Map<SearchParamKeyType, string[]>()
searchParams.forEach((value, key) => {
if (!this.searchParamsMap.has(key as SearchParamKeyType)) {
this.searchParamsMap.set(key as SearchParamKeyType, [value])
} else {
this.searchParamsMap.get(key as SearchParamKeyType)?.push(value)
}
})
}
}
URLSearchParams객체인 searchParams를 전달받아 Map형태로 변환한다. 변환한 searchParamsMap은 추후 searchParams를 조작할 때 사용할 객체이다.
이 때, key값으로 사용할 타입은 정해져 있기에 미리 지정해두었다.
type SearchParamKeyType =
| 'courseType'
| 'format'
| 'category'
| 'level'
| 'programmingLanguage'
| 'price'
| 'keyword'
| 'page'
type SearchParamsType = Map<SearchParamKeyType, string[]>
2. searchParamsMap를 조작하는 다양한 메서드
-
Create
export class SearchParamsManager { // .. addSearchParam(key: SearchParamKeyType, value: string) { if (!this.searchParamsMap.has(key)) this.searchParamsMap.set(key, [value]) else this.searchParamsMap.get(key)?.push(value) } }addSearchParam: key와 value를 전달받아, Map에 추가한다.
-
Read
export class SearchParamsManager { // .. hasSearchParam(key: SearchParamKeyType, value: string) { return !!( this.searchParamsMap.has(key) && this.searchParamsMap.get(key)?.includes(value) ) ) } getSearchParamsValue(key: SearchParamKeyType) { return this.searchParamsMap.get(key)?.[0] } getAllSearchParamsValue(key: SearchParamKeyType) { return this.searchParamsMap.get(key) || [] } }hasSearchParam: key값과 value를 전달받아 존재하는 지 여부를 return한다
getSearchParamsValue: key값을 전달받아 해당하는 value를 return한다.getAllSearchParamsValue: key값을 전달받아 해당하는 value를 return한다.- 이 때, 모든 요소를 찾아 배열로 반환한다.
-
Update
export class SearchParamsManager { // .. setSearchParam(key: SearchParamKeyType, value: string) { this.searchParamsMap.set(key, [value]) } }setSearchParam: key와 value를 전달받아 해당 key에 해당하는 value를 덮어쓴다.
-
Delete
export class SearchParamsManager { // .. removeSearchParam(key: SearchParamKeyType, value: string) { const values = this.searchParamsMap.get(key) if (values) { const index = values.indexOf(value) if (index > -1) { values.splice(index, 1) if (values.length === 0) { this.searchParamsMap.delete(key) } } } } removeAllSearchParams(key: SearchParamKeyType) { this.searchParamsMap.delete(key) } }removeSearchParam: key와 value를 전달받아 key-value에 해당하는 값을 제거한다.
removeAllSearchParams: key를 전달받아 key에 해당하는 모든 값을 제거한다.
-
toString
export class SearchParamsManager { // .. toString() { const searchParams = new URLSearchParams() this.searchParamsMap.forEach((values, key) => { values.forEach((value) => searchParams.append(key, value)) }) return searchParams.toString() } }-
toString: Map자료형의 searchParamsMap를 다시 문자열로 바꾸는 메서드이다. searchParams를 원하는대로 수정하였다면, 문자열로 다시 바꾸어 라우팅할 때 사용한다.const router = useRouter(); router.replace(`?${searchParamsManager.toString()}`)
-
추가 개선
1. SearchParamsManager를 사용할 때 생기는 코드
위 SearchParamsManager를 활용하여 searchParams를 조금 더 자유롭게 조작할 수 있게 되었다. 아직 불편한 점이 있다. SearchParamsManager를 조작하기 위한 예시 코드이다.
const searchParams = useSearchParams();
const router = useRouter();
const searchParamsManager = new SearchParamsManager(searchParams);
searchParamsManager.addSearchParam('a','b');
searchParamsManager.remove('searchParamsManagerpage')
router.replace(`?${searchParamsManager.toString()}`)
모두 위와 같은 패턴을 보였다. SearchParamsManager는 클래스이기에 내부에서 훅을 사용하지 못한다. 그래서, 이러한 SearchParamsManager패키징한 커스텀 훅을 만들기로 결정하였다.
-
필요한 훅 호출 및 객체 생성
export default function useSearchParamsRouter() { const router = useRouter() const searchParams = useSearchParams() const searchParamsManager = new SearchParamsManager(searchParams) // .. }router는 라우팅할 때 필요하며,searchParams는 URLSearchParams 객체를 생성하기 위해 필요하다. 이 searchParmas를 더 조작하기 쉽게 추상화한 searchParamsManager를 생성하여, 앞으로는 이 객체를 활용할 것이다. -
updateURLexport default function useSearchParamsRouter() { // .. const updateURL = (newSearchParams: string) => { if (searchParamsManager.toString() === `?${newSearchParams}`) return router.replace(`?${newSearchParams}`, { scroll: false }) } }변경된 searchParams로 라우팅하는 함수이다. 이 때, 변경 이전과 searchParams가 동일하다면, 라우팅하지 않는다.
-
manipulateSearchParamsexport default function useSearchParamsRouter() { // .. const manipulateSearchParams = ( action: (manager: SearchParamsManager) => void, ) => { action(searchParamsManager) updateURL(searchParamsManager.toString()) } }searchParams값을 변경하는 콜백함수를 전달받는다. 이 때, 인자에는 searchParamsManager가 들어간다. 그리고 변경된 searchParams값으로 URL을 업데이트한다.
-
url을 직접 조작하는 함수들 : C,U,D
export default function useSearchParamsRouter() { // .. const addSearchParam = (key: SearchParamKeyType, value: string) => { manipulateSearchParams((manager) => manager.add(key, value)) } const setSearchParam = (key: SearchParamKeyType, value: string) => { manipulateSearchParams((manager) => manager.set(key, value)) } const removeSearchParam = (key: SearchParamKeyType, value: string) => { manipulateSearchParams((manager) => manager.remove(key, value)) } const removeAllSearchParams = (key: SearchParamKeyType) => { manipulateSearchParams((manager) => manager.removeAll(key)) } } -
url을 직접 조작하지 않고 조회만 하는 함수들 : R
export default function useSearchParamsRouter() { // .. const getSearchParamsValue = (key: SearchParamKeyType) => searchParamsManager.get(key) const getAllSearchParamsValue = (key: SearchParamKeyType) => searchParamsManager.getAll(key) }이 함수를 넣을까 말까 고민을 많이 했다. 하지만, 조회만을 위해 SerachParamManager객체를 생성하고, 이에 따라 행동을 취하는 것은 오히려 더 불필요한 코드를 만들 것이라 판단하여, 조회만 하는 함수도 추가하였다.
2. 중복되는 searchParams접미사의 제거
기존 SearchParamsManager의 메서드에는 모두 끝에 searchParams 혹은 searchParam이 붙어있었다. SearchParamsManager로 생성한 searchParamsManager 인스턴스에도 searchParams에 관한 것이라는 명시가 있으니, 불필요하다고 판단하였다. 따라서 변경한 메소드명은 다음과 같다.
이렇게 단축한 네이밍만으로도 충분히 의미를 유추할 수 있을 것이라 판단하였다.
사용 방법
1. SearchParamsManager (class)
const searchParamsManager = new SearchParamsManager(searchParams)
searchParams를 조작할 때는 대부분이 searchParams를 기반으로 라우팅을 하는 경우이기에 SearchParamsManager를 직접적으로 사용하는 경우는 거의 없다.
SearchParamsManager를 기반으로 라우팅 기능을 추가하여 패키징한 아래의 useSearchparamsRouter를 주로 사용한다고 보면 된다.
2. useSearchparamsRouter (hook)
const searchParamRouter = useSearchParamsRouter()
// ..
searchParamRouter.set('keyword', debouncedKeyword)