๐Ÿ”ฎ ์†Œ๋งˆ๋ฒ• ํ”„๋กœ์ ํŠธ -4 (crawling)

๋ฐฐ๊ฒฝํ™”๋ฉด์„ ์ฐพ์„ ๋•Œ, alpacoders๋ผ๋Š” ๋Œ€ํ˜• Wallpaper ์‚ฌ์ดํŠธ๋ฅผ ์ž์ฃผ ์ด์šฉํ•˜๋Š” ํŽธ์ธ๋ฐ

ํ‚ค์›Œ๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ, 30๊ฐœ์”ฉ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ๋‹ค์ŒํŽ˜์ด์ง€๋ฅผ ํด๋ฆญํ•ด์„œ ๋„˜์–ด๊ฐ€์•ผ์ง€๋งŒ ๋‹ค์Œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ์— ๋ถˆํŽธํ•จ์„ ๋Š๊ปด ํ‚ค์›Œ๋“œ๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ ํด๋ฆญํ•˜๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€ ์—†์ด ํ•œ๋ˆˆ์— ์ด๋ฏธ์ง€๋“ค์„ ๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์— ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ธฐํšํ•ด๋ณด์•˜๋‹ค.

์ „ํ†ต์ ์ธ web๊ฒŒ์‹œํŒ -> SPAํ˜•ํƒœ

์ด๋ฒˆ์—๋Š” ๋ฆฌ์•กํŠธ functial component + hooks ๊ฐ€ ์•„๋‹ˆ๋ผ class component๋ฅผ ์ด์šฉํ•˜์˜€๋‹ค.


#. Project Map


์ œ์ž‘๋…ธํŠธ ํ•œ๋ˆˆ์—๋ณด๊ธฐ[์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ]

1. ๋ ˆ์ด์•„์›ƒ

1-1. ๋ฉ”์ธํ™”๋ฉด (๊ฒ€์ƒ‰์ „)

crawling1

  1. page ๋ทฐ : ์šฐ์ธก ์ƒ๋‹จ์— page ๋ทฐ๋ฅผ ๋งŒ๋“ค์–ด์„œ ํ•ด๋‹น ํ‚ค์›Œ๋“œ์— ๋Œ€ํ•œ ํ˜„์žฌ ์ด๋ฏธ์ง€ ์ˆ˜ / ์ „์ฒด ์ด๋ฏธ์ง€ ์ˆ˜ ๋ฅผ ๋ณด์ด๋„๋ก ํ–ˆ๋‹ค.
  2. ๊ฒ€์ƒ‰ ์ฐฝ : ์ค‘์•™์— ๊ฒ€์ƒ‰ ์ฐฝ์„ ๋‘์—ˆ๋‹ค.
  3. ์„ค๋ช… : ๊ฒ€์ƒ‰ ์ฐฝ ์•„๋ž˜์—๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์ ์—ˆ๋‹ค.
  4. ์ด๋ฏธ์ง€ ๋ณด๊ธฐ๋ฐฉ์‹ ๋ฒ„ํŠผ : ์„ค๋ช… ๋ฐ‘์— ์ด๋ฏธ์ง€ ๋ณด๊ธฐ ๋ฐฉ์‹์— ๋Œ€ํ•œ ๋ฒ„ํŠผ์„ ๋‘์—ˆ๋‹ค.

1-2. ์„œ์นญํ™”๋ฉด (๊ฒ€์ƒ‰ํ›„)

keyword : captain

crawling2

  1. z-index: ํ•ด๋‹น ํ™”๋ฉด์„ ๋ฉ”์ธํ™”๋ฉด์˜ z-index๋ณด๋‹ค ๋‚ฎ๊ฒŒ ํ•˜์—ฌ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด์„œ ๋‹ค์‹œ ๊ฒ€์ƒ‰์„ ํ•˜๊ฑฐ๋‚˜ ๋ฒ„ํŠผ๋“ฑ์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค.
  2. image Link : ์ด๋ฏธ์ง€๋ฅผ ํฌ๋กค๋ง ํ•  ๋•Œ, ํ•ด๋‹น url์„ ๊ฐ™์ด ํฌ๋กค๋งํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ ๊ณ ํ™”์งˆ ์ด๋ฏธ์ง€๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋งํฌ๋กœ ์ด๋™๋˜๊ฒŒ ํ•˜์˜€๋‹ค.

1-3. ์ด๋ฏธ์ง€ ๋ณด๊ธฐ๋ฐฉ์‹ ๋ฒ„ํŠผ

๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด state์˜ gridmode๋ฅผ ๋ณ€๊ฒฝํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

<div
  onClick={() => {
    this.setState({
      gridmode: 10,
    })
  }}
