
https://github.com/learn-programmers/KDC_jsTest
이번강의는 [2.레이아웃, API요청] ➡️ [3.로딩UI] ➡️ [1.랜덤버튼] 이 순서로 들어야 하는 것 같다.
프론트: npm start server
백: npm start server
[frontend] 파일부터 살펴보기
index.html을 프로젝트의 초기파일로 본다.
main이 가장 뒤에 있는 이유?
api, util등 메소드들이 담긴 파일들을 다 로드하고 마지막에 실행하라는 의미.
new App(document.querySelector("#App"))
:index.html의 body에 있던 <div id="App"></div>에 넣는다는?
App.js
console.log("app is running!");
class App {
$target = null;//DOM을 가리킨다.
data = [];// Appdㅣ라는 컴포넌트가 가지고 있는 데이터 (상태랑 통일하게 취급?)
constructor($target) {//타켓 받아서 초기화, 하면 화면에 3개의 인스턴스가 보인다
this.$target = $target;
this.searchInput = new SearchInput({//1
$target,
onSearch: keyword => {//onSearch(e.target.value)
api.fetchCats(keyword).then(({ data }) => this.setState(data));
}
});
this.searchResult = new SearchResult({//2
$target,
initialData: this.data,
onClick: image => {
this.imageInfo.setState({
visible: true,
image
});
}
});
this.imageInfo = new ImageInfo({//3
$target,
data: {
visible: false,
image: null
}
});
}
setState(nextData) {
console.log(this);
this.data = nextData;
this.searchResult.setState(nextData);
}
}
[SearchInput.js]
const TEMPLATE = '<input type="text">';
class SearchInput {
constructor({ $target, onSearch }) {
const $searchInput = document.createElement("input");// $searchInput: DOM요소를 하나 만든다.
//input 엘리먼트 하나 만들어서 클래스에 등록.
this.$searchInput = $searchInput;
this.$searchInput.placeholder = "고양이를 검색해보세요.|";
$searchInput.className = "SearchInput";
$target.appendChild($searchInput);//$target은 SearchInput클래스 전체의 타깃이기 때문에 웨에서 만든 3줄(자식)을 넣어줘야 함.
$searchInput.addEventListener("keyup", e => {
if (e.keyCode === 13) {//키보드 타이핑에 의해서 키코드가 13(Enter)이면
onSearch(e.target.value);//target.value는 keyword
}
});
console.log("SearchInput created.", this);
}
render() {}
}

백엔드 서버가 4001번포트로 열려 있어서 [api.js]파일에
const API_ENDPOINT ="http://localhost:4001"; 라고 수정했다.
아무렇게나 검색하고 엔터를 누르니 고양이 사진들이 뜬다.

mediaquery
기본 문법
미디어 쿼리는 CSS에서 어떤 스타일을 선택적으로 적용하고 싶을 때 사용한다. (다른 프로그래밍 언어의 if 조건문과 비슷한 개념이라고 생각하시면 이해가 쉬울 것)
@media 키워드로 시작하는 미디어 쿼리의 문법 구조는 아래와 같다.
@media (조건) {
스타일
}
스타일 부분에는 일반적인 CSS 코드가 들어가는데, 조건 부분이 만족될 때는 스타일이 적용되고, 만족되지 않을 때는 스타일이 무시된다.


css파일에 아래와 같이 추가했다.
@media screen and(max-width: 992px){
.SearchResult {
grid-template-columns: repeat(3, minmax(250px, 1fr));//3개
}
}
@media screen and (max-width: 768px){
.SearchResult {
grid-template-columns: repeat(2, minmax(250px, 1fr));//2개
}
}
@media screen and (max-width: 576px){
.SearchResult {
grid-template-columns: repeat(1, minmax(250px, 1fr));//1개
}
}
화면을 줄여도 컬럼4개였는데, 크롬 캐시를 삭제하니 화면 크기를 줄이니까 컬럼이 3,2,1개로 줄어들었다.

class 이름이 content-wrapper 인 것 확인
768인 경우 width: 100%; 추가
@media screen and (max-width: 768px){
.SearchResult {
grid-template-columns: repeat(2, minmax(250px, 1fr));
}
.ImageInfo .content-wrapper{
width: 100%;
}
}

