Search Bar 구현하기
대부분 검색엔진은 외부 라이브러리 등을 사용하는 것으로 알고 있다. 하지만 이번 프로젝트에서의 검색 기능은 다른 자료를 참고하지 않고 직접 구현해보고 싶었다. 검색 기능은 Category 영역의 정보를 받고 Answer 영역에도 영향을 주어야했기 때문에 Search Bar에서는 검색어를 state로 받고 초기화 기능만 구현하였다.
const SearchBar = ({ search, setSearch }) => {
const handleChange = (e) => {
setSearch(e.target.value);
};
const handleClear = (e) => {
e.preventDefault();
console.log('clear');
setSearch('');
};
return (
<StyledWrapper search={search}>
<form>
<input placeholder='검색' onChange={handleChange} value={search} />
<button
className='clearButton'
type={'button'}
onClick={handleClear}
disabled={!search}>
초기화
</button>
</form>
</StyledWrapper>
...
Search state
search state는 상위 컴포넌트인 FaqBody에 구현하여 여러 컴포넌트들에서 사용할 수 있게 하였다.
const FaqBody = ({ currentUser }) => {
const [cate, setCate] = useState('정치성향별 다섯 부족');
const [search, setSearch] = useState('');
return (
<StyledWrapper>
<div className='container'>
<h2>oxopolitics FAQ</h2>
<WritingAdmin User={currentUser} />
<CategoriesContainer cate={cate} setCate={setCate} />
<SearchBar search={search} setSearch={setSearch}/>
<AnswersContainer cate={cate} setCate={setCate} search={search} />
</div>
</StyledWrapper>
);
};
검색 기능 구현
이번 프로젝트에서 검색 기능은 선택한 Category의 Answer 목록에서 검색어와 일치하는 항목들을 filtering해서 보여주는 것을 목표로 하였다. 이 기능은 Array 빌트인 함수인 filter와 includes를 이용하여 구현하였다. 제목 또는 내용에 search와 일치하는 것이 있는 경우 필터링해 보여주었다.
<StyledWrapper>
{!!answers &&
answers
.filter(
(ans) =>
ans.title.includes(search) || ans.description.includes(search)
)
.map((elem) => (
<Answer
ansArr={elem}
key={elem.id}
answerId={elem.id}
cate={cate}
search={search}
/>
))}
</StyledWrapper>
검색어 High-lighting 기능 구현
이번 프로젝트에서 가장 애를 먹었던 부분이다. 하이라이팅 역시 오픈소스 라이브러리가 있겠지만 다른 사람의 것을 참고하지 않고 직접 구현한 다음 비교해보고 싶었다. 내가 구현한 하이라이팅 알고리즘은 다음과 같다.
- 텍스트를 search 값을 기준으로 나눈다(split 메소드 이용). split을 이용하면 배열의 항목 사이마다 search 값이 들어가야한다.
- 1에서 만든 array를 map을 이용하여 span으로 만든다.
- 배열 항목 사이마다 search 값을 <b> 태그로 감싸서 넣어준다.
위 과정은 다음과 같이 메소드를 만들어 구현하였다.
//Answer.jsx
...
return (
<StyledWrapper className={`answerContainer${active ? ' active' : ''}`}>
<span onClick={answerCardClick}>
<h4>{search ? highlightText(title, search) : title}</h4>
{validAdmin(currentUser) ? (
<div className='adminButtons'>
<button onClick={handleUpdate} type='button'>
{isEdit ? '취소' : '수정'}
</button>
<button onClick={handleDelete} type='button'>
삭제
</button>
</div>
) : null}
<img src='img/arrow.svg' alt={active ? '닫힌 질문' : '열린 질문'} />
</span>
{isEdit && <>{getEditForm()}</>}
{active && !isEdit ? <>{makeText(description, search)}</> : null}
</StyledWrapper>
);
...
const highlightText = (text, search) => {
const split = text.split(search);
return (
<>
{split.map((s, idx) => {
return (
<span key={idx}>
{idx != 0 && <b>{search}</b>}
{s}
</span>
);
})}
</>
);
};
const makeText = (answer, search) => {
let answerSplit = answer.split('\\n');
let text = [];
for (let e of answerSplit) {
if (e) {
text.push(e);
}
}
return (
<p>
{text.map((el, idx) => {
if (search && el.includes(search)) {
return <span key={idx}>{highlightText(el, search)}</span>;
}
return <span key={idx}>{el}</span>;
})}
</p>
);
};
데이터베이스에서 받은 string에 줄바꿈이 적용되지 않았기 때문에 줄마다 별도의 span으로 만들어주기 위해 makeText라는 함수를 추가로 만들어주었다.
프로젝트 회고
실제 프로젝트 중에는 블로그에 적은 것보다 훨씬 많은 고민들을 했었는데 나중에 쓰려니 기억이 잘 나지 않는다😂 앞으로는 꼭 중간중간 메모해놓는 습관을 가져야겠다... 그래도 현업자 분들께서 내가 고민했던 부분들을 알아주신건 긍정적으로 볼 수 있겠다ㅎㅎ 코딩 공부를 시작하고 무에서 유를 창조한 것은 처음인데 배포까지 무사히 마칠 수 있어서 즐거운 경험이었다. 일주일이라는 짧은 시간 안에 마무리해야해서 구현하지 못한 아쉬운 부분도 있지만 이것들은 다음 프로젝트에서 반영하기로 하자.
아쉬운 부분들
- 검색 기능을 구현할 때 includes를 이용하여서 한글의 특성상 타이핑 도중 자음과 모음이 분리될 때(ex. 분ㄹ -> 분리) 필터링이 되지않는 현상이 발생했다. 다른 검색엔진에서 해결방법을 찾아봐야할 것 같다.
- env 파일로는 보안이 불가능했다. 이를 보완할 방법을 찾아봐야한다.
- 하이라이팅을 구현할 때 b 태그를 이용하다보니 띄어쓰기가 제대로 동작이 되지않아 margin 값을 강제로 주어 해결하였다.
- 함수명, 변수명 재사용 가능하도록 리팩토링을 하지 못했다
- answer를 한번에 불러오기 보다는 무한스크롤을 구현하여 조금씩만 불러오기