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 ์ํ์ฝ๋๋ณ๋ก ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ถ๋ฆฌํด ๋ณด์๋ค.
๋ง๋ฌด๋ฆฌ
๋ฏธ์ฒ ๋ค ํ์ง ๋ชปํ ๋ฌธ์ ๋ค๋ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ณ ๋ฏผํด๋ณด๋ฉฐ ํด๊ฒฐํด๋ด์ผ๊ฒ ๋ค.
์ผ๋จ ๊ฐ์๋ถํฐ... ๋ค์ ์ด์ฌํ ๋ค์ด์ผ์ง
