๊ฐœ๋ฐœ ๊ณต๋ถ€/๋ฐ๋ธŒ์ฝ”์Šค TIL

[ํด๋ผ์šฐ๋”ฉ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์—”์ง€๋‹ˆ์–ด๋ง TIL] 240122 - ๊ณ ์–‘์ด ์‚ฌ์ง„ ๊ฒ€์ƒ‰ ์‹ค์Šต 5

๊ฐ€์šค์ด 2024. 1. 23. 08:35

Intro


์˜ค๋Š˜์€ ๋“œ๋””์–ด! ์ตœ์ข… ๋ชจ์˜ํ…Œ์ŠคํŠธ๋ฅผ ํ’€์—ˆ๋‹ค.

์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ์งง์•˜๋‹ค. 2์‹œ๊ฐ„ ์งœ๋ฆฌ๋Š” ์•„๋‹Œ ๊ฒƒ ๊ฐ™์€๋ฐ.. 2์‹œ๊ฐ„์ด ์ฃผ์–ด์กŒ๋‹ค. ๊ธˆ์š”์ผ(19์ผ)์— ํ’€์–ด์•ผ ํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„์งœ์ธ ๊ฒƒ ๊ฐ™๋‹ค.

์•„๋ฌดํŠผ, ํ•ด์„ค ๊ฐ•์˜๋ฅผ ๋“ค์œผ๋ฉฐ ๋”ฐ๋ผํ•˜๋‹ค๊ฐ€ ๋ง‰์ƒ ํ˜ผ์ž ํ•˜๋ ค๋‹ˆ ๋ญ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋ฉด ์ข‹์„๊นŒ ๊ณ ๋ฏผ์„ ๋งŽ์ด ํ–ˆ๋‹ค.

์‹œ๊ฐ„์ด ๋ถ€์กฑํ•ด ๋‹ค ํ’€์ง„ ๋ชปํ–ˆ์ง€๋งŒ ๋ชป ํ‘ผ ๊ฑฐ ๋‹ค์‹œ ํ’€์–ด๋ณด๊ณ  ๋ณต์Šตํ•ด์•ผ๊ฒ ๋‹ค.

 

 

 

์˜ค๋Š˜ ํ•™์Šตํ•œ ๋‚ด์šฉ

: ๋ชจ์˜ํ…Œ์ŠคํŠธ


<< ๋ชจ์˜ํ…Œ์ŠคํŠธ ํ‘ผ ๋ฌธ์ œ๋“ค >>

HTML, CSS ๊ด€๋ จ

โœ… ํ˜„์žฌ HTML ์ฝ”๋“œ๊ฐ€ ์ „์ฒด์ ์œผ๋กœ <div> ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋งˆํฌ์—…์„ ์‹œ๋งจํ‹ฑํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

 

๊ฒ€์ƒ‰์ฐฝ๊ณผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ฐฝ์€ section์œผ๋กœ ๊ตฌ๋ถ„ํ–ˆ๊ณ  class name์„ ์ฃผ์—ˆ๋‹ค.

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋Š” ul-li ํƒœ๊ทธ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.


โœ… ์œ ์ €๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๋””๋ฐ”์ด์Šค์˜ ๊ฐ€๋กœ ๊ธธ์ด์— ๋”ฐ๋ผ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ์˜ row ๋‹น column ๊ฐฏ์ˆ˜๋ฅผ ์ ์ ˆํžˆ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
992px ์ดํ•˜: 3๊ฐœ / 768px ์ดํ•˜: 2๊ฐœ / 576px ์ดํ•˜: 1๊ฐœ
@media screen and (max-width: 992px) {
  .SearchResult {
    grid-template-columns: repeat(3, minmax(250px, 1fr));
  }
}

@media screen and (max-width: 768px) {
  .SearchResult {
    grid-template-columns: repeat(2, minmax(250px, 1fr));
  }
}

@media screen and (max-width: 576px) {
  .SearchResult {
    grid-template-columns: repeat(1, minmax(250px, 1fr));
  }
}

 

๋ฏธ๋””์–ด ์ฟผ๋ฆฌ ์‚ฌ์šฉ๋ฒ•์€ ์ต์ˆ™์น˜ ์•Š์•„์„œ ๋งค๋ฒˆ ๊ฒ€์ƒ‰ํ•ด์„œ ์ฐพ์•„๋ณธ๋‹ค.

์™ธ์›Œ์•ผ๊ฒ ๋‹ค. @media screen...

 

 

 

์ด๋ฏธ์ง€ ์ƒ์„ธ ๋ณด๊ธฐ ๋ชจ๋‹ฌ ๊ด€๋ จ

