페블_
반짝이는 시냅스
페블_
전체 방문자
오늘
어제
  • 전체글 보기 (96)
    • QA (0)
    • 프로젝트 회고 (4)
    • 프로젝트 과정 기록 (12)
    • UI 구현 연구일지 (8)
    • Front-end (31)
      • Javascript (7)
      • CSS (10)
      • React (5)
      • Typescript (3)
      • Nextjs (3)
      • 스타일링 라이브러리 (3)
    • Back-end (0)
      • Express (0)
      • DB (0)
    • CS (0)
      • 자료구조 & 알고리즘 (0)
    • CI&CD (1)
    • 툴 사용법 (4)
      • Git (1)
      • Library&패키지 (2)
      • 기타 개발관련 (1)
    • 알고리즘 이론 & 풀이 (36)
      • 백준(BOJ) (14)
      • 프로그래머스 (22)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 선형대수학
  • 토이프로젝트
  • storybook
  • 캐러셀
  • JS
  • 개발블로그_시작
  • TypeScript
  • 파이썬
  • emotion
  • 백준
  • eslint
  • UI 컴포넌트
  • Python
  • 시리즈_표지
  • chartjs
  • 생각
  • react
  • 알고리즘

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
페블_

반짝이는 시냅스

[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (2) - 검색어 키보드로 선택하기
UI 구현 연구일지

[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (2) - 검색어 키보드로 선택하기

2023. 6. 14. 15:45

1편 보러가기

 

[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (1)

전체코드 (관련 코드는 SearchPage와 Search 폴더에 있음) 검색어 자동완성 기능은 의외로 자주 사용되는 UI이기 때문에 디바운스 연습할 겸 미국의 도시명을 검색할 수 있는 검색창 및 자동완성 기능

bluepebble25.tistory.com

전체코드 (관련 코드는 SearchPage와 Search 폴더에 있음)

 

GitHub - bluepebble25/react-pieces: react로 자주 구현되는 솔루션 조각들을 모아놓는 저장소

react로 자주 구현되는 솔루션 조각들을 모아놓는 저장소. Contribute to bluepebble25/react-pieces development by creating an account on GitHub.

github.com

 

앞에서는 마크업 및 데이터 fetch, 검색어 필터링, 디바운스까지 만들어봤다.

이번에 구현할 내용은

1. 검색어 자동완성을 화살표 방향키로 선택하는 기능
2. 키보드로 위로 올라가거나 내려가면 목록도 따라서 스크롤되는 기능

 

 

자동완성 키보드로 선택하는 기능 구현하기

키보드로 현재 몇 번째 아이템을 선택했는지 저장하고 있는 인덱스와, 리스트 아이템 중 key index가 일치하는 요소에게 focus 효과를 주는 원리이다.

 

1. 현재 몇 번째 리스트 아이템을 선택하고 있는지 저장하는 currIndex 상태를 만든다.
    그리고 검색어 자동완성 목록(SearchPreview) 컴포넌트에게 currIndex 값을 내려준다.

function SearchPage() {
  // ...
  const [currIndex, setCurrIndex] = useState(-1);
  
  const selectRef = useRef<HTMLLIElement>(null);
  // ...
  return (
    <Container>
      <Title>Search for Cities</Title>
      <SearchForm
        hasQuery={query ? true : false}
        onChangeInput={onChangeInput}
        onKeyDown={handleArrowKey}
      />
      {query && (
        <SearchPreview
          previewList={previewList}
          currIndex={currIndex}
          selectRef={selectRef}
        />
      )}
    </Container>
  );
  
}

 

2. 아래의 키보드 핸들 함수를 만들어 input에 onKeyDown 이벤트로 부착한다.

  • input 창에서 화살표를 안누른 기본 상태가 currIndex가 -1이고, 첫번째 리스트 아이템을 선택했을 때가 1 이런 식으로 카운트된다. 맨 마지막에서 아래 키를 한번 더 누르면 0이 되어 리스트의 첫 번째로 돌아간다.
const handleArrowKey = (e: React.KeyboardEvent) => {
    const DOWN = 'ArrowDown';
    const UP = 'ArrowUp';
    const ESCAPE = 'Escape';
    const ENTER = 'Enter';

    if (previewList.length > 0) {
      switch (e.key) {
        case DOWN:
          setCurrIndex((prev) => ++prev);
          if (previewList.length === currIndex + 1) {
            // 자동완성 리스트 맨 아래에서 아래 화살표 눌렀을 때 맨 위로
            setCurrIndex(0);
          }
          break;
        case UP:
          setCurrIndex((prev) => --prev);
          if (currIndex <= 0) setCurrIndex(-1);
          break;
        case ESCAPE:
          setCurrIndex(-1);
          break;
        case ENTER:
          if (currIndex >= 0) {
            selectRef.current &&
              (selectRef.current.children[0] as HTMLElement).click();
          }
          break;
      }
    }
  };

 

3. 리스트의 index 값과 currIndex가 일치한 요소에게 isFocused=ture 속성을 부여한다.

styled-component에서 isFocused가 true인 경우에 색을 진하게 주도록 하면 포커스 된 효과가 나타난다.

interface SearchPreviewProps {
  previewList: string[];
  currIndex: number;
  selectRef: React.RefObject<HTMLLIElement>;
}

interface SearchPreviewItemProps {
  isFocused?: boolean;
  selectRef?: React.RefObject<HTMLLIElement>;
}

function SearchPreview({
  previewList,
  currIndex,
  selectRef,
}: SearchPreviewProps) {
  return (
    <SearchPreviewList>
      // ...
    previewList.map((item, index) => {
      const location = item.split('>');
      const isFocused = index === currIndex;
      return (
        <SearchPreviewItem
          key={index}
          isFocused={isFocused}
          ref={isFocused ? selectRef : null}
        >
          <Link
            to={`https://www.google.com/maps/place/${location[0]}+${location[1]}`}
            target="_blank"
          >
            <p>{item}</p>
          </Link>
        </SearchPreviewItem>
      );
    })
    </SearchPreviewList>
  );
}

const SearchPreviewItem = styled.li<SearchPreviewItemProps>`
  // ...
  background-color: ${(props) => props.isFocused && '#efefef'};

  &:hover {
    background-color: #efefef;
  }
`;

 

+) 키보드 핸들러 함수에서 엔터를 눌렀을 때 아이템을 클릭한 효과를 줬다. 그러면 컴포넌트를 누를 때와 같이 검색 결과 페이지로 이동한다. 나는 구글 map에 해당 도시의 지도를 검색하도록 연결했다.

 

리스트 스크롤 기능 구현하기

element.scrollIntoView() 메소드를 사용해 구현할 것이다.

이 메소드는 해당 요소가 호출될 때 사용자에게 잘 보이도록 상위 요소를 스크롤하는 특성을 가졌다.

그래서 선택된 리스트 아이템.scrollIntoView()를 실행하게 되면 상위의 <ul>을 스크롤하게 된다.

 

  • 현재 어떤 요소가 focus 되어 있는지 상위 요소가 알기 위해서는, 앞에서 css 스타일링을 위해 index === currIndex로 구한 isFocus는 불충분하다.
    상위요소가 현재 어떤 아이템이 선택되었는지 알기 위해 isFocused된 요소에 조건부로 ref를 부착하자. SearchPreview(검색 자동완성 리스트)에게 ref를 내려준다.
  • currIndex가 바뀔 때마다(=화살표로 리스트 아이템을 내려가며 선택할 때마다) handleScroll 함수를 실행하도록 한다.
function SearchPage() {
  // ...
  const selectRef = useRef<HTMLLIElement>(null);
  
  useEffect(() => {
    const handleScroll = () => {
      const selectedList = selectRef.current;
      selectedList?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    };

    handleScroll();
  }, [currIndex]);
  
  return (
    <Container>
      <Title>Search for Cities</Title>
      <SearchForm
        hasQuery={query ? true : false}
        onChangeInput={onChangeInput}
        onKeyDown={handleArrowKey}
      />
      {query && (
        <SearchPreview
          previewList={previewList}
          currIndex={currIndex}
          selectRef={selectRef}
        />
      )}
    </Container>
  );
}

 

SearchPrevirwProps는 받아온 ref를 조건부로 리스트에게 달아주기.

아까 어떤 요소가 선택되었는지 알기 위해 currIndex와 index가 일치하는지 검사한 것처럼 여기서도 일치하는 요소에게 ref를 달아주면 된다.

interface SearchPreviewProps {
  previewList: string[];
  currIndex: number;
  selectRef: React.RefObject<HTMLLIElement>;
}

interface SearchPreviewItemProps {
  isFocused?: boolean;
  selectRef?: React.RefObject<HTMLLIElement>;
}

function SearchPreview({
  previewList,
  currIndex,
  selectRef,
}: SearchPreviewProps) {
  return (
    <SearchPreviewList>
      {previewList.length === 0 ? (
        <SearchPreviewItem>
          <p>No Matching Results</p>
        </SearchPreviewItem>
      ) : (
        previewList.map((item, index) => {
          const location = item.split('>');
          const isFocused = index === currIndex;
          return (
            <SearchPreviewItem
              key={index}
              isFocused={isFocused}
              ref={isFocused ? selectRef : null}
            >
              <Link
                to={`https://www.google.com/maps/place/${location[0]}+${location[1]}`}
                target="_blank"
              >
                <p>{item}</p>
              </Link>
            </SearchPreviewItem>
          );
        })
      )}
    </SearchPreviewList>
  );
}

 

마주한 문제와 해결방법 (Typescript 타입 부여)

1. styled-components props 타입 에러 문제

styled-components에서 props를 사용해 스타일링 하려면 아래처럼 styled.li<타입> 형태로 props의 타입을 지정해줘야 한다.

interface SearchPreviewItemProps {
  isFocused?: boolean;
  selectRef?: React.RefObject<HTMLLIElement>;
}

const SearchPreviewItem = styled.li<SearchPreviewItemProps>`
  // ...
  background-color: ${(props) => props.isFocused && '#efefef'};

  &:hover {
    background-color: #efefef;
  }
`;

 

2. 'Element' 형식에 'click' 속성이 없습니다. (ts2339) 문제

keyboard 핸들러에서 엔터 이벤트를 만들 때 <li> 아이템 하위의 <a>태그 (<Link> 컴포넌트)를 선택하려고

selectRef.current.,children[0].click()을 했는데 에러가 발생했다.

'Element' 형식에 'click' 속성이 없습니다. (ts2339)
case ENTER:
  if (currIndex >= 0) {
    selectRef.current &&
      selectRef.current.children[0].click();
  }
  break;

 

그래서 다음과 같이 영어로 검색해봤더니 원인을 찾을 수 있었다.

Property 'click' does not exist on type 'Element'

current.children으로 선택한 엘리먼트의 타입이 HTMLElement가 아니라 자동으로 Element로 인식되었기에 타입을 HTMLElement로 명확히 지정해주면 되는 문제였다.

case ENTER:
  if (currIndex >= 0) {
    selectRef.current &&
      (selectRef.current.children[0] as HTMLElement).click();
  }
  break;

'UI 구현 연구일지' 카테고리의 다른 글

[React] useInfiniteQuery와 Intersection Observer 이용한 무한스크롤 구현 👀 unsplash 이미지 무한으로 불러오기  (0) 2023.07.26
[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (1)  (0) 2023.06.14
[React 컴포넌트] 댓글 기능 구현하기  (0) 2022.07.12
[React 컴포넌트] Like, Dislike (좋아요, 싫어요) 기능 만드는 법  (0) 2022.07.11
[React 컴포넌트] Favorite(찜, 좋아요) 버튼 만드는 법  (0) 2022.07.11
    'UI 구현 연구일지' 카테고리의 다른 글
    • [React] useInfiniteQuery와 Intersection Observer 이용한 무한스크롤 구현 👀 unsplash 이미지 무한으로 불러오기
    • [React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (1)
    • [React 컴포넌트] 댓글 기능 구현하기
    • [React 컴포넌트] Like, Dislike (좋아요, 싫어요) 기능 만드는 법
    페블_
    페블_

    티스토리툴바