1. 검색 키워드를 입력하면 추천 검색어 API를 이용해 추천 검색어를 보여준다.
2. 키보드 혹은 마우스로 추천 검색어를 선택할 수 있게 한다.
3. 검색된 결과에 따라 고양이 사진이 화면에 렌더링 되어야한다.
1. 검색 API : https://cat-search.edu-api.programmers.co.kr/keywords?q={keyword}
2. 사진 검색 API : https://cat-search.edu-api.programmers.co.kr/search?q={keyword}
💡 디바운스(debounce)
타이머로 이벤트를 지연시키다가 이벤트가 발생하기 전에 같은이벤트가 또 들어오면 이전의 이벤트를 취소하고 타이머를 거는 동작의 반복을 뜻한다.
❗ 엔터를 누르면 하이라이트 처리된 검색어가 반영되는 기능이 세션 스토리지 연동 구현 이후 작동하지 않는다.
해당 이슈는 현재 확인 중에 있다.
2/20 해당 이슈 수정 완료
🖥 index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>고양이 사진 검색기</title>
<link rel = "stylesheet" href="https://cat-search-dev-serverlessdeploymentbucket-3288xrrz4slb.s3.ap-northeast-2.amazonaws.com/public/css/cat-search.css">
</head>
<body>
<main class="App"></main>
<script src = "./src/main.js" type="module"></script>
</body>
</html>
🖥 main.js
import App from './App.js'
const $target = document.querySelector('.App')
new App({ $target })
🖥 api.js
const END_POINT = 'https://cat-search.edu-api.programmers.co.kr'
export const request = async(url) => {
try {
const res = await fetch(`${END_POINT}${url}`)
if(!res.ok) {
throw new Error('API 호출 실패')
}
return await res.json()
} catch (e) {
alert(e.message)
}
}
🖥 Header.js
import Keyword from "./Keyword.js"
export default function Header ({$target, initialState, onKeywordInput, onEnter}) {
const $header = document.createElement('header')
$header.className = 'Header'
$target.appendChild($header)
this.state = initialState
this.setState = nextState => {
if(this.state.keyword !== nextState.keyword) {
this.state = nextState
keyword.setState({
value: this.state.keyword
})
}
}
const $title = document.createElement('h1')
$title.style.textAlign = 'center'
$title.innerHTML = '🐱고양이 사진 검색기🔍'
$header.appendChild($title)
const keyword = new Keyword({
$target: $header,
initialState : {
keyword: this.state.keyword
},
onKeywordInput,
onEnter
})
}
🖥 keyword.js
export default function Keyword({$target, initialState, onKeywordInput, onEnter}) {
const $keyword = document.createElement('input')
$keyword.className = 'Keyword'
$target.appendChild($keyword)
this.state = initialState
this.setState = nextState => {
this.state = nextState
$keyword.value = this.state.value
}
$keyword.addEventListener('keyup', e => {
if(e.key === 'Enter') {
e.preventDefault()
onEnter()
} else {
onKeywordInput(e.target.value)
}
})
}
🖥 SuggestKeywords.js
export default function SuggestKeywords({$target, initialState, onKeywordSelect}) {
const $suggest = document.createElement('div')
$suggest.className = 'Keywords'
$target.appendChild($suggest)
this.state = initialState
this.setState = nextState => {
this.state = {...this.state, ...nextState}
this.render()
}
this.render = () => {
const { keywords, cursor } = this.state
$suggest.innerHTML = `
<ul>
${keywords && keywords.map((keyword<, i) => `
<li class = "${cursor === i ? 'active' : ''}">${keyword}</li>
`).join('')}
</ul>
`
$suggest.style.display = keywords && keywords.length > 0 ? 'block' : 'none'
}
this.render()
$suggest.addEventListener('click', e => {
const $li = e.target.closest('li')
if($li) {
onKeywordSelect($li.textContent) // 텍스트 요소만 뽑아서 이벤트 작동
}
})
window.addEventListener('keydown', (e) => {
if($suggest.style.display !== 'none') {
const { key } = e
// 위 화살표, 아래 화살표, 엔터 입력 시
if(key === 'ArrowUp') {
const nextCursor = this.state.cursor-1
this.setState({
...this.state,
cursor: nextCursor < 0 ? this.state.keywords.length - 1 : nextCursor
})
} else if(key === 'ArrowDown') {
const nextCursor = this.state.cursor+1
this.setState({
...this.state,
cursor: nextCursor > this.state.keywords.length - 1 ? 0 : nextCursor
})
} else if(key==='Enter') {
onKeywordSelect(this.state.keywords[this.state.cursor])
}
}
})
}
🖥 SearchResults.js
export default function SearchResults ({$target, initialState}) {
const $searchResults = document.createElement('div')
$searchResults.className = 'SearchResults'
$target.appendChild($searchResults)
this.state = initialState
this.setState = nextState => {
this.state = nextState
this.render()
}
this.render = () => {
$searchResults.innerHTML = `
${this.state && this.state.map(result => `
<div>
<img src="${result.url}" ?>
</div>
`).join('')}
`
}
this.render()
}
🖥 storage.js
const storage = window.sessionStorage
export const getItem = (key, defaultValue) => {
try {
const storedValue = storage.getItem(key)
if(storedValue) {
return JSON.parse(storedValue)
}
return defaultValue
} catch {
return defaultValue
}
}
export const setItem = (key, value) => {
storage.setItem(key, JSON.stringify(value))
}
export default {
getItem,
setItem
}
🖥 debounce.js
export default function debounce(fn, delay) {
let timer = null;
return function () {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay)
}
}
🖥 App.js
import { request } from './api.js'
import storage from './storage.js'
import debounce from './debounce.js';
import Header from "./Header.js";
import SuggestKeywords from './SuggestKeywords.js';
import SearchResults from './SearchResults.js';
export default function App({$target}) {
this.state = {
keyword: '',
keywords: [],
catImages:[]
}
this.cache = storage.getItem('keywords_cache', {})
this.setState = nextState => {
this.state = nextState
header.setState({
keyword: this.state.keyword
})
suggestKeywords.setState({
keywords: this.state.keywords
})
if(this.state.catImages.length > 0) {
searchResults.setState(this.state.catImages)
}
}
const header = new Header({
$target,
initialState: {
keyword: this.state.keyword
},
onKeywordInput: debounce(async (keyword) => {
if(keyword.trim().length > 1) {
let keywords = null
if(this.cache[keyword]) {
keywords = this.cache[keyword]
} else {
keywords = await request(`/keywords?q=${keyword}`)
this.cache[keyword] = keywords
storage.setItem('keywords_cache', this.cache)
}
this.setState({
...this.state,
keyword,
keywords
})
}
}, 300),
onEnter: () => {
fetchCatsImage()
}
})
const suggestKeywords = new SuggestKeywords({
$target,
initialState: {
keywords: this.state.keywords,
cursor: -1 // 커서가 -1인 경우는 아무것도 없는 것
},
onKeywordSelect: (keyword) => {
this.setState({
...this.state,
keyword,
keywords: []
})
fetchCatsImage()
}
})
const searchResults = new SearchResults({
$target,
initialState: this.state.catImages
})
const fetchCatsImage = async () => {
const {data} = await request(`/search?q=${this.state.keyword}`)
this.setState({
...this.state,
catImages: data,
keywords: []
})
}
}
🖨 완성 화면
오류 수정에 많은 시간이 걸렸다.
하지만 완성했죠?