클라우딩 어플리케이션 엔지니어링 데브코스-0115

진영·2024년 1월 21일
post-thumbnail

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() {}
}

기초 API요청 확인 및 수정


백엔드 서버가 4001번포트로 열려 있어서 [api.js]파일에
const API_ENDPOINT ="http://localhost:4001"; 라고 수정했다.

아무렇게나 검색하고 엔터를 누르니 고양이 사진들이 뜬다.

반응형웹

유저가 사용하는 디바이스의 가로 길이에 따라 검색결과의 row 당 column 갯수를 적절히 변경해주어야 합니다.

  • 992px 이하: 3개
  • 768px 이하: 2개
  • 576px 이하: 1개

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개로 줄어들었다.

디바이스 가로 길이가 768px 이하인 경우, 모달의 가로 길이를 디바이스 가로 길이만큼 늘려야 합니다.


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); 되는 게 잘 이해가 안된다.


🌟UI 동작 구현 순서🌟

  1. UI의 흐름을 나열해 봅니다 = 시나리오
  2. 동작이 필요한 point를 뽑아 냅니다
    1. browser event
    2. 특정 시점
  3. 메소드를 일단 만들어 배치해 봅니다.
    1. console.log0를 통해 인자(argument)확인
    2. 동작 작성
  4. 작은 리팩토링
    1. 중복 제거
    2. 변수명 점검
  5. 확인 & 개선

예제 1. 로딩 UI

  • UI의 흐름
  1. 목록 요청 시작
    • 로딩 UI show= 등장
  2. 요청중...
    1. show
  3. 요청 끝
  4. 요청 결과 상태에 배치, 대입= Update
  5. 요청 결과 렌더링
    • 로딩 UI hide= 술겨짐

로딩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;


랜덤 버튼 추가하기

  • Searchinput 옆에 버튼을 하나 배치하고, 이 버튼을 클릭할 시 /api/cats/random 을 호출하여 화면에 뿌리는 기능을 추가합니다. 버튼의 이 름은 마음대로 정합니다.

🐥TIP

  • api.js에 random요청 메소드를 추가합니다.
  • 랜덤 버튼 컴포넌트를 화면에 배치합니다.
  • click이벤트에 fetch와 검색 결과 업데이트 구문을 작성합니다.

onSearch 메소드를 활용해보자?

❓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 파라미터를 제어하는 상태를 추가하여 작성해 보세요.

0개의 댓글