[마켓만들기] 키워드 검색 기능(api query, 검색어 하이라이팅)

seo young park·2022년 3월 30일
7
post-thumbnail

✨ 검색 기능

서버와 통신하여 키워드 검색 기능을 구현할 것이다. 사용자가 입력한 검색어와 일치하는 유저 데이터를 보여줄 것이며, 이 때 일치하는 텍스트는 하이라이트 처리할 것이다.

✨ api와 통신하여 유저 검색 기능 구현

검색 기능은 사용자가 입력한 키워드를 서버에 전송하고, 서버에서는 전달받은 키워드와 일치하는 데이터를 db에서 꺼내 반환하는 방식으로 진행된다. 따라서 프론트에서는 useState를 사용해 유저가 입력하는 키워드를 동적으로 전달해주면 된다. 먼저, useState와 setState, 그리고 onChange 이벤트를 활용해 키워드를 동적으로 업데이트하는 코드를 구현했다. 입력값이 없을 때, 유저 리스트를 전부 불러오는 상황이 발생해서 '%^&'라는 특수문자를 넣어놓았다.

 const [keyword, setKeyword] = useState('%^&');
 
 const handleSearch = (event) => {
    if (event.target.value === '') {
      setKeyword('%^&');
    } else {
      setKeyword(event.target.value);
    }
 };

  return (
    <Searchuserheads>
      <button onClick={back}>
        <img src="/assets/icon/icon-arrow-left.png" alt="뒤로가기" />
      </button>
      <input
        type="text"
        className="cont-home-search-input"
        placeholder="계정을 검색할 수 있습니다."
        onChange={handleSearch}
      />
    </Searchuserheads>
  );
  

그리고 api 명세에 따라 키워드를 적절한 위치에 넣어주었다. 콘솔로 확인해보니 캡쳐화면과 같이 쿼리가 찍히는 것을 확인할 수 있었다.

 const userToken = localStorage.getItem('Token');
    fetch(`${API_ENDPOINT}/user/searchuser/?keyword=${keyword}`, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${userToken}`,
        'Content-type': 'application/json',
      },
})

💄 검색어 하이라이트

이제 검색어와 일치하는 텍스트를 하이라이트 처리하는 기능을 구현할 것이다.

정규표현식 플래그를 활용한 고급탐색

정규표현식에서는 다음와 같이 플래그를 통해 전역을 탐색하거나 대소문자를 무시하는 특성 등을 지정할 수 있다. 플래그는 단독으로 사용할 수도, 순서와 상관없이 한꺼번에 지정할 수도 있다.

  • g RegExp.prototype.global 전역 탐색하는 옵션
  • i RegExp.prototype.ignoreCase 대소문자를 무시하는 옵션

g,i 플래그를 활용하여 일치하는 텍스트는 모두 찾고, 대소문자 상관없이 하이라이트 스타일을 적용할 것이다. 이제 다음의 코드를 하나씩 뜯어보자.

  const matchedText = (text, keyword) => {
    //키워드가 빈 값이 아니거나, text가 키워드를 포함하고 있다면(조건)
    if (keyword !== '' && text.includes(keyword)) {
      //키워드를 기준으로 text를 쪼갠다.
      const parts = text.split(new RegExp(`(${keyword})`, 'gi'));

      return (
        <>
        // 문자열이 담긴 배열을 map 돌림
          {parts.map((part, index) =>
      		//소문자로 변환 후 비교하여 일치하면 MarkedText 스타일 적용
            part.toLowerCase() === keyword.toLowerCase() ? (
              <Markedtext key={index}>{part}</Markedtext>
            ) : (
              //일치하지않으면 그대로 출력
              part
            )
          )}
        </>
      );
    }

    return text;
  };

String.prototype.split(separator)

split() 메서드는 string 객체를 지정한 키워드를 기준으로 여러 개의 문자열로 나눈다.
만약 "철수와 영희가 코딩을 하고 있다."라는 문장이 있다고 가정했을 때

//공백을 넣을 경우, 공백을 기준으로 문자열을 나눈다.
const result = "철수와 영희가 코딩을 하고 있다.".split(' ');
console.log(result); // [ '철수와', '영희가', '코딩을', '하고', '있다.' ]

//''을 넣을 경우, 글자마다 문자열을 나눈다.
const result = "철수와 영희가 코딩을 하고 있다.".split('');
console.log(result); // [ '철', '수', '와', ' ', '영', '희', '가', ' ', '코', '딩', '을', ' ', '하', '고', ' ', '있', '다', '.' ]

여기서 gi 플래그를 적용한 정규표현식과 키워드를 넣어서 사용할 경우, 다음과 같이 키워드를 기준으로 문자열을 나누게 된다.


const text = "철수와 영희가 코딩을 하고 있다.";
const keyword = "영희";

const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
console.log(keyword); // [ '철수와 ', '영희', '가 코딩을 하고 있다.' ]

String.prototype.toLowerCase()

toLowerCase() 메서드는 문자열을 소문자로 변환해 반환한다.

const word = 'aPplE';

console.log(word.toLowerCase()); // 'apple'

코드에 적용하면 map을 돌릴 때, 키워드와 키워드를 기준으로 쪼갠 문자열을 모두 소문자로 변환하여 비교한다. 일치하면 MarkedText 스타일을 적용하고, 일치하지 않으면 그대로 출력한다.

return (
{parts.map((part, index) =>
   //소문자로 변환 후 비교하여 일치하면 MarkedText 스타일 적용
   part.toLowerCase() === keyword.toLowerCase() ? (
       <Markedtext key={index}>{part}</Markedtext>
          ) : (
   //일치하지않으면 그대로 출력
  part
 )}
)

최종 코드는 다음과 같다.

export const UserList = ({ keyword}) => {
  const matchedText = (text, query) => {
    if (query !== '' && text.includes(query)) {
      const parts = text.split(new RegExp(`(${query})`, 'gi'));

      return (
        <>
          {parts.map((part, index) =>
            part.toLowerCase() === query.toLowerCase() ? (
              <Markedtext key={index}>{part}</Markedtext>
            ) : (
              part
            )
          )}
        </>
      );
    }

    return text;
  };

  return (
    <Container>
      {profile.map((data, index) => (
        <li key={`follow-${index}`}>
          <div>
            <Link
              to={`/profile/${data.accountname}`}
            >
              <img
                src={data.image}
                alt="사용자 이미지"
                onError={(e) => {
                  e.target.onerror = null;
                  e.currentTarget.src = '/assets/basic-profile-img.png';
                }}
              />
              <div>
				//유저 닉네임이 일치하면 하이라이트 처리 
                <h3>
                  {matchedText(data.username, keyword)}
                </h3>
				//유저 계정 아이디가 일치하면 하이라이트 처리 
                <small>
                  &#64;{matchedText(data.accountname, keyword)}
                </small>
              </div>
            </Link>
          </div>
        </li>
      ))}
    </Container>
  );
};

const Markedtext = styled.span`
  color: ${PALLETS.ORANGE};
`;

export default UserList;

📸 기능 구현 화면

0개의 댓글