โœ… ๋””๋ฐ”์ด์Šค ๊ฐ€๋กœ ๊ธธ์ด๊ฐ€ 768px ์ดํ•˜์ธ ๊ฒฝ์šฐ, ๋ชจ๋‹ฌ์˜ ๊ฐ€๋กœ ๊ธธ์ด๋ฅผ ๋””๋ฐ”์ด์Šค ๊ฐ€๋กœ ๊ธธ์ด๋งŒํผ ๋Š˜๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค.
@media screen and (max-width: 768px) {
  .SearchResult {
    grid-template-columns: repeat(2, minmax(250px, 1fr));
  }

  .ImageInfo .content-wrapper {
    width: 100%
  }
}

 

์ด ๋ฌธ์ œ๋„ ๋ฏธ๋””์–ด์ฟผ๋ฆฌ ๋ฌธ์ œ

max-width: 768px์ผ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์— width: 100%๋ฅผ ์ถ”๊ฐ€ํ•ด ๊ฐ€๋กœ ๊ธธ์ด๋งŒํผ ๋Š˜๋ฆฌ๋Š” ์Šคํƒ€์ผ์„ ์ ์šฉํ–ˆ๋‹ค.

 


โœ… ์ด๋ฏธ์ง€๋ฅผ ๊ฒ€์ƒ‰ํ•œ ํ›„ ๊ฒฐ๊ณผ๋กœ ์ฃผ์–ด์ง„ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ๋œจ๋Š”๋ฐ, ๋ชจ๋‹ฌ ์˜์—ญ ๋ฐ–์„ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ / ํ‚ค๋ณด๋“œ์˜ ESC ํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ / ๋ชจ๋‹ฌ ์šฐ์ธก์˜ ๋‹ซ๊ธฐ(x) ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋‹ซํžˆ๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
this.$imageInfo.addEventListener("click", e => {
  if(e.target.className === "ImageInfo" || e.target.className === "close") this.setState({
      visible: false,
      image: null
    })
  })

document.addEventListener("keydown", e => {
  if(e.key === 'Escape') this.setState({
    visible: false,
    image: null
  })
})

 

ํ•ด์„ค ๊ฐ•์˜ ๋“ค์œผ๋ฉด์„œ ๊ณต๋ถ€ํ•œ ๋ถ€๋ถ„์ด์–ด์„œ ์ˆ˜์›”ํ•˜๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‹ค.

์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ๊ฐ™์•„์„œ ์ด ๋ถ€๋ถ„์€ ํ•จ์ˆ˜๋กœ ๋บ์–ด๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

 


โœ… ๋ชจ๋‹ฌ์—์„œ ๊ณ ์–‘์ด์˜ ์„ฑ๊ฒฉ, ํƒœ์ƒ ์ •๋ณด๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ •๋ณด๋Š” /cats/:id ๋ฅผ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • api.js
export const api = {
  ...
  fetchCatInfo: async id => {
    const result = fetchData(`${API_ENDPOINT}/api/cats/${id}`)
    return result
  },
  ...
}

 

  • SearchResult.js
...

this.$searchResult.querySelectorAll(".item").forEach(($item, index) => {
  $item.addEventListener("click", async () => {
    const catInfo = (await api.fetchCatInfo(this.data[index].id)).data
    this.onClick(catInfo);
  });

  ...

});

 

  • App.js
import { api } from './api.js'

...

this.searchResult = new SearchResult({
  $target,
  initialData: this.data,
  onClick: image => {
    this.imageInfo.setState({
      visible: true,
      image
    });
  }
});

...

 

api.js์—์„œ id๋กœ ์กฐํšŒํ•˜๋Š” API๋ฅผ ์ƒ์„ฑํ•˜๊ณ  SearchResult์—์„œ ๊ณ ์–‘์ด ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ–ˆ์„ ๊ฒฝ์šฐ App์—์„œ ๋ฐ›์€ onClickํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

์ฒ˜์Œ์—” SearchResult์—์„œ api๋ฅผ ํ˜ธ์ถœํ•˜๋ ค ํ–ˆ๋Š”๋ฐ App์— imgaeInfo๋ฅผ ์ˆ˜์ •ํ•ด์ค˜์•ผํ•˜๋‹ˆ App์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๊ฒŒ ์ข‹์€ ๊ฒƒ ๊ฐ™์•˜๋‹ค.

 

 

 

 

๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ ๊ด€๋ จ

โœ… ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ ํฌ์ปค์Šค๊ฐ€ input์— ๊ฐ€๋„๋ก ์ฒ˜๋ฆฌํ•˜๊ณ , ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•œ ์ƒํƒœ์—์„œ input์„ ํด๋ฆญํ•  ์‹œ์—๋Š” ๊ธฐ์กด์— ์ž…๋ ฅ๋˜์–ด ์žˆ๋˜ ํ‚ค์›Œ๋“œ๊ฐ€ ์‚ญ์ œ๋˜๋„๋ก ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

  • SearchInput.js
