개요
지난번에 이어서 문제를 더 풀어볼게요~
<검색 페이지 관련>을 풀려고합니다
원래는 <스크롤 페이징 구현>을 먼저 풀려고했는데 여러가지 문제가..
접는 글로 주저리 썼던걸 넣어둘게요 ㅎㅎ
Q.검색 결과 화면에서 유저가 브라우저 스크롤 바를 끝까지 이동시켰을 경우, 그 다음 페이지를 로딩하도록 만들어야 합니다.
그 다음페이지라고하면 흠.. 뭘까요? 일단 코드를 봐야알거같네요
fetchCats api를 사용한다고했을때 f를 검색하면 32개가 나오거든요

또한 a를 검색해도 동일하게 32개가 나옵니다
그렇다면 이건 32개씩 쪼개서 보내주는걸까요?
그렇다면 다음 페이지만큼의 32개를 가져오려면 어떻게 해야하는걸까요?
문제에 나와있는 api설명은 충분하지않은것같은데요..흠..
도대체 뭘 어떻게 하라는 건지요...
어떻게 하라는 겁니까 도대체?
다음 페이지가 뭡니까..
api에서 사용할수있는 파라미터를 알려주시던가요.. 그냥 때려맞추라는겁니까뭡니까 ㅜ
뭔가 문제를 계속 보니 <검색 페이지 관련> 에서 있는 "필수 SearchInput 옆에 버튼을 하나 배치하고, 이 버튼을 클릭할 시 /api/cats/random50 을 호출하여 화면에 뿌리는 기능을 추가합니다. 버튼의 이름은 마음대로 정합니다." 이 문제와 연계된것일수도 있다는 생각이 드네요
그러면 <검색 페이지 관련>을 먼저 해보겠습니다;
Q.페이지 진입 시 포커스가 input 에 가도록 처리하고, 키워드를 입력한 상태에서 input 을 클릭할 시에는 기존에 입력되어 있던 키워드가 삭제되도록 만들어야 합니다.
이 문제 난이도는 그리 높지 않은것같네요
focus()함수랑 처리를 하면될거같습니다
일단 포커스가 input에 가도록 처리했습니다
// App.js
this.searchInput.$searchInput.focus()
두번째는 input을 클릭했을때네요
value를 null로줘서 리스너를 추가했습니다
// searchInput.js
$searchInput.addEventListener("click", e =>{
$searchInput.value = null
})
$searchInput.addEventListener("keyup", e => {
if (e.key === "Enter") {
onSearch(e.target.value);
}
});
더불어 밑에 keyup도 keyCode를 사용하고있어서 key를 사용하게 바꾸었습니다
Q.필수 데이터를 불러오는 중일 때, 현재 데이터를 불러오는 중임을 유저에게 알리는 UI를 추가해야 합니다.
이번엔 이문제네요
UI추가 별로 자신이 없는데요
저는 그냥 로딩중이라는 텍스트가 띄어지도록 할게요
imageInfo파일을 많이 참고해서 컴포넌트를 새로 만들었습니다
// loading.js
export class Loading {
$loading = null
constructor({$target, data}){
const $loading = document.createElement("div")
$loading.className = "Loading"
this.$loading = $loading
$target.appendChild($loading)
this.data = data;
this.render()
}
setState(nextData){
this.data = nextData;
this.render();
}
render(){
if(this.data.visible){
this.$loading.innerHTML = `
<div class="content-wrapper">
로딩중 ~ |
</div>`;
this.$loading.style.display = "block"
} else {
this.$loading.style.display = "none";
}
}
}
그리고 App.js에 추가한 부분입니다
// App.js
this.loading.setState({
visible: true
})
await api.fetchCats(keyword).then(({ data }) => this.setState(data));
this.loading.setState({
visible: false
})
...
onClick: async image => {
this.loading.setState({
visible: true
})
image = await api.fetchCat(image.id).then(({data})=>image=data);
this.loading.setState({
visible: false
})
this.imageInfo.setState({
visible: true,
image
});
}
...
this.loading = new Loading({
$target,
data: {
visible: false
}
})
style 추가한 부분이에요
<!-- style.css-->
.Loading {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
}
.Loading .content-wrapper {
font-size: 40px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border: 1px solid #eee;
border-radius: 5px;
}
나오는 화면은 이렇습니다
imageInfo를 많이 참고할수있어서 망정이지
처음부터 한다고하면 좀 난감해질거같습니다;;
또한 css랑 글씨체도 이미 세팅되어있는 부분들이 있어서 따라할 수 있었구요
흠 여러모로 걸리는 점이 많지만 일단 다음 문제로 넘어가볼게요
Q.(필수) 검색 결과가 없는 경우, 유저가 불편함을 느끼지 않도록 UI적인 적절한 처리가 필요합니다.
일치되는 고양이 사진이 없을경우 처리군요
간단하게 이렇게 처리했더니 문제점이 있네요~
// searchResult.js
if(this.data.length > 0){
this.$searchResult.innerHTML = this.data
.map(
cat => `
<div class="item">
<img src=${cat.url} alt=${cat.name} />
</div>
`
)
.join("");
this.$searchResult.addEventListener("click", event => {
const $target = event.target
const $item = selector.findSelectorInUpper($target, ".item")
if($item){
const index = Array.from(this.$searchResult.querySelectorAll(".item")).indexOf($item)
this.onClick(this.data[index])
}
})
} else {
this.$searchResult.innerHTML = `
<div class="empty">
일치하는 고양이가 없네요 ~ |
</div>
`
}
사용자가 아무 검색도 하지않았는데 이렇게 나오고있다는 점이에요!
검색하기 전인지 후인지에 따라서 다르게 나오게 해야할것같아요
searchResult에서 받는 data를 개조해봤어요
// App.js
this.searchResult = new SearchResult({
$target,
initialData: {
data: this.data,
initial: true
},
...
setState(nextData) {
console.log(this);
this.data = nextData;
this.searchResult.setState({
data: nextData,
initial: false
});
}
// searchResult.js
else if(!this.data.initial) {
this.$searchResult.innerHTML = `
<div class="empty">
일치하는 고양이가 없네요 ~ |
</div>
`
}
이렇게 조건문을 추가해서
처음에는 아무문구도 안뜨고, 검색 결과가 없을 경우에만 표시가되도록 했어요~
Q.최근 검색한 키워드를 SearchInput 아래에 표시되도록 만들고, 해당 영역에 표시된 특정 키워드를 누르면 그 키워드로 검색이 일어나도록 만듭니다. 단, 가장 최근에 검색한 5개의 키워드만 노출되도록 합니다.
흠 이것도 컴포넌트를 새로 만드는게 낫겠네요
일단 새로 만들어봤어요
// latestSearch.js
export class LatestSearch {
$latestSearch = null
data = null
constructor({$target}){
const $latestSearch = document.createElement("div")
$latestSearch.className = "LatestSearch"
this.$latestSearch = $latestSearch
$target.appendChild($latestSearch)
this.data = []
this.render();
}
setState(nextData){
this.data = nextData
this.render()
}
addData(keyword){
if(this.data.length<5){
this.setState([keyword, ...this.data])
} else {
this.setState([keyword, this.data.slice(1)])
}
}
render(){
if(this.data.length>0){
const description = '최근 검색어 : '
const searches = this.data.map(item =>
`<span class="item">${item}</span>`
)
this.$latestSearch.innerHTML = description + searches
}
}
}
// App.js
onSearch: async keyword => {
this.loading.setState({
visible: true
})
await api.fetchCats(keyword).then(({ data }) => this.setState(data));
this.latestSearch.addData(keyword)
this.loading.setState({
visible: false
})
}
...
this.latestSearch = new LatestSearch({
$target
})
그리고 App.js에도 위처럼 틈틈히 추가해주었어요
모습은 이렇습니다
이제 표시는 됐고
눌렀을때 검색이 되도록 해야겠죠?
searchInput에 들어가있는 onSearch를 이용할수있게 변수화하고 App.js파일에서 수행하게했어요
// searchInput.js
this.onSearch = onSearch
...
$searchInput.addEventListener("keyup", e => {
if (e.key === "Enter") {
this.onSearch(e.target.value);
}
});
// App.js
this.latestSearch = new LatestSearch({
$target,
searchInput: keyword =>{
this.searchInput.setState(keyword)
this.searchInput.onSearch(keyword)
}
})
그리고 latestSearch에서 이벤트 등록하는 부분을 constructor로 옮겼습니다
// latestSearch.js
constructor({$target, searchInput}){
...
this.$latestSearch.addEventListener("click", e =>{
const $target = e.target
const $item = selector.findSelectorInUpper($target, ".item")
if($item){
searchInput(e.target.innerText)
}
})
휴 어찌저찌 해결이됐네요
Q.페이지를 새로고침해도 마지막 검색 결과 화면이 유지되도록 처리합니다.
갈수록 까다로운 문제들이 나오는군요
웹 스토리지를 이용해야겠죠? 이부분은 저도 순수 javascript로는 처음해보는데요
// 키에 데이터 쓰기
localStorage.setItem("key", value);
이런식으로 하면 된다고하네요??
이걸 이용해서 해보죠
파일을 새로 하나 만들었어요 store라는 폴더 안에 만들었습니다
// store/storeSearch.js
export default {
setLatestSearch(keyword){
localStorage.setItem("latestSearch", keyword)
},
getLatestSearch(){
return localStorage.getItem("latestSearch")
},
removeLatestSearch(){
localStorage.removeItem("latestSearch")
}
}
그리고 App.js에서 사용해주었습니다
// App.js
storeSearch.setLatestSearch(keyword)
await api.fetchCats(keyword).then(({ data }) => this.setState(data));
this.latestSearch.addData(keyword)
this.loading.setState({
visible: false
...
const latestSearch = storeSearch.getLatestSearch()
if(latestSearch){
this.latestSearch.searchInput(latestSearch)
storeSearch.removeLatestSearch()
}
이렇게하니까 새로고침하면 마지막에 검색한게 뜨네요
두번째 새로고침하면 뜨지않도록 삭제하는것도 넣어줬어요
Q.(필수) SearchInput 옆에 버튼을 하나 배치하고, 이 버튼을 클릭할 시 /api/cats/random50 을 호출하여 화면에 뿌리는 기능을 추가합니다. 버튼의 이름은 마음대로 정합니다.
저거 랜덤 api이용하는게 한 항목으로 따로있던데요
그건 result위의 배너고 이건 input옆의 버튼이라 다른 영역으로 보는듯합니다
일단 버튼을 만들어봤습니다
// randomButton.js
export class RandomButton {
constructor({$target, buttonClick}){
this.$randomButton = document.createElement("button")
this.$randomButton.className = "RandomButton"
$target.appendChild(this.$randomButton)
this.$randomButton.addEventListener("click", e=>{
buttonClick()
})
this.render()
}
render(){
this.$randomButton.innerHTML = `|Random|`
}
}
화면엔 이렇게 나와요
input 옆에 나오게해야겠죠?
저는 저 searchInput과 randomButton을 searchLine이라는 걸로 한번 감싸려고합니다
지금은 searchInput이 App에서 파생되는데, 그렇게 하지않고
App이 searchLine을 파생하고, searchLine이 searchInput과 randomButton을 파생하게 했어요
// App.js
this.searchLine = new SearchLine({
$target,
onSearch: keyword => {
this.loading.loadingOn()
this.latestSearch.addItem(keyword)
store.setLocalStorage("latestSearch", keyword)
api.fetchCats(keyword).then(({ data }) => {
this.setState(data)
this.loading.loadingOff()
});
},
buttonClick: () =>{
this.loading.loadingOn()
api.fetchCatsRandom().then(({ data }) => {
this.setState(data)
this.loading.loadingOff()
});
}
})
// searchLine.js
import { RandomButton } from "./randomButton.js"
import { SearchInput } from "./SearchInput.js"
export class SearchLine {
constructor({$target, onSearch, buttonClick}){
this.$searchLine = document.createElement("div")
this.$searchLine.className = "SearchLine"
$target.append(this.$searchLine)
this.randomButton = new RandomButton({
$target: this.$searchLine,
buttonClick
})
this.searchInput = new SearchInput({
$target: this.$searchLine,
onSearch
})
}
}
이런식입니다
그럼 이제 css를 이용해서 한 라인에 나오도록 고쳐볼게요
// css.style
.SearchLine {
display: flex
}
searchLine을 flex로 값을 주니
이렇게 정렬이 됐네요
이걸로 만족하고 넘어가겠습니다 ㅎ
Q.lazy load 개념을 이용하여, 이미지가 화면에 보여야 할 시점에 load 되도록 처리해야 합니다.
점점 난이도가 높아지네요!
==> 이거 문제를 풀다가 <HTML, CSS 관련> 문제를 처리하고나서 하는게 낫다고 판단이 되네요
고양이 사진 검색 사이트 (4) 포스팅에서 이어서 할게요~!
Q.추가 검색 결과 각 아이템에 마우스 오버시 고양이 이름을 노출합니다.
이번 포스팅의 마지막 문제가 될거같네요~
일단 기본적으로 title이라는 속성을 이용하면 될거같은데요~
// searchResult.js
.map(
cat => `
<div class="item">
<img src=${cat.url} alt=${cat.name} title=${cat.name}/>
</div>
`
)
title속성을 이용해서 마우스오버 효과를 주었습니다
이런식입니다 ㅎ
이번 포스팅은 여기까지하고
못다한 문제풀이는 다음 포스팅에서 이어서 풀어볼게요~
조회하시는분들에게 도움이 되었길!!
'기술 > 과제테스트 연습' 카테고리의 다른 글
[프로그래머스 과제테스트 연습] 고양이 사진 검색 사이트 (5) - 프론트엔드 (스크롤 페이징 구현) (0) | 2023.06.26 |
---|---|
[프로그래머스 과제테스트 연습] 고양이 사진 검색 사이트 (+) - 프론트엔드 (기타) (0) | 2023.06.26 |
[프로그래머스 과제테스트 연습] 고양이 사진 검색 사이트 (4) - 프론트엔드 (HTML, CSS 관련) (0) | 2023.06.23 |
[프로그래머스 과제테스트 연습] 고양이 사진 검색 사이트 (2) - 프론트엔드 (이미지 상세 보기 모달 관련) (0) | 2023.06.22 |
[프로그래머스 과제테스트 연습] 고양이 사진 검색 사이트 (1) - 프론트엔드 (코드 구조 관련) (0) | 2023.06.21 |