>
  10๊ฐœ์”ฉ๋ณด๊ธฐ
</div>

๊ทธ๋ฆฌ๊ณ  state์˜ gridmode ์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€๋ฅผ ๋ ˆ์ด์•„์›ƒ์„ ๋ณ€๊ฒฝํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

{
  this.state.gridmode === 10 &&
    this.state.result_arr.map((item, i) => (
      <a
        key={i}
        href={'https://wall.alphacoders.com/' + item.link}
        target="_blank"
      >
        <img style={{ width: '10%' }} src={item.img}></img>
      </a>
    ))
}

2. ์ƒํƒœ๊ด€๋ฆฌ

class Layout extends Component {
  state = {
    searchingName: '',
    page: 1,
    imageNumber: 0,
    imageMaxNumber: 0,
    gridmode: 4,
    result_arr: []
  }

2-1. ๋ฆฌ์•กํŠธ state

๊ธฐ์กด ํ”„๋กœ์ ํŠธ์˜ ํ•˜์œ„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ๋“ค์€ functional component + hooks๋กœ ๋งŒ๋“ค์—ˆ์—ˆ๋Š”๋ฐ, ์ด๋ฒˆ ํฌ๋กค๋ง ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ class component + state ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค.

์ƒํƒœ๋ณ€์ˆ˜๋Š”

  1. searchingName : ๊ฒ€์ƒ‰์–ด
  1. page : ํ•ด๋‹น ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ํฌ๋กค๋ง ํŽ˜์ด์ง€ ์ธ๋ฑ์Šค
  1. imageNumber : ํ•ด๋‹น ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ด๋ฏธ์ง€ ์ˆ˜
  1. imageMaxNumber : ํ•ด๋‹น ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ „์ฒด ์ด๋ฏธ์ง€ ์ˆ˜
  1. gridmode : ์ด๋ฏธ์ง€ ๋ณด๊ธฐ๋ฐฉ์‹
  1. result_arr : ์ด๋ฏธ์ง€ ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐˆ ๋ฐฐ์—ด

3. ํฌ๋กค๋ง

3-1. fetch (AJAX)

ajax ๋ฐฉ์‹์€ ๋‚ด์žฅ API fetch ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

const url = 'url + searchingName info + page info'
fetch(url).then(res => {
  return res.text()
})

url ์— ๋Œ€ํ•œ html์„ text() ๋ฉ”์†Œ๋“œ๋กœ ๊ฐ€์ ธ์˜ค๊ฒŒ๋” ํ•จ.

3-2. proxy (CORS)

์ฃผ์†Œ๊ฐ€ ๋‹ค๋ฅธ ์„œ๋ฒ„์˜ ์š”์ฒญ ๋•Œ๋ฌธ์— CORS ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ ๋‚ด๊ฐ€ ์„œ๋ฒ„๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†๋Š” ์ž…์žฅ์ด์–ด์„œ

https://cors-anywhere.herokuapp.com proxy API๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

๐Ÿฅ ํ˜น์‹œ๋‚˜ ํ•ด์„œ nodeJS์—์„œ ํฌ๋กค๋ง์ฝ”๋“œ๋ฅผ ๋Œ๋ ค๋ดค๋Š”๋ฐ CORS์ด์Šˆ๊ฐ€ ์—†์—ˆ๋‹ค. ๋‹ค์‹œ ๊ณต๋ถ€ํ•  ๊ฒƒ!

3-3. Cheerio (Parsing)

๋ถˆ๋Ÿฌ์˜จ html ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•˜๊ธฐ ์œ„ํ•ด cheerio ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

npm i cheerio
const cheerio = require('cheerio')
 fetch(url)
      .then(res => {
        return res.text()
      })
      .then(text => {
        const $ = cheerio.load(text)
        let json = [],
          id,
          link,
          img
        const maxNum = Number(
          $('#page_container > h1')
            .text()
            .split(' ')[8]
        )
        // โญ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ
        if (window.innerWidth < 1070) {
          $('#page_container > div:nth-child(6) > div.thumb-container').each(function(i, elem) {
            id = i
            link = $(this)
              .find('div.thumb-container > a')
              .attr('href')
            img = $(this)
              .find('div.thumb-container > a.wallpaper-thumb > img')
              .attr('data-src')
            json.push({ id: id, link: link, img: img })
          })
        } else {
          $('#page_container > div:nth-child(6) > div.thumb-container-big').each(function(i, elem) {
            id = i
            link = $(this)
              .find('div.thumb-container > div.boxgrid > a')
              .attr('href')
            img = $(this)
              .find('div.thumb-container > div.boxgrid > a > img')
              .attr('data-src')
            json.push({ id: id, link: link, img: img })
          })
        }
        // โญ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ
        if (this.state.imageMaxNumber >= this.state.imageNumber) {
          this.setState({
            result_arr: this.state.result_arr.concat(json),
            imageMaxNumber: maxNum,
            imageNumber: this.state.imageNumber + 30
          })
        }
      })
      .catch(error => console.log(error))
  }

