우리의 팀 프로젝트는 RTK를 사용하여 상태관리중이므로, 이에 맞춰서 slice와 리듀서 함수를 생성해주었다.
import { createSlice } from '@reduxjs/toolkit'
const searchQuerySlice = createSlice({
name: 'searchQuery',
initialState: {
query: '',
},
reducers: {
setQuery: (state, action) => {
state.query = action.payload
},
},
})
export const { setQuery } = searchQuerySlice.actions
export default searchQuerySlice.reducer
검색 query를 받아와 세팅하는 action 함수를 작성하였다. 후에 store에 연결을 해주었다.
이미 프로젝트 내에서 RTK를 사용중이었기 때문에 Provider 또 다시 세팅할 필요는 없었다.
textSearch()이를 따로 보내줄 필요 없이, query의 상태가 변한다면 이를 감지하여 검색되는 api를 보내 검색된 결과를 받아오게끔 api 함수를 작성하였다.
// 검색된 포스트 가져오기
export const getSearchedPost = async (query) => {
try {
const { data, error } = await supabase
.from('posts')
.select()
.textSearch('title', {query})
if (error) throw error
return data
} catch (error) {
console.error('검색된 포스트 가져오기 실패:', error.message)
throw error
}
}
여기서 데이터에 해당 query가 있는지 없는지 확인하고 값을 주는 함수는 textSearch()이다. (column, query)를 매개변수로 받으며 찾고 싶은 데이터 column을 대입 후에 유저에게 받아온 query를 대입하면 된다. 그리고 이를 홈페이지에서 검색시 콜되는 custom hook에 추가해주었다.
const useFetchPosts = (isHome) => {
// ... 다른 상태들
const query = useSelector((state) => state.searchQuery.query)
useEffect(() => {
const fetchPosts = async () => {
try {
let data
if (isHome) {
// 검색어가 있는 경우와 없는 경우로 나누는 로직 추가
if (query.length > 0) {
data = await getSearchedPost(query);
} else {
data = await getAllPosts()
}
} else if (...) {
// ...
}
} catch () {
} finally {
}
}
fetchPosts()
}, [... , query])
return ...
}
이미 존재하던 custom hook에서 어느 시점에서 검색된 결과를 받아와야하는지 지정해주고 useEffect의 의존성 배열에 query를 추가해서 완성 시켰다.
기존에 사용하던 useState의 상태 대신 useSelector를 사용하여 RTK를 이용하여 저장한 state에서 query를 받아온다. query를 input의 value로 지정해줄 필요가 있었고 유저 인풋이 달라질때마다 dispatch를 통해 저장된 상태를 변경해주었다.
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setQuery } from '../../redux/slices/searchQuerySlice'
// 나머지 import
const SearchInput = () => {
// query 상태를 selector를 통해 받아온다
const query = useSelector((state) => state.searchQuery.query)
// query 변경시 필요
const dispatch = useDispatch()
const handleInputChange = (e) => {
dispatch(setQuery(e.target.value))
}
return (
<>
<SearchInputField
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search..."
/>
</>
)
}
export default SearchInput
다만 문제가 이렇게 되면 유저가 입력하는 문자마다 데이터 베이스로 API call이 보내지게 되므로 debounce 기능을 추가해주는 것이 효율적으로 보였다.
우선 아래와 같이 debounce 함수를 정의하였다. 예전에 영화 검색 사이트에서 사용하던 로직을 활용하였다.
const debounce = (func, delay) => {
let timer
return function () {
const args = arguments
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
debounce 시킬 함수와 기다리는 시간을 delay로 받는 함수이다.
그리고선 이전의 SearchInput 컴포넌트 내에서 로직을 debounce를 적용한 로직으로 수정하였다.
...
const query = useSelector((state) => state.searchQuery.query)
const [searchInput, setSearchInput] = useState('');
const dispatch = useDispatch()
const debouncedSearch = React.useCallback(
debounce((value) => {
console.log('Debounced Query:', value)
dispatch(setQuery(value))
}, 500),
[]
)
const handleInputChange = (e) => {
dispatch(setQuery(e.target.value))
// input의 value도 바꿔주고
setSearchInput(e.target.value)
// debounce도 실행
debouncedSearch(e.target.value)
}
...
return (
<>
<SearchInputField
type="text"
value={searchInput}
onChange={handleInputChange}
placeholder="Search..."
/>
</>
)
다만 이전처럼 query를 input의 value로 둘 수가 없어서 새로운 state를 사용하여 searchInput의 value로 대체하였다.
query를 input의 value로 지정할 수 없는 이유는 debounce 함수가 일정 시간을 기다린 후에야 query를 받아오는데 이렇게 되면 인풋 창에 아무 타자를 열심히 쳐도, 보이지 않는 이슈가 있어서 바꾸었다.
textSearch()의 단점은 "콜라 문제"라는 데이터가 존재한다면 "콜라", "문제", "콜라 문제" 단어 형태로만 검색 결과를 주었다. 그래서 "콜"만 검색하면 원하는 결과가 나오지 않았다.


그래서 추가로 구글링하던 중 ilike 함수를 사용하면 된다고 하여 사용하였는데
try {
const { data, error } = await supabase
.from('posts')
.select()
.ilike('title', query)
if (error) throw error
return data
}
이번에는 단어 검색도 "콜"만 검색하는 것도 되지 않았다. ㅇㅅㅇ .... (당황스럽군...)

찾아본 결과 .ilike('title', `%${query}%`)이 맞는 문법이라 하여 바꾸어 주었다.

이제는 "콜"만 쳐도 검색 결과가 나온다. 문제를 금방해결해서 다행이다ㅠㅠ
Supabase의 데이터 함수가 아직은 익숙치 않지만 조금씩 경험치를 쌓고 있다는 것을 프로젝트를 통해서 배우고 있다.