class SearchInput {
  constructor({ $target, onSearch, onRandomSearch }) {
    ...
    
    // input ํด๋ฆญ ์‹œ ๊ธฐ์กด ์ž…๋ ฅ๋œ ํ‚ค์›Œ๋“œ ์‚ญ์ œ
    this.$searchInput.addEventListener("click", e => {
      e.target.value = ''
    })
    
    this.render()
  }

  render() {
    // ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ ํฌ์ปค์Šค
    this.$searchInput.focus()
  }
}

export default SearchInput

 

๊ฐ•์˜๋ฅผ ๋‹ค ์•ˆ๋“ค์–ด์„œ ๊ทธ๋Ÿฐ๊ฐ€.. ํ•ด์„ค ๊ฐ•์˜์—์„œ ์•ˆํ’€์–ด ๋ณธ ๋ฌธ์ œ๋“ค์ด ์žˆ์–ด์„œ ํ’€์–ด๋ดค๋‹ค.

ํฌ์ปค์Šค๋ฅผ ์–ด๋–ป๊ฒŒ ์ค„์ง€ ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€ ๊ตฌ๊ธ€๋ง์œผ๋กœ focus()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ๋˜์—ˆ๋‹ค.

ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋  ๋•Œ ํฌ์ปค์Šค๋ฅผ ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™์•„์„œ render()ํ•จ์ˆ˜์— ๊ตฌํ˜„ํ–ˆ๋‹ค.

 

inputํด๋ฆญ ์‹œ ๊ธฐ์กด ์ž…๋ ฅํ•œ ๋‚ด์šฉ์„ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด input click์ด๋ฒคํŠธ์— e.target.value๋ฅผ ์ดˆ๊ธฐํ™” ํ•ด ์ฃผ์—ˆ๋‹ค.

 

 


โœ… SearchInput ์˜†์— ๋ฒ„ํŠผ์„ ํ•˜๋‚˜ ๋ฐฐ์น˜ํ•˜๊ณ , ์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ์‹œ /api/cats/random50์„ ํ˜ธ์ถœํ•˜์—ฌ ํ™”๋ฉด์— ๋ฟŒ๋ฆฌ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฒ„ํŠผ์˜ ์ด๋ฆ„์€ ๋งˆ์Œ๋Œ€๋กœ ์ •ํ•ฉ๋‹ˆ๋‹ค.

 

  • api.js
import { RESULT_STATUS, API_ENDPOINT } from './config.js'

const fetchData = async url => {
  try {
    const result = (await fetch(url))
    if(result.status === 200) return result.json()
    else throw new Error(RESULT_STATUS[result.status])
  } catch(e) {
    alert(e)
  }
}

export const api = {

  ...
  
  fetchRandomCats: async () => {
    const result = fetchData(`${API_ENDPOINT}/api/cats/random50`)
    return result
  },
  
  ...
  
}

 

  • App.js
...

this.searchInput = new SearchInput({
  $target,
  
  ...
  
  onRandomSearch: async () => {
    const data = (await api.fetchRandomCats()).data
    this.setState(data)
  }
});

...

 

  • SearchInput.js
class SearchInput {
  constructor({ $target, onSearch, onRandomSearch }) {
    const $wrapper = document.createElement("section")
    $wrapper.className = "input-wrapper"
    
    ...

    const $randomBtn = document.createElement("button")
    this.$randomBtn = $randomBtn
    this.$randomBtn.className = "RandomButton"
    this.$randomBtn.textContent = "๋žœ๋ค๊ณ ์–‘์ด|"
    $wrapper.appendChild(this.$randomBtn)

    this.$randomBtn.addEventListener("click", () => {
      onRandomSearch()
    })

    ...
  }

  ...
  
}

export default SearchInput

 

api ํ˜ธ์ถœ ๊ด€๋ จ ๋ฌธ์ œ๋ฅผ ํ’€์–ด๋ณด๋ฉด์„œ ์•Œ๊ฒŒ๋œ ๊ฑด App.js์—์„œ api๋ฅผ ์š”์ฒญํ•˜๊ณ  ์‘๋‹ต ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋“ค์„ ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ์ „๋‹ฌํ•ด์ฃผ๋Š” ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ฝ”๋“œ๊ฐ€ ์ง๊ด€์ ์ด๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

 

 

 


โœ… ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐ ์•„์ดํ…œ์— ๋งˆ์šฐ์Šค ์˜ค๋ฒ„์‹œ ๊ณ ์–‘์ด ์ด๋ฆ„์„ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • SearchResult.js
...