➡️화면을 줄이면 모달크기가 100%가 된다.
다크 모드(Dark mode)를 지원하도록 CSS를 수정해야 합니다.
• CSS 파일 내의 다크 모드 관련 주석을 제거한 뒤 구현합니다.
• 모든 글자 색상은 #FFFFFF , 배경 색상은 #000000 로 한정합니다.
(TIP
• 기존 작성된 prefers-color-scheme 스펙을 확인합니다.
• 주석을 제거하고 결과를 확인합니다.
• 글자 색상, 배경 색상을 확인하면서 미션대로 수정합니다.
/* dark mode 처리*/
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
color: white;
}
}

• 기본적으로는 OS의 다크모드의 활성화 여부를 기반으로 동작하게 하되, 유저가 테마를 토글링 할 수 있도록 좌측 상단에 해당 기능을 토글하는 체크박스를 만듭니다.
(TIP
• 참고 자료의 'The Complete Guide to the Dark Mode Toggle'를 참고하여 토글 버튼을 작성합니다.
• 방법1. input 이벤트 제어, DOM 제어, 로컬스토리지 사용법 등을 각각 학습하여 코드를 작성합니다.
• 방법2. 코드를 copy & paste하여 원하는 동작이 되는 정도만 작성합니다.
SearchInput을 복사해서 DarkModeToggle.js 파일을 만든다.
[html]에 추가:<script src="src/DarkModeToggle.js"></script>
[App.js]에 추가: (searchInput 복사해서 수정)
this.DarkModeToggle = new DarkModeToggle({
$target,
// onSearch: keyword => {
// api.fetchCats(keyword).then(({ data }) => this.setState(data));
// }
});
$DarkModeToggle.addEventListener("change", e => {//체크박스 체크
console.log(e.target);
});

$DarkModeToggle.addEventListener("change", e => {//체크박스 체크
console.dir(e.target);
});

$DarkModeToggle.addEventListener("change", e => {//체크박스 체크
console.dir(e.target.checked);
});
➡️체크 됐을 때 true, 안됐을 때 false 나온다.

체크하면 'color-mode'가 dark로 바뀐다.

윈도우에서 다크모드 여부 받아와서, 다크모드 true이면 체크박스에 체크되게.
if(this.isDarkMode){
this.$DarkModeToggle.checked = true;
}
➡️코드 정리
this.$DarkModeToggle.checked = this.isDarkMode;
[style.css]
/*변수선언*/
:root[color-mode = "dark"]{ /*html의 최상단 컬러모드가 dark면*/
--background: #000;
--textcolor: white;
}
body{
background-color: var(--background);
colot: var(--textcolor);
}
라이트모드

다크모드


[DarkModeToggle.js]의 constructor에 그냥 썼던 것을 초기화 함수를 만들어서 정리하자.
Before
// 초기화
// isDarkMode state, checkbox 상태, html attribute
this.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
//console.log(window.matchMedia('(prefers-color-scheme: dark)').matches);
console.log(this.isDarkMode);
this.$DarkModeToggle.checked = this.isDarkMode;
//console.log("DarkModeToggle created.", this);
document.documentElement.setAttribute('color-mode', this.isDarkMode? 'dark' : 'light');
After
initColorMode(){
// 초기화
// isDarkMode state, checkbox 상태, html attribute
this.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
console.log(this.isDarkMode);
this.$DarkModeToggle.checked = this.isDarkMode;
document.documentElement.setAttribute('color-mode', this.isDarkMode? 'dark' : 'light');
}
🤔중복코드를 묶어보자
Before:document.documentElement.setAttribute가 중복됨
$DarkModeToggle.addEventListener("change", e => {//체크박스 체크
console.dir(e.target.checked);
document.documentElement.setAttribute('color-mode', e.target.checked ? "dark" :"light");
});
this.initColorMode();
}
initColorMode(){
// 초기화
// isDarkMode state, checkbox 상태, html attribute
this.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
//console.log(window.matchMedia('(prefers-color-scheme: dark)').matches);
console.log(this.isDarkMode);
this.$DarkModeToggle.checked = this.isDarkMode;
//console.log("DarkModeToggle created.", this);
document.documentElement.setAttribute('color-mode', this.isDarkMode? 'dark' : 'light');//->this.setColorMode(this.isDarkMode);
}
After
$DarkModeToggle.addEventListener("change", e => {//체크박스 체크
this.setColor(e.target.checked);
});
...
setColor(isDarkMode){
document.documentElement.setAttribute('color-mode', isDarkMode? 'dark' : 'light');
}
직접 대입하는 것보다 set state를 쓰는 게 좋다?
• (선택 미션입니다)
• 현재 HTML 코드가 전체적으로 div>로만 이루어져 있습니다. 이 마크업을 시맨틱한 방법으로 변경해야 합니다
🥲html 태그별 역학을 그새 까먹어서 복습한 후에 도전해봐야겠다.
$target.appendChild($searchInput);가 $target.appendChild($wrapper); 되는 게 잘 이해가 안된다.
예제 1. 로딩 UI
[index.html]에 로딩 컴포넌트 추가
DarkModeToggle.js를 복사해서 Loading.js 만든다.
로딩UI는 render메소드가 필요하다.
[Loading.js]
class Loading {
constructor({ $target}) {
const $loading = document.createElement("div"); //section은 아닐듯?
this.$loading = $loading;
$target.appendChild(this.$loading); //$target에 append
this.render();
}
render(){
this.$loading.innerHTML = `
<div class= "Loading">로딩<div>`;
}
}
[App.js]의 constructor에 로딩을 추가한다.
this.Loading = new Loading({
$target,
});
결과: 로딩 글자가 표시된다 ➡️ ❇️ 스타일을 다듬어보자.