โญ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ

๋˜ํ•œ ํฌ๋กค๋ง์„ ํ•˜๋ ค๋ฉด ํ•ด๋‹น ์›น์‚ฌ์ดํŠธ์˜ css ์„ ํƒ์ž์— ์ ‘๊ทผ์„ ํ•ด์•ผํ•˜๋Š”๋ฐ ํ•ด๋‹น์‚ฌ์ดํŠธ๋Š” ๋””๋ฐ”์ด์Šค์— ๋”ฐ๋ผ ์„ ํƒ์ž๊ฐ€ ๋‹ฌ๋ž๋‹ค.

์ฆ‰, ๋ชจ๋ฐ”์ผ๊ณผ ๋ฐ์Šคํฌํƒ‘ css ์„ ํƒ์ž๊ฐ€ ๋‹ฌ๋ž๋‹ค.

๊ทธ๋ž˜์„œ innerWidth๊ฐ€ 1070๋ณด๋‹ค ํด ๊ฒฝ์šฐ ๋ฐ์Šคํฌํƒ‘์œผ๋กœ ์ธ์‹ํ•˜๊ฒŒ๋”, 1070 ์ดํ•˜์ผ ๊ฒฝ์šฐ๋Š” ๋ชจ๋ฐ”์ผ๋กœ ์ธ์‹ํ•˜๊ฒŒ๋” ์กฐ๊ฑด๋ฌธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์—ˆ๋‹ค.

โ“ innerWidth ๋ง๊ณ  ํ˜„์žฌ ํ™˜๊ฒฝ(๋ฐ์Šคํฌํƒ‘, ๋ชจ๋ฐ”์ผ)์„ ์ธ์‹ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณผ ๊ฒƒ.

โญ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ

์Šคํฌ๋กค ์ด๋ฒคํŠธ๋กœ ์ธํ•œ HTTP ์š”์ฒญ,์‘๋‹ต ๋ถ€๋ถ„์ด ์š”์ฒญ,์‘๋‹ต,์š”์ฒญ,์‘๋‹ต ์ˆœ์ด ์•„๋‹Œ ์š”์ฒญ,์š”์ฒญ,์‘๋‹ต,์‘๋‹ต ์ˆœ์˜ ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ธํ•ด ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์— ๊ฐ”์„ ๋•Œ ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์š”์ฒญ์„ ์—ฌ๋Ÿฌ๋ฒˆ ์‘๋‹ตํ•ด์„œ ์ค‘๋ณต๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ์ค‘๊ฐ„ ํŽ˜์ด์ง€์ผ ๊ฒฝ์šฐ, ์—ฌ๋Ÿฌ ์š”์ฒญ์„ ๋ฐ›์•„๋„ ์ƒ๊ด€ ์—†์œผ๋‚˜ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ค‘๋ณต์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ์˜ค๋ฅ˜๋ฅผ ๋‚ณ๋Š”๋‹ค.

๊ทธ๋ž˜์„œ ์š”์ฒญํ•˜๋Š” ๋ถ€๋ถ„์ด ์•„๋‹Œ ์‘๋‹ต ๋ถ€๋ถ„์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๋Š” ๊ณผ์ •์—์„œ ์กฐ๊ฑด๋ฌธ์„ ๋„ฃ์–ด ์ด๋ฅผ ํ•ด๊ฒฐํ•˜์˜€๋‹ค.

4. ๋ฌดํ•œ์Šคํฌ๋กค

  componentDidMount() {
    window.addEventListener(
      'scroll',
      _.debounce(() => {
        if (
          window.scrollY + document.documentElement.clientHeight >
            document.documentElement.scrollHeight - 260 &&
          this.state.imageMaxNumber >= this.state.imageNumber
        ) {
          this.setState(
            {
              page: this.state.page + 1
            },
            () => this.crawling()
          )
        }
      }, 1000)
    )
  }

4-1. scroll event

์Šคํฌ๋กค ์ด๋ฒคํŠธ๋Š” componentDidMount() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์—๋Š” ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ์กฐ๊ฑด๋ฌธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์—ˆ๋‹ค.

4-2. _.debounce (lodash)

