오늘은 저번에 이어서 고양이 사진 검색 사이트 두번째 해설 강의로 새로운 기능을 추가로 구현해나가는 날이다
기능 구현하는 방법을 잘 공부해두어야겠다
오늘도 화이팅!
# 미션 2
# 모달 제어
모달 클릭 시 해당 모달에 대한 상세 정보 띄우기 + css 수정
1. imageInfo - 모달 상세 정보 보여주는 메소드 작성
//모달에서 상세 정보 보여주기
//async - await 적용 가능
showDetail(data) {
// 상세 정보 요청
api.fetchCatDetail(data.cat.id).then(({ data }) => {
// 정보 업데이트
this.setState({
visible: true,
cat: data,
});
});
}
2. App.js 수정 - onClick 이벤트 발생 시 showDetail 메소드 실행
this.searchResult = new SearchResult({
$target,
initialData: this.data,
onClick: (cat) => {
this.imageInfo.showDetail({
visible: true,
cat,
});
},
});
3. api.js 수정 - 디테일 정보 받아오는 api 생성
fetchCatDetail: (id) => {
return fetch(`${API_ENDPOINT}/api/cats/${id}`).then((res) => res.json());
},
# 모달 닫기
1. ImageInfo - 모달을 닫는 close 메소드 생성
// 모달 닫기
closeImageInfo() {
console.log("닫기");
this.setState({
visible: false,
cat: undefined,
});
}
2. SearchInput - render 메소드 내에 모달 닫을 수 있도록 로직 작성
//x 표시 눌러서 닫기
// this.$imageInfo.querySelector(".close").addEventListener("click", (e) => {
// this.closeImageInfo();
// });
// esc 눌러서 닫기
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
this.closeImageInfo();
}
});
this.$imageInfo.addEventListener("click", (e) => {
console.log(e.target.className);
if (
e.target.className === "ImageInfo" ||
e.target.className === "close"
) {
this.closeImageInfo();
}
});
# keypress VS. keydown VS. keyup
▶keypress
실제로 입력된 키에 반응한다
반복적으로 발생하지 않으며 연속적인 키 입력에 대해 반응하지 않는다
주로 문자 키 입력에 사용되며 특수 키 등에는 반응하지 않을 수 있다
document.addEventListener('keypress', function(event) {
console.log('Key press:', event.key);
});
▶keydown
키보드의 키가 처음 눌렸을 때 발생하는 이벤트이다
키를 누를 때마다 한 번만 발생하며 계속 누르고 있는 동안에는 반복적으로 발생한다
반복 키를 처리하는 데 유용하다
모든 키에 대한 이벤트 처리가 가능하며 Shift, Ctrl, Alt 등에도 반응한다
document.addEventListener('keydown', function(event) {
console.log('Key down:', event.key);
});
▶keyup
키보드의 키를 눌렀다가 놓았을 때 발생한다
키를 눌렀다 놓을 때 한 번만 발생한다
모든 키에 대한 이벤트 처리가 가능하며 Shift, Ctrl, Alt 등에도 반응한다
document.addEventListener('keyup', function(event) {
console.log('Key up:', event.key);
});
# 로컬 스토리지
1. 최근 검색한 키워드 SerchInput 아래에 표시하기
더미 데이터를 만들어서 SearchInput 아래에 띄운다
SearchInput 컴포넌트 하위에 KeywordHistory 컴포넌트 삽입
this.KeywordHistory = new KeywordHistory({
$target,
onSearch,
});
KeywordHostory 컴포넌트 생성 후 로직 작성 (전체 코드 생략)
데이터를 가지고 오는 로직을 구현할 때는 더미데이터를 만들어서 가져와보는 부분부터 작업하면 수월하다
init() {
let dummy = ["아", "고양이", "캣"];
this.setState(dummy);
}
더미데이터로 직접 집어넣었을 때 잘 동작한다
2. 해당 영역에 표시된 특정 키워드 누르면 검색 일어나도록 구현하기 (최근 검색 키워드 5개)
이제 로컬스토리지에서 데이터를 가지고 오자
//로컬 스토리지에서 데이터 가져오기
init() {
const data = localStorage.getItem("keywordHistory");
console.log(data);
this.setState(data);
}
이렇게 로직을 작성해주고 확인하니 아무것도 뜨지 않는다
왜냐면 로컬스토리지에 아무것도 없기 때문! (당연한 것)
로컬스토리지는 string 형태만 저장할 수 있다
따라서 데이터를 가공해서 문자열을 배열로 만들자 -> split 사용
localStorage 에 넣을 때는 다시 string으로 변환한다 -> join 사용
새로고침 시 localStorage 에 아무것도 없는 경우의 null 예외처리를 해주어야 한다 -> 삼항연산자 사용
keyup 에서 한글을 사용시 enter 가 두번 요청되는 오류가 발생한다 -> keypress 사용
$searchInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
onSearch(e.target.value);
// 최근 키워드 저장
//배열로 push
//null 일 때의 예외처리
let keywordHistory =
localStorage.getItem("keywordHistory") === null
? []
: localStorage.getItem("keywordHistory").split(",");
keywordHistory.push(e.target.value);
//join 사용해서 string 으로
localStorage.setItem("keywordHistory", keywordHistory.join(","));
}
});
메소드 분리
부모 컴포넌트에서 자식 컴포넌트로 분리 -> 부모 컴포넌트에서 부르기
$searchInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
onSearch(e.target.value);
this.KeywordHistory.addKeyword(e.target.value);
}
});
자식 컴포넌트에서 메서드 분리하기
addKeyword(keyword) {
// 최근 키워드 저장
//배열로 push
//null 일 때의 예외처리
let keywordHistory =
localStorage.getItem("keywordHistory") === null
? []
: localStorage.getItem("keywordHistory").split(",");
keywordHistory.unshift(keyword);
//최근 검색어 5개 제한
keywordHistory = keywordHistory.slice(0, 5);
//join 사용해서 string 으로
localStorage.setItem("keywordHistory", keywordHistory.join(","));
}
메소드 하위에 init() 메소드를 불러와서 검색어 입력 시 바로 렌더링 되도록 구현하기
# unshift
배열의 맨 앞에 하나 이상의 요소를 추가하는 메서드
기존 요소들은 인덱스가 하나씩 증가하게 되므로 새로운 요소가 맨 앞에 위치한다
배열의 길이가 늘어나며, 배열을 큐로 사용할 때나 배열의 순서를 동적으로 조작해야 할 때 유용하다
//unshift example
let fruits = ['Banana', 'Orange', 'Apple'];
// 배열 맨 앞에 요소 추가
fruits.unshift('Mango', 'Pineapple');
console.log(fruits);
// 출력: ['Mango', 'Pineapple', 'Banana', 'Orange', 'Apple']
3. 페이지 새로고침 시에도 마지막 검색 결과 화면 유지
App.js에서 로직 작성
this.searchInput = new SearchInput({
$target,
onSearch: (keyword) => {
this.Loading.show();
api.fetchCats(keyword).then(({ data }) => {
this.setState(data);
this.Loading.hide();
// 로컬에 저장
this.saveResult(data);
});
//받은 검색결과 값을 localStorage에 저장하는 메소드
saveResult(result) {
console.log(result);
//JSON.stringify 메소드 사용해서 string으로 저장
localStorage.setItem("lastResult", JSON.stringify(result));
}
//init 메소드
init() {
const result =
localStorage.getItem("lastResult") === null
? []
: JSON.parse(localStorage.getItem("lastResult"));
this.setState(lastResult);
}
setItem getItem
key-value -> join, split 작업 필요
null 처리 잘해주기
JSON.parse, JSON.stringify
# 스크롤 다음 페이지
검색 결과 화면에서 유저가 브라우저 스크롤 바가 끝까지 이동하면 그 다음 페이지 로딩가 로딩된다
1. App.js - onNextPage 메소드
// 배열 합칠 때는 concat, 요소 합칠 때는 push
onNextPage: () => {
console.log("다음 페이지 로딩");
this.Loading.show();
const keywordHistory =
localStorage.getItem("keywordHistory") === null
? []
: localStorage.getItem("keywordHistory").split(",");
const lastKeyword = keywordHistory[0];
const page = this.page + 1;
//새로운 api에 의해서 새로운 데이터를 기존 데이터에 추가한다
api.fetchCats("cat", page).then(({ data }) => {
let newData = this.data.concat(data);
this.setState(newData);
this.page = page;
this.Loading.hide();
});
},
2. SearchResult - 스크롤 메서드
//스크롤 시 다음 목록 랜더링
isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
applyEventToElement = (items) => {
document.addEventListener("scroll", () => {
items.forEach((el, index) => {
if (this.isElementInViewport(el) && items.length - 1 === index) {
this.onNextPage();
}
});
});
};
# 미션 3
# 검색 결과 리팩토링
잦은 scroll 이벤트를 리팩토링한다
scroll 이벤트는 계속해서 발생하는데 이는 CPU, 메모리 낭비 등의 문제 뿐만 아니라 사용자 경험도 하락시킨다
따라서 위에 구현한 스크롤 이벤트를 리팩토링하여 개선한다
# IntersectionObserver
listObserver = new IntersectionObserver((items, observer) => {
items.forEach((item) => {
//item이 화면에 보일 때
if (item.isIntersecting) {
//이미지를 로드한다
//dataset의 src로 대체한다
//레이지 로딩
item.target.querySelector("img").src =
item.target.querySelector("img").dataset.src;
//마지막 요소를 찾아낸다
let dataIndex = Number(item.target.dataset.index);
console.log(dataIndex);
//마지막 요소 라면? -> nextPage 호출
if (dataIndex + 1 === this.data.length) {
this.onNextPage();
}
}
});
render() {
this.$searchResult.innerHTML = this.data
.map(
(cat, index) => `
<li class="item" data-index = ${index}>
<img src="https://via.placeholder.com/200x300" data-src=${cat.url} alt=${cat.name} />
</li>
`
)
.join("");
this.$searchResult.querySelectorAll(".item").forEach(($item, index) => {
$item.addEventListener("click", () => {
this.onClick(this.data[index]);
});
//observer 등록
this.listObserver.observe($item);
# getBoundingClientRect()
DOM 요소의 위치와 크기에 대한 정보를 제공한다
반환되는 객체는 해당 요소의 상대적인 위치를 나타내는 사각형 정보를 포함한다
top, left, bottom, right, width, height 등의 정보가 포함된다
일반적으로 스크롤이나 뷰포트 내에서 요소의 위치를 확인하고자 할 때 사용된다
const element = document.getElementById('myElement');
const rect = element.getBoundingClientRect();
console.log(rect.top, rect.left, rect.bottom, rect.right);
# isIntersecting()
IntersectionObserverEntry 객체의 속성 중 하나로, 대상 요소가 현재 뷰포트와 교차되는지 여부를 나타낸다
IntersectionObserver 를 사용하면 스크롤 이벤트를 직접 관리하지 않고도 특정 요소가 화면에 나타나거나 사라지는 등의 상호작용을 감지할 수 있다
true 일 때 요소가 뷰포트에 진입, false 일 때 뷰포트를 벗어난 것이다
# 새로 알게 된 것
이번 강의의 모든 내용을 새로 알게 되었다
확실하게 배워두면 유용할 기능들을 많이 구현했다
그런데 어렵다.. 한번 강의를 본 것으로는 완벽하게 이해하지 못했다
구현하는 방법은 알았지만 다시 해보라고 하면 못할 것 같은..
그래도 반복되는 로직들을 통해서 전체 구조를 잡는 법, 메소드로 따로 분리하는 법, 부모 컴포넌트에서 자식 컴포넌트로 넘겨주는 것 등을 확실하게 알았다
스크롤 내리는 기능 구현은 완벽하게 이해될 때까지 다시 한번 구현해봐야겠다
다음 강의도 열심히 공부하고 정리해서 실력을 길러야겠다
'TIL > 프로그래머스 데브코스' 카테고리의 다른 글
클라우딩 어플리케이션 엔지니어링 TIL Day 18, 19 - 고양이 사진첩 만들기 모의고사, 최종 모의고사 후기 (1) | 2024.01.24 |
---|---|
클라우딩 어플리케이션 엔지니어링 TIL Day 17 - 고양이 사진 검색 사이트 해설 (3) (0) | 2024.01.20 |
클라우딩 어플리케이션 엔지니어링 TIL Day 15 - 고양이 사진 검색 사이트 해설 (1) (0) | 2024.01.18 |
클라우딩 어플리케이션 엔지니어링 데브코스 1기 월간 회고 1편 - 시작이 반이다 (0) | 2024.01.17 |
클라우딩 어플리케이션 엔지니어링 TIL Day 14 - 가계부 서비스 기능 개발하기 (0) | 2024.01.17 |