[style.css]
❇️ 덮는 형식으로 만들어보자➡️ postion: fixed;
vh: viewport의 height 화면높이.⬅️여기서는 fixed라서 먹히는 거다?
rgba: 알파값(투명도)설정가능.
.Loading{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
text-align: center;
}
결과: 로딩이 화면 전체를 덮어버림. 
Before
`<div class= "Loading">로딩<div>`
After
❇️ 로딩을 <p> 태그로 감싸고 <p>스타일링을 해주자.
.Loading p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 40px;
}
//가로, 세로 중앙 센터링 방법 중 하나.
결과: 화면 중앙에 글자가 나온다.

❇️ 로딩을 숨겨보자.
방법1. 클래스로 제어
방법2. 컴포넌트의 상태로 제어
➡️상태를 제어해보자.
[Loading.js]
class Loading에 아래 코드 추가.
$loading = null;
data = null;
초기상태 설정해보자.
this.data = {
show: true
}
➡️ 로딩 컴포넌트가 갖는 data는 show속성이 true.
data의 상태가 중요
show==true면 보이고, false면 안보이게.
render(){
if(this.data.show){
this.$loading.innerHTML = `
<div class= "Loading">
<p>
🤔로딩중🤔
</p>
<div>`;
}else{
this.$loading.innerHTML ='';
}
}
state data를 어디서 제어할까?
: [App.js]에 있는 onSearch메소드
: fetchCats 요청 전에 로딩 show
this.searchInput = new SearchInput({
$target,
onSearch: keyword => {//키보드에 input을 입력했을 때
//로딩 show
console.log("show");
this.Loading.show();
api.fetchCats(keyword).then(({ data }) => {
this.setState(data);//data는 목록데이터. 목록데이터가 제공되고,
console.log("hide");
//로딩 hide
this.Loading.hide();
});
},
onRandomSearch: () =>{//[랜덤고양이]버튼을 눌렀을 때.
console.log("랜덤?");
console.log("random show");
this.Loading.show();
api.fetchRandomCats().then(({data}) =>{
this.setState(data);
console.log("random hide");
this.Loading.hide();
})
}
});
❇️로딩 메소드를 만들어보자.
[Loading.js]에 show, hide 메소드 만든다.
this.data.show를 업데이트 해주자.
다른 데(SearchResult.js)서 쓰이는 setState를 가져와보자.
[Loading.js]
show(){
this.setState({
show: true
});
}
hide(){
this.setState({
show: false
});
}
setState(nextData) {
this.data = nextData;
this.render();
}
결과:

네트워크 탭에서 확인해보니 random 50 api가 잘 요청됐다.