๊ทธ๋ฆฌ๊ณ  ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ถ€๋ถ„์— lodash ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ _.debounce ๋ฉ”์†Œ๋“œ๋ฅผ ๋„ฃ์–ด์ฃผ์–ด ์ตœ์ ํ™” ์ž‘์—…๋„ ํ•ด์ฃผ์—ˆ๋‹ค.

npm i lodash
const _ = require('lodash')

5. ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ (callback)

submitHandler = e => {
  e.preventDefault()
  this.setState(
    {
      result_arr: [],
      imageNumber: 0,
      imageMaxNumber: 0,
      page: 1,
    },
    () => {
      this.crawling()
    }
  )
}

ํฌ๋กค๋ง ํ•จ์ˆ˜๋ฅผ state๊ฐ€ ๋ฐ”๊พผ ํ›„์— ์‹คํ–‰๋˜๋„๋ก callback ์„ ์ด์šฉํ–ˆ๋‹ค.

๋ฌดํ•œ์Šคํฌ๋กค ๋ถ€๋ถ„๋„ ๋งˆ์ฐฌ๊ฐ€์ง€ callback์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ ๊ฐ€์žฅ ๋ง‰ํžˆ๊ณ  ์ดํ•ดํ•˜๋ ค๊ณ  ์• ์ผ๋˜ ๋ถ€๋ถ„์ด javascript์˜ ๋น„๋™๊ธฐ๋ฐฉ์‹์ด์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

6. ๊ฐœ์ธ์ ์ธ ํ”ผ๋“œ๋ฐฑ

6-1. ๋ฆฌ๋•์Šค ๊ธฐ๋Šฅ ์—†์Œ

๋ฆฌ๋•์Šค hooks (useDispatch(), useSelector())๋Š” ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—๋งŒ ์“ธ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋งˆ์ง€๋ง‰์— ๊นจ๋‹ฌ์•˜๋‹ค..

hooks

๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋ฆฌ๋“€์„œ๋ฅผ ์žฅ์ฐฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์ตœ๋Œ€ํ•œ ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ž๋Š” ์ƒ๊ฐ์„ ํ•˜๋ฉฐ, ๋ฆฌ๋•์Šค ๊ธฐ๋Šฅ์„ ํฌ๊ธฐํ–ˆ๋‹ค.

6-2. class ํ˜• ์ปดํฌ๋„ŒํŠธ

๊ทธ๋Ÿผ ๋” ์›๋ก ์ ์œผ๋กœ ํ•จ์ˆ˜ํ˜•์„ ํ•˜์ง€ ๋ชปํ•œ ์ด์œ ๋Š”?

์ฒ˜์Œ์—๋Š” ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ + hooks๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ์œผ๋‚˜, hooks ๋กœ ๋งŒ๋“  ์ƒํƒœ๋“ค์ด ๊ณ„์† ๋ฆฌ์…‹๋˜๊ณ  ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  useState hooks ์˜ setState ๋ถ€๋ถ„์—์„œ ์ฝœ๋ฐฑํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ค„ ๋ชฐ๋ผ ๊ทธ๋ƒฅ class ๋กœ ๋‹ค์‹œ ์ž‘์„ฑํ•˜์˜€๋‹ค.

hooks์˜ ์›๋ฆฌ๋ฅผ ์ข€๋” ๊นŠ๊ฒŒ ํ•™์Šตํ•  ํ•„์š”๋ฅผ ๋Š๊ผˆ๋‹ค.

6-3. ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜

์Šคํฌ๋กค์„ ๋‚ด๋ฆฌ๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธด ํ•˜๋Š”๋ฐ ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋„ฃ์ง€ ์•Š์•˜๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”์ง€ ๋งˆ๋Š”์ง€ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ux์ ์ธ ๋ฉด์—์„œ ๋ณ„๋ฃจ๋‹ค..

๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ๊ณต๋ถ€ํ•˜๊ธฐ!

7. ์ด์Šคํ„ฐ์—๊ทธ

์Šคํฌ๋กค์ด ๋‹ต๋‹ตํ•˜๋‹ค๋ฉด 10๊ฐœ์”ฉ ๋ณด๊ธฐ ๋ฒ„ํŠผ์„ ๋งˆ๊ตฌ ํด๋ฆญํ•˜์ž. ํฌ๋กค๋ง ์ฝ”๋“œ๋ฅผ ์—ฌ๊ธฐ์—๋„ ๋„ฃ์–ด๋‘์–ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ๋นจ๋ฆฌ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.


Written by@taenyKim
๋ฐฐ์šฐ๋ฉฐ ์„ฑ์žฅํ•˜๊ณ  ๊ธฐ๋กํ•˜๊ธฐ #FE #UI #๊ฐœ๋ฐœ #life

GitHubFacebook