[Next.js] 자동 완성 검색 리스트 구현

찐새·2022년 12월 11일
2

next.js

목록 보기
38/41
post-thumbnail
post-custom-banner

[JavaScript] 조잡한 검색 리스트 만들기로 구현한 결과물을 Next.js로 옮겼다.

0. 기본 세팅

데이터로 사용한 배열:

const fruits = ["apple","banana","orange","lemon","lime","pure","peach","berry","dorian","mango","starfruit","dragonFruit","almond","walnut","grape","persimmon"]

input의 변화를 감지하고 데이터를 상태에 저장하는 기본 코드이다. TypeScript 환경이라 변화할 때의 이벤트 인자를 얻기 위해 ChangeEventHandler<HTMLInputElement> 타입을 할당했다.

import { useState } from "react";
import type { ChangeEventHandler } from "react";

export default function Home() {
  const fruits = [...];
  const [isHidden, setIsHidden] = useState(true);
  const [result, setResult] = useState("");
  const [search, setSearch] = useState("");

  const onChange: ChangeEventHandler<HTMLInputElement> = (e) => {
    const { value } = e.currentTarget;
    setSearch(value);
  };
  
  const onResetClick = () => {
    setResult("");
    setSearch("");
  };
  
  return (
      <main className="container">
        <section>
          <input
            type={"search"}
            onChange={onChange}
            value={search}
          />
          <ul hidden={isHidden}>
            {fruits.map((fruit, idx) => (
              <li key={idx} style={{ cursor: "pointer" }}>
                {fruit}
              </li>
            ))}
          </ul>
          <button onClick={onResetClick}>reset</button>
        </section>
        <div>{result}</div>
      </main>
  );
}

1. Focus In/Out

바닐라 JSfocus 이벤트는 focusinfocusout이지만, jsx에서는 onFocusonBlur에 매칭된다.

import type { FocusEventHandler } from "react";

function Home() {
  const onFocusIn: FocusEventHandler<HTMLInputElement> = (e) => {
    setIsHidden(false);
  };
  const onFocusOut: FocusEventHandler<HTMLInputElement> = (e) => {
    setIsHidden(true);
  };

  return (
    <section>
      <input
        onFocus={onFocusIn}
        onBlur={onFocusOut}
        />
      <ul hidden={isHidden}>
        {/* (...) */}
      </ul>
    </section>
  );
};

input에 포커스가 잡히면(onFocus) ul은 노출되고, 벗어나면(onBlur) ul은 다시 숨는다. 삼항 연산자를 사용하면 요소를 초기화면에 렌더링하지 않을 수 있다.

{!isHidden ? (
  <ul>
    {fruits.map((fruit, idx) => (
      <li
        key={idx}
        onMouseOver={onMouseOver}
        onMouseLeave={onMouseLeave}
        onClick={onAddResultClick}
        style={{ cursor: "pointer" }}
        hidden={!fruit.includes(search)}
        >
        {fruit}
      </li>
    ))}
  </ul>
) : null}

2. Mouse Over/Leave

마우스 이벤트이기에 MouseEventHandler<HTMLLIElement>를 할당했다.

import { useState } from "react";
import type { MouseEventHandler } from "react";

function Home() {
  const [liOver, setLiOver] = useState(false);
  
  const onMouseOver: MouseEventHandler<HTMLLIElement> = (e) => {
    setLiOver(true);
    e.currentTarget.style.background = "pink";
  };
  const onMouseLeave: MouseEventHandler<HTMLLIElement> = (e) => {
    setLiOver(false);
    e.currentTarget.style.background = "none";
  };

  return (
    <section>
      {/* (...) */}
      <ul hidden={isHidden}>
        {fruits.map((fruit, idx) => (
          <li
            key={idx}
            onMouseOver={onMouseOver}
            onMouseLeave={onMouseLeave}
            style={{ cursor: "pointer" }}
            hidden={!fruit.includes(search)}
            >
            {fruit}
          </li>
        ))}
      </ul>
    </section>
  );
};

검색한 값을 포함하지 않은 lihidden으로 숨겼다.

3. 검색 요소 클릭

[JavaScript] 조잡한 검색 리스트 만들기에서는 하위 요소 클릭 시 focusout이 먼저 동작하여 클릭이 안 되는 현상을 어렵게 해결했다. 여기서는 한 줄 추가로 상위 요소의 이벤트를 방지했다.

function Home() {
  const onFocusOut: FocusEventHandler<HTMLInputElement> = (e) => {
    if (liOver) return;
    setIsHidden(true);
  };

  const onAddResultClick: MouseEventHandler<HTMLLIElement> = (e) => {
    const { textContent } = e.currentTarget;
    setResult(textContent as string);
    setIsHidden(true);
  };
  return (
    <section>
      {/* (...) */}
      <ul hidden={isHidden}>
        {fruits.map((fruit, idx) => (
          <li
            key={idx}
            onClick={onAddResultClick}
            >
            {fruit}
          </li>
        ))}
      </ul>
    </section>
  );
};

마우스를 올렸을 때 liOvertrue 상태이고, 그럴 때 onFocusOut얼리 리턴(early return)하여 setIsHidden 동작을 막았다.

결과

전체 코드

import { useState } from "react";
import type {
  MouseEventHandler,
  ChangeEventHandler,
  FocusEventHandler,
} from "react";

export default function Home() {
	const fruits = ["apple","banana","orange","lemon","lime","pure","peach",
                    "berry","dorian","mango","starfruit","dragonFruit",
                    "almond","walnut","grape","persimmon"]
  const [isHidden, setIsHidden] = useState(true);
  const [liOver, setLiOver] = useState(false);
  const [result, setResult] = useState("");
  const [search, setSearch] = useState("");

  const onChange: ChangeEventHandler<HTMLInputElement> = (e) => {
    const { value } = e.currentTarget;
    setSearch(value);
  };
  const onFocusIn: FocusEventHandler<HTMLInputElement> = (e) => {
    setIsHidden(false);
  };
  const onFocusOut: FocusEventHandler<HTMLInputElement> = (e) => {
    if (liOver) return;
    setIsHidden(true);
  };
  const onResetClick = () => {
    setResult("");
    setSearch("");
  };

  const onMouseOver: MouseEventHandler<HTMLLIElement> = (e) => {
    setLiOver(true);
    e.currentTarget.style.background = "pink";
  };
  const onMouseLeave: MouseEventHandler<HTMLLIElement> = (e) => {
    setLiOver(false);
    e.currentTarget.style.background = "none";
  };
  const onAddResultClick: MouseEventHandler<HTMLLIElement> = (e) => {
    const { textContent } = e.currentTarget;
    setResult(textContent as string);
    setIsHidden(true);
  };
  return (
    <main className="container">
      <section>
        <input
          type={"search"}
          onFocus={onFocusIn}
          onBlur={onFocusOut}
          onChange={onChange}
          value={search}
        />
        <ul hidden={isHidden}>
          {fruits.map((fruit, idx) => (
            <li
              key={idx}
              onMouseOver={onMouseOver}
              onMouseLeave={onMouseLeave}
              onClick={onAddResultClick}
              style={{ cursor: "pointer" }}
              hidden={!fruit.includes(search)}
            >
              {fruit}
            </li>
          ))}
        </ul>
        <button onClick={onResetClick}>reset</button>
      </section>
      <div>{result}</div>
    </main>
  );
}
profile
프론트엔드 개발자가 되고 싶다
post-custom-banner

0개의 댓글