Loading.js]
console.log("show"), ("hide")가 안찍혀서 애먹었는데,
알고보니 onSearch에 구현한 것은 "아"같은 Input 입력+enter 일때 동작하고
onRandomSearch에 구현한 것은 [랜덤고양이]버튼을 눌렀을 때 동작하는 것이었다.
나는 onSearch에 콘솔을 찍어놓고 자꾸 [랜덤고양이]버튼을 눌러서 아무것도 안찍히는 거였다.(오늘의 삽질.....)
❓innerHTML
element.innerHTML = 새로운 콘텐츠
HTML 요소의 콘텐츠를 변경한다.
❓z-index
CSS z-index 속성은 위치 지정 요소와, 그 자손 또는 하위 플렉스 아이템의 Z축 순서를 지정합니다. 더 큰 z-index 값을 가진 요소가 작은 값의 요소 위를 덮습니다.
❓position: absolute;
🐥TIP
onSearch 메소드를 활용해보자?
Syntax
Use the event name in methods like addEventListener(), or set an event handler property.addEventListener("search", (event) => {}); onsearch = (event) => {};
키워드 인자 필요.
Event type
A generic Event.
Examples
Copy to Clipboard
// addEventListener version
const input = document.querySelector('input[type="search"]');
input.addEventListener("search", () => {
console.log(`The term searched for was ${input.value}`);
});
Copy to Clipboard
// onsearch version
const input = document.querySelector('input[type="search"]');
input.onsearch = () => {
console.log(`The term searched for was ${input.value}`);
};

⬆️ 랜덤버튼이 input 과 함께 있으면 좋겠음 ➡️ 별도의 컴포넌트를 따로 만들지 않고, SeatchInput.js에 함께 만들어보겠음!
const $RandomButton = document.createElement("button");
this.$RandomButton = $RandomButton;
this.$RandomButton.className = "RandomButton";
this.$RandomButton.textContent = "랜덤고양이";
$target.appendChild($RandomButton);
let element = document.createElement(tagName[, options]);
const $randomButton = document.createElement("button");
HTML 문서에서, Document.createElement() 메서드는 지정한 tagName의 HTML 요소를 만들어 반환합니다. tagName을 인식할 수 없으면 HTMLUnknownElement (en-US)를 대신 반환합니다.
결과: [랜덤고양이]버튼이 생겼다.

버튼을 다듬어보자.
.RandomButton{
width: 18%;
font-size: 40px;
cursor: pointer; //버튼에 커서가 가면 화살표에서 손가락 모양으로 바뀐다.
}
❓궁금증
따로 미디어쿼리를 적용하지 않았는데 화면 크기에 따라 랜덤고양이 버튼의 위치가 바뀐다.
강의를 다 듣고 살펴봐야겠다. 
(990px 정도부터 버튼이 아래로 내려옴.)

버튼이 작동하게 하기 위해 이벤트 리스너를 만들어보자.
[SearchInput.js]에서 원래 있던 input keyup을 활용해서 아래 코드를 추가한다.
$RandomButton.addEventListener("click", e => {
console.log(e);//event log를 찍어보자.
});
결과: 랜덤고양이 버튼을 누르면 이벤트가 찍힌다.

랜덤메서드는 [App.js]에서 정의하고 있다.
[App.js]의 SerachInput안에 onRandomSearch 메서드를 만들고,
this.searchInput = new SearchInput({
$target,
onSearch: keyword => {
api.fetchCats(keyword).then(({ data }) => this.setState(data));
},
onRandomSearch: () =>{
console.log("랜덤?");
}
});
[SearchInput.js]에 인자로 추가한다.
constructor({ $target, onSearch, onRandomSearch })
console.log를 통해 확인해보니 잘 전달됐다.

[api.js]에 RandomCats를 만들어보자.
• 이미지를 검색한 후 결과로 주어진 이미지를 클릭하면 모달이 뜨는데, 모달 영역 밖을 누르거나 / 키보드의 ESC 키를 누르거나 / 모달 우측의 닫기
(x) 버튼을 누르면 닫히도록 수정해야 합니다.
• 모달에서 고양이의 성격, 태생 정보를 렌더링합니다. 해당 정보는 cats/: 1d 를 통해 불러와야 합니다.
(TIP
• 모달 띄우기는 이미 작성이 되어 있습니다. 이벤트 요청과 메소드 호출 구조를 파악합니다.
• 모달을 닫는 메소드를 작성합니다.
• 영역밖 클릭, ESC키 누름에 대한 이벤트를 작성하고 위 닫기 메소드를 호출합니다.
• 검색 결과 화면에서 유저가 브라우저 스크를 바를 끝까지 이동시켰을 경우, 그 다음 페이지를 로딩하도록 만들어야 합니다.
• 과제 API 명세에 page에 대한 내용이 없습니다. 아래처럼 작업을 해주세요
-/cats/search?page=1, /cats/search?page=2 처럼 엔드포인트에 page라는 파라미터를 추가해 주세요
-- 1페이지는 파라미터를 붙일 필요 없습니다.
-- page 파라미터를 제어하는 상태를 추가하여 작성해 보세요.