React 코딩 챌린지 : TypeHead
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.