React 코딩 챌린지 : TypeHead

발행: (2026년 1월 17일 오후 11:39 GMT+9)
4 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역이 필요한 전체 텍스트를 제공해 주시겠어요? 텍스트를 주시면 요청하신 대로 한국어로 번역해 드리겠습니다.

TypeHead

import { useState, useEffect, useRef } from "react";

const Typeahead = ({ placeholder = "Search..." }) => {
  const [query, setQuery] = useState("");
  const [suggestions, setSuggestions] = useState([]);
  const [allUsers, setAllUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [activeSuggestion, setActiveSuggestion] = useState(-1);
  const inputRef = useRef(null);

  // Fetch all users once on component mount
  useEffect(() => {
    const fetchAllUsers = async () => {
      setIsLoading(true);
      setError(null);
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts`
        );
        if (!response.ok) throw new Error("Failed to fetch users");
        const data = await response.json();
        console.log(data);
        setAllUsers(data.map((user) => user.title));
      } catch (err) {
        setError(err.message);
        setAllUsers([]);
      } finally {
        setIsLoading(false);
      }
    };

    fetchAllUsers();
  }, []);

  // Filter suggestions locally based on query
  useEffect(() => {
    if (query.length 
      title.toLowerCase().includes(query.toLowerCase())
    );
    setSuggestions(filteredSuggestions);
    setActiveSuggestion(-1);
  }, [query, allUsers]);

  // Handle keyboard navigation for accessibility
  const handleKeyDown = (e) => {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setActiveSuggestion((prev) =>
        prev 
        prev > 0 ? prev - 1 : suggestions.length - 1
      );
    } else if (e.key === "Enter" && activeSuggestion >= 0) {
      e.preventDefault();
      setQuery(suggestions[activeSuggestion]);
      setSuggestions([]);
      setActiveSuggestion(-1);
    }
  };

  // Handle suggestion click
  const handleSuggestionClick = (suggestion) => {
    setQuery(suggestion);
    setSuggestions([]);
    setActiveSuggestion(-1);
    inputRef.current.focus();
  };

  return (
    
      
        Search for users
      
       setQuery(e.target.value)}
        placeholder={placeholder}
        aria-autocomplete="list"
        aria-controls="suggestions-list"
        aria-expanded={suggestions.length > 0}
        role="combobox"
        className="typeahead-input"
      />
      {isLoading && Loading...}
      {error && (
        
          {error}
        
      )}
      {suggestions.length > 0 && (
        

          {suggestions.map((suggestion, index) => (
             handleSuggestionClick(suggestion)}
              tabIndex={-1}
            >
              {suggestion}
            
          ))}
        

      )}
    
  );
};

export default Typeahead;

CSS

.typeahead-container {
  position: relative;
  width: 300px;
  margin: 20px auto;
}

.typeahead-input {
  width: 100%;
  padding: 8px;
  font-size: 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.suggestions-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #ccc;
  border-radius: 4px;
  max-height: 200px;
  overflow-y: auto;
  list-style: none;
  padding: 0;
  margin: 4px 0 0 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}

.suggestion-item {
  padding: 8px;
  cursor: pointer;
}

.suggestion-item:hover,
.suggestion-item.active {
  background-color: #f0f0f0;
}

.loading,
.error {
  padding: 8px;
  color: #666;
  font-size: 14px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

주요 변경 사항 및 설명

  • 한 번에 모든 사용자 가져오기 – 컴포넌트가 마운트될 때 https://jsonplaceholder.typicode.com/posts에서 전체 게시물 목록을 가져와 제목을 allUsers에 저장합니다. (실제 사용자를 원한다면 URL을 /users로 교체하세요.)

  • 로컬 필터링 – 매 키 입력마다 요청을 보내는 대신, 컴포넌트는 이미 가져온 제목들을 Array.filter로 로컬에서 필터링하며, 쿼리를 대소문자 구분 없이 매치합니다.

  • 성능 – 작은 데이터 세트(예: JSONPlaceholder에서 반환되는 100개의 게시물)에서는 이 방법이 더 빠르고 네트워크 트래픽을 줄여줍니다. 매우 큰 데이터 세트의 경우 메모이제이션(useMemo)이나 서버‑사이드 페이지네이션을 추가할 수 있습니다.

  • 접근성 및 UI – 키보드 네비게이션, ARIA 속성 및 스타일링은 그대로 유지됩니다. 위와 같이 동일한 styles.css를 사용할 수 있습니다.

arlier.

Notes:

- The local filtering is case‑insensitive for better user experience (`toLowerCase()`).
- If the API fetch fails, an error message is displayed, and the typeahead will not function until the data is loaded.
- For real‑world applications with larger datasets, consider implementing pagination or a more efficient search algorithm if performance becomes an issue.
Back to Blog

관련 글

더 보기 »

ReactJS Hook 패턴 ~Latest Ref 패턴~

ReactJS Hook Pattern ~Latest Ref Pattern~의 커버 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%...

ReactJS 훅 패턴 ~UseImperativeHandle~

useImperativeHandle는 자식 컴포넌트가 ref를 통해 특정 메서드를 부모 컴포넌트에 노출하도록 허용합니다. 이 패턴은 부모가 자식의 함수를 호출해야 할 때 유용합니다.