this.$searchResult.querySelectorAll(".item").forEach(($item, index) => {

  ...
  
  $item.addEventListener("mouseover", () => {
    const $catName = $item.querySelector(".catName")
      $catName.style.display = "block"
  })

  $item.addEventListener("mouseout", () => {
    const $catName = $item.querySelector(".catName")
    $catName.style.display = "none"
  })
});

 

์ด๊ฒƒ๋„ ํ•ด์„ค ๊ฐ•์˜์—์„œ ๋ชป ๋ณธ ๋ฌธ์ œ

๋งˆ์šฐ์Šค ์˜ค๋ฒ„, ๋งˆ์šฐ์Šค ์•„์›ƒ ์ด๋ฒคํŠธ๋กœ ๊ณ ์–‘์ด ์ด๋ฆ„์„ display block -> none์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

๊ณ ์–‘์ด ์ด๋ฆ„์„ ์–ด๋–ป๊ฒŒ ๋„์›Œ์•ผ ํ• ์ง€ UI์ƒ ๊ณ ๋ฏผ์ด ๋งŽ์€ ๋ฌธ์ œ์˜€๋‹ค.

์‹œ๊ฐ„์ด ๋ถ€์กฑํ•ด์„œ ์™„์„ฑ๋„ ์žˆ๊ฒŒ ๊ตฌํ˜„์€ ๋ชปํ—€๋‹ค.

  • style.css
.SearchResult .catName {
  display: none;
  position: relative;
  top: -100px;
  z-index: 1;
  background-color: lightgray;
}

 

position: relative๋ฅผ ์ด์šฉํ•ด์„œ ์ด๋ฏธ์ง€ ์œ„์— ์˜ฌ๋ ค๋ณด์•˜๋Š”๋ฐ ๋งŒ์กฑ์Šค๋Ÿฝ์ง„ ์•Š๋‹ค.

์ข€ ๋” ๊ณ ๋ฏผํ•ด์„œ ๋‹ค์‹œ ๊ตฌํ˜„ํ•ด๋ด์•ผ๊ฒ ๋‹ค.

 

 

 

์ฝ”๋“œ ๊ตฌ์กฐ ๊ด€๋ จ

โœ… ES6 module ํ˜•ํƒœ๋กœ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

 

  • index.html
<script type="module" src="src/main.js"></script>

 

 


โœ… API fetch ์ฝ”๋“œ๋ฅผ async, await๋ฌธ์„ ์ด์šฉํ•˜์—ฌ ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”.
ํ•ด๋‹น ์ฝ”๋“œ๋“ค์€ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด์„œ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜์–ด์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • api.js
import { RESULT_STATUS, API_ENDPOINT } from './config.js'

const fetchData = async url => {
  try {
    const result = (await fetch(url))
    if(result.status === 200) return result.json()
    else throw new Error(RESULT_STATUS[result.status])
  } catch(e) {
    alert(e)
  }
}

export const api = {
  fetchCats: async keyword => {
    const result = fetchData(`${API_ENDPOINT}/api/cats/search?q=${keyword}`)
    return result
  },
  fetchCatInfo: async id => {
    const result = fetchData(`${API_ENDPOINT}/api/cats/${id}`)
    return result
  },
  fetchRandomCats: async () => {
    const result = fetchData(`${API_ENDPOINT}/api/cats/random50`)
    return result
  }
}

 

async, await๋ฌธ์„ ์ด์šฉํ•ด ์ˆ˜์ •ํ•œ ์ฝ”๋“œ

 

  • config.js
export const API_ENDPOINT =
  "https://q9d70f82kd.execute-api.ap-northeast-2.amazonaws.com/dev";

export const RESULT_STATUS = {
  500: 'Server Error',
  400: 'Bad Request',
  401: 'Unauthorized',
  404: 'page not found'
}

 

์ƒ์ˆ˜๋Š” config.js ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ

HTTP ์ƒํƒœ์ฝ”๋“œ๋ณ„๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ถ„๋ฆฌํ•ด ๋ณด์•˜๋‹ค.

 

 

 

 

๋งˆ๋ฌด๋ฆฌ


๋ฏธ์ฒ˜ ๋‹ค ํ’€์ง€ ๋ชปํ•œ ๋ฌธ์ œ๋“ค๋„ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ณ ๋ฏผํ•ด๋ณด๋ฉฐ ํ•ด๊ฒฐํ•ด๋ด์•ผ๊ฒ ๋‹ค.

์ผ๋‹จ ๊ฐ•์˜๋ถ€ํ„ฐ... ๋‹ค์‹œ ์—ด์‹ฌํžˆ ๋“ค์–ด์•ผ์ง€