너무 편리해서 놀랍도다!

 

아리땁군요

 


 이미지를 여러 가지로 만지고 놀던 중에 이미지 편집 라이브러리가 필요하게 되었습니다. 직접 만들어 볼 생각은  하지도 않았습니다. 그리고 찾은  게  CropperJS인데요. 너무 간편하고 편리해서 사용법을 공유하고 싶습니다.

 

 문서보고 적용하는 것이 어렵지는 않지만 한 가지 실수를 하는 바람에 2시간 정도를 꼬박 검색하는데 날린 거 같아요. 여러분은 실수하지 마시라고 팁도 넣어드립니다.

 

 적용하는데 있어서 헷갈리지 않도록 기본 코드만 넣어보았어요. 적용이 완료되면 아래처럼 보입니다.

 

저도 아리따운 분을 모셔왔어요!

 바로 코드로 들어갑니다!

 

test.vue

<template>
<div>
  <div class="w-full p-4 text-center bg-green-200 rounded-t-md">
    <div class="flex justify-between pb-2">
        <h3 class="text-lg font-medium text-gray-900">
            CropperJS!
        </h3>
        <!-- "X" image : heroicons -->
        <svg class="h-4 w-4 text-gray-700 cursor-pointer sm:mr-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
        </svg>
    </div>
    <div class="flex">
        <div class="w-3/4 mr-4">
            <img ref="image" :src="src" class="hidden" alt="image-edit">
        </div>
        <div class="w-1/4">
            <div class="pb-2">Preview</div>
            <div class="flex items-center justify-center h-32 bg-yellow-100">
                <div class="preview overflow-hidden w-full h-32 text-center bg-gray-200"></div>
            </div>
        </div>
    </div>
  </div>
  <div class="bg-teal-100 p-4 text-right rounded-b-md">
    <button type="button" class="w-32 p-2 m-2 border rounded-md shadow-sm bg-red-600">
      Save
    </button>
    <button type="button" class="w-32 p-2 m-2 border rounded-md shadow-sm bg-white">
      Cancel
    </button>
  </div>
</div>
</template>

 먼저 HTML 구성에서 15번째 줄에 img 태그가 있는데 hidden(display:none;)으로 처리해서 보이지 않게 해 주세요. 그리고 20번째 줄에 있는 preview 클래스가 중요합니다. 바로 그 곳에 CropperJS가 미리보기를 넣어줘요. 나머지는 입맛에 맞게 바꾸셔도 됩니다.

<script>
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'

export default {
  name: 'Home',
  mounted() {
    this.image = this.$refs.image
    this.cropper = new Cropper(this.image, {
        preview: '.preview',
    })
  },
  computed: {
  },
  data: ()=>({
    src: '/images/girl.jpg',
    image: {},
    cropper: {}
  }),
  methods: {
  }
}
</script>

 제가 2시간동안 헤맨이유가 여기에 있습니다. 3번째 줄에 보면 css를 꼭 import 해주세요. 가이드 문서에서도 저렇게 하라고 알려주는데 그걸 놓치고 아까운 시간을 날려버렸습니다. 만약 추가를 안 해주면 편집 컨테이너가 뜨는 게 아니라 이미지만 떠버려서 편집이 불가능해집니다.

 

 그럼 CropperJS에게 이미지의 위치를 Vue의 경우 refs속성(javascript의 경우 getElementById도 가능)을 이용해서 위치를 알려줍니다. 그리고 생성된 인스턴스를 data cropper에 저장하구요. 그리고 option으로 preview의 위치를 preview class 선택자를 이용하여 알려주면 됩니다.

<style scoped>
img {
    display: block;
    max-width: 100%; /* This rule is very important, please do not ignore this! */
}
</style>

 이제 마지막입니다. 위에 script에서 css를 import해주고 나서도 한 가지 해줘야 하는데 css를 추가해줘야 합니다. 이걸 추가 안 하게 되면 컨테이너에 이미지가 자기 사이즈대로 보여서 작게 보이든, 크게 보이든 이쁘게 보이지 않아요.

 

 자, 이제 끝났습니다. 나머지 여러가지 기능들을 추가할 수 있는데 위에 만들어 놓은 cropper 인스턴스를 이용하면 쉽게 메소드를 만들 수 있습니다. 예를 들면 이런 식입니다.

this.cropper.reset() // 원본으로 돌아가기
this.cropper.zoom(0.2) // 확대
this.cropper.zoom(-0.2) // 축소

 

 참 쉽죠? 기능들이 다양하고 많으니 라이브러리에 대한 설명은 아래 링크를 만들어 놓을게요!


라이브러리 링크!

fengyuanchen.github.io/cropperjs/

 

Cropper.js

 

fengyuanchen.github.io


성공하면 쉬워 보이는구나

 

이미지 업로드 마지막 포스팅!

여러개의 이미지를 한번에 등록해보자


① 이미지 업로드하기 #1 - VueJS + NodeJS

② 이미지 업로드하기 #2 - Amazon S3 setting + upload

③ 이미지 업로드하기 #3 - Drag and Drop

④ 이미지 업로드하기 #4 - Multi image management

 

 

 

이미지 다중 업로드 & 삭제

 이번에는 이전 코드를 조금 수정해서 이미지를 여러 개를 한 번에 업로드가 가능하도록 만들고, 등록된 이미지를 삭제하는 것까지 해보겠습니다.

 

 미리 말씀드리지만 이미지 삭제의 경우 FRONT END에서 이미지의 URL만 삭제되게 했습니다. Amazon S3에는 이미지가 저장되어 있는 상태니까 S3에서 삭제 규칙을 생성해주시거나 직접 삭제해 주어야 합니다.

 

자 바로 FRONT END로 코드로 가보겠습니다.

 

▶ FRONTEND

src/views/upload.vue

<template>
<div class="container mx-auto text-sm">
  <div class="grid grid-cols-4 gap-2">
    <div v-for="num in 4" :key="num" class="flex justify-center text-center">
      <div class="w-32 h-32 mb-4 border-2 border-dotted border-blue-500">
        <div v-if="images[num]" class="w-full h-full flex items-center justify-center"
             @mouseenter="showImageMenu(num, true)"
             @mouseleave="showImageMenu(num, false)">
          <img :src="images[num]" :alt="`image${num}`">
          <div v-if="show[num]" class="absolute">
            <div class="h-8 w-8 flex justify-center items-center rounded-full bg-blue-500"
                 @click="removeImage(num)">
              <svg class="w-6 h-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
              </svg>
            </div>
          </div>
        </div>
        <div v-else class="w-full h-full flex items-center justify-center cursor-pointer hover:bg-pink-100"
             @click="clickInputTag(num)"
             @drop.prevent="dropInputTag(num, $event)"
             @dragover.prevent>
          <input :ref="`image${num}`"
                 type="file" name="image" accept="image/*" multiple="multiple"
                 class="hidden"
                 @change="uploadImage(num)">
          <svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
        </div>
        <div>
          Image {{ num }}
        </div>
      </div>
    </div>
  </div>
</div>
</template>
<script>
import axios from 'axios'

export default {
  data: ()=>({
    images: [],
    show: []
  }),
  methods: {
    removeImage: function(num) {
      this.images.splice(num, 1, null)
    },
    showImageMenu: function(num, bool) {
      this.$set(this.show, num, bool)
    },
    uploadImage: function(num, file) {
      let images = file || Array.from(this.$refs[`image${num}`][0].files, v => v)
      
      for (let i = 0; i < images.length; i++) {
        let form = new FormData()
        let image = images[i]
        
        form.append('image', image)
		
        if (num+i < 5) {
          axios.post('/upload', form, {
              header: { 'Content-Type': 'multipart/form-data' }
          }).then( ({data}) => {
              this.$set(this.images, num+i, data)
          })
          .catch( err => console.log(err))
        }
      }
    },
    dropInputTag: function(num, event) {
      let file = Array.from(event.dataTransfer.files, v => v)
      this.uploadImage(num, file)
    },
    clickInputTag: function(num) {
      this.$refs[`image${num}`][0].click()
    }
  }
}
</script>

 

1. INPUT TAG를 4개로 변경

 먼저 HTML 코드에서 v-for를 이용해서 4개의 INPUT 태그를 만들어줍니다. 그리고 ref에도 num을 붙여서 중복되지 않도록 체크해줍니다.

 

2. clickInputTag 메소드 변경(INPUT 태그 클릭했을 때)

 INPUT 태그를 클릭했을 때 어떤 태그를 클릭했는지 알 수 있도록 num인자를 추가해줍니다(다른 메소드도 동일해요). 아, 그리고 ref의 숫자가 늘어나면서 this.$refs['images${num}']의 값이 배열이니까 [0]을 붙여주어야 제대로 작동합니다.

 

3. dropInputTag 메소드 변경(INPUT 태그에 이미지를 drop 했을 때)

 이미지를 drop했을 경우 uploadImage 메소드의 인자로 들어가는 file 변수의 경우, 1개일 때 Array.from 끝에 [0]을 붙일 때와 다르게 이것만 지워주면 배열의 형태로 전달이 됩니다. 심플하죠?

 

4. uploadImage 메소드 변경(반복문을 통해 여러 번 동작하도록 함)

 이제 여기에서 Array.from 안에 files뒤에 [0]을 지워주면 images 변수에 배열의 형태로 저장이 됩니다. 그럼 for문을 이용해서 반복적으로 이미지를 S3에 저장하고 URL을 받아오면 됩니다.

 이때 저희는 이미지를 업로드할 수 있는 INPUT 태그를 4개를 만들었고, num의 인자가 1부터 4까지 반복되도록 만들었으니, 이미지를 여러 개 업로드하더라도 5가 초과되는 이미지의 경우 upload 되지 않도록 if문을 작성해줍니다.

 

5. 이미지 업로드 성공 이후에 마우스 오버 시 삭제 가능하도록 변경

 @mouseenter, @mouseleave 속성을 이용해서 showImageMenu 메소드가 실행이 되도록 만들어서 svg가 마우스 오버가 되었을 때 보이도록 만들어줍니다. 그리고 이 svg를 클릭했을 때 removeImage 메소드가 실행되도록해서 이미지의 url을 삭제해줍니다.

 이때 중요한 것은 splice의 세 번째 인자를 빈 문자열 값('')을 설정해서 뒤에 있는 이미지가 앞으로 당겨지지 않도록 해야 완벽해지죠.


 드디어 이미지 업로드 시리즈를 끝냈습니다. 뿌듯하군요.

 

혹시 보면서 불편한 부분, 이해가 안 되는 곳 등 의견이 있으면 언제든 알려주세요! 최대한 수정하고 반영하도록 하겠습니다. 그러면서 저도 배울 수 있거든요!

 

그럼 다음에 다른 주제로 뵙겠습니다. 안녕히!


▶ BACKEND 코드는 이미지 업로드하기 #1 같습니다! 


 


배우는 즐거움을 함께 느꼈으면 좋겠어요!

 

구현하기 전에 왜 그렇게 겁냈을까.. 몇 줄만 추가하면 되는 것을!

드래그 앤 드랍 구현하기


① 이미지 업로드하기 #1 - VueJS + NodeJS

② 이미지 업로드하기 #2 - Amazon S3 setting + upload

이미지 업로드하기 #3 - Drag and Drop

④ 이미지 업로드하기 #4 - Multi image management

 

 

 이전 이미지 업로드하기 #1 에서는 클릭을 통해 이미지 업로드를 했다면 이번에는 드래그 앤 드랍을 통해 이미지 업로드하는 방법에 대해 포스팅하겠습니다.

끌어서 놓기

  ▶ FRONTEND

src/views/upload.vue

<template>
<div class="container mx-auto text-sm">
  <div class="w-32 h-32 border-2 border-dotted border-blue-500">
    <div v-if="images" class="w-full h-full flex items-center">
      <img :src="images" alt="image">
    </div>
    <div v-else class="w-full h-full flex items-center justify-center cursor-pointer hover:bg-pink-100"
          @click="clickInputTag()"
          @drop.prevent="dropInputTag($event)"
          @dragover.prevent>
      <input ref="image" id="input"
             type="file" name="image" accept="image/*" multiple="multiple"
             class="hidden"
             @change="uploadImage()">
      <svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
    </div>
  </div>
</div>
</template>

 먼저 추가된 코드는 input 태그 바로 위에 div 태그에 @drop.prevent와 @dragover.prevent 입니다. 요 두 가지 이벤트만 등록해두면 이미지를 드래그해서 갖다 놓을 수 있게 됩니다. 참 쉽죠? 그리고 drop event가 발생했을 때 dropInputTag 메소드가 실행되면서 인자로 event를 전달해서 업로드할 이미지의 정보를 읽을 수 있게 합니다.

<script>
import axios from 'axios'

export default {
  data: ()=>({
    images: ''
  }),
  methods: {
    uploadImage: function(file) {
      let form = new FormData()
      let image = file || this.$refs['image'].files[0]
      
      form.append('image', image)

      axios.post('/upload', form, {
          header: { 'Content-Type': 'multipart/form-data' }
      }).then( ({data}) => {
        this.images = data
      })
      .catch( err => console.log(err))
    },
    dropInputTag: function(event) {
      let file = Array.from(event.dataTransfer.files, v => v)[0]
      this.uploadImage(file)
    },
    clickInputTag: function() {
      this.$refs['image'].click()
    }
  }
}
</script>

 위에서 drop event가 실행되면 dropInputTag 메소드에서 파일에 대한 정보를 읽습니다.

 

 event.dataTransfer.files에 이미지의 정보가 담겨있는데 이 배열이 "유사 배열"입니다. 유사 배열은 배열이지만 배열 같지 않은 이상한 녀석인데요. 이 유사 배열을 얕게 복사해서 배열처럼 사용할 수 있게 해주는 것이 Array.from 메소드입니다. 유사 배열에 대해 나중에 기회가 되면 더 자세히 포스팅하도록 하겠습니다(설명하려면 저도 공부를 더 해야 하기에...).

 

 어쨌든 이미지에 대한 정보를 file 변수에 넣었으니 그냥 원래 있던 uploadImage 메소드에 인자로 넣어서 전달해주겠습니다. 이전 포스팅에서와는 다르게 원래 uploadImage 메소드에 file이라는 인자를 넣어서, 인자가 전달이 되면 전달된 인자를 image변수에 넣어주고, 인자가 전달이 안되면 클릭해서 실행했을 때와 동일하게 image를 읽어오는 방법으로 구현했습니다.

 

 나머지는 똑같아서 따로 설명할 것도 없네요.


▶ BACKEND 코드는 이미지 업로드하기 #1 와 같습니다! 


 포스팅이 시리즈로 나누어져 있는 것이 의아해하시는 분들이 계실 것 같아서 말씀드립니다. 굳이 제가 코드를 나눠서 업로드하는 이유는 제가 배우는 입장이었을 때 이 방법이 가장 좋았기 때문입니다. 너무 길거나 여러 기능을 포함하는 코드는 보고도 어디가 어떻게 작동하는지 이해가 어렵더라구요.

 

 나중에 이미지 업로드 시리즈의 마지막 포스팅에서 제가 구현하려고 한 모든 기능이 들어간 코드를 포스팅할 예정입니다.

 

 그럼 다음에는 여러 개의 이미지 파일을 선택해서 한 번에 올리는 방법과, 올린 이미지를 삭제하는 방법을 포스팅하도록 하겠습니다.


세상에 고수는 많으니 저는 섬세한 컨셉으로
쓸 때는 몰랐지만 많은 기술이 숨어 있었다!


이미지 업로드하기 #1 - VueJS + NodeJS

이미지 업로드하기 #2 - Amazon S3 setting + upload

이미지 업로드하기 #3 - Drag and Drop

이미지 업로드하기 #4 - Multi image management

 

 토이 프로젝트를 하던 중에 이미지 업로드를 해야 할 일이 생겼습니다. 구현한 모습은 아래와 같아요.

 

 구현 단계는 먼저 한 개의 이미지를 업로드를 시작으로 아마존 저장소인 S3(AWS S3)에 저장하고 URL을 불러와서 썸네일을 보여줄 겁니다. 그리고 이후에 드래그 앤 드롭이 가능하도록 만들고, 여러 개의 이미지를 업로드하고 순서를 바꾸는 등의 잡기술을 구현해 볼 예정이에요.

 

 도움에는 Tailwindcss(css framework), Express(nodejs web framework), 기타 라이브러리가 있습니다.


여러분의 시간은 소중하죠. 바로 시작합니다. 먼저 드래그 앤 드롭은 구현하지 않은 코드입니다.

 

 작동 순서는 이렇습니다.

1. 사각형을 클릭한다.

2. input tag가 열리고 이미지를 선택한다.

3. 선택한 이미지가 업로드된다.

4. 업로드된 이미지의 url을 가져와서 보여준다.

 

▶ FRONTEND

src/views/upload.vue

<template>
<div class="w-32 h-32 border-2 border-dotted border-blue-500">
  <div v-if="images"
       class="w-full h-full flex items-center">
    <img :src="images" alt="image">
  </div>
  <div v-else
       class="w-full h-full flex items-center justify-center cursor-pointer hover:bg-pink-100"
       @click="clickInputTag()">
       <input ref="image" id="input"
              type="file" name="image" accept="image/*" multiple="multiple"
              class="hidden"
              @change="uploadImage()">
       <svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
       </svg>
  </div>
</div>
</template>

 

<script>
import axios from 'axios'

export default {
  data: ()=>({
    images: ''
  }),
  methods: {
    uploadImage: function() {
      let form = new FormData()
      let image = this.$refs['image'].files[0]
      
      form.append('image', image)

      axios.post('/upload', form, {
          header: { 'Content-Type': 'multipart/form-data' }
      }).then( ({data}) => {
        this.images = data
      })
      .catch( err => console.log(err))
    },
    clickInputTag: function() {
      this.$refs['image'].click()
    }
  }
}
</script>

 

 

 설명을 해보자면

1.  그럼 사각형을 클릭해서 clickInputTag 메소드를 실행(click event)합니다.

→ Input tag는 hidden class(display:none과 같음)를 추가해서 안 보이게 만들었기 때문에 vuejs의 refs 속성을 이용해서 DOM에 접근합니다. javascript의 경우 id나 class 선택자를 통해 접근하는 것과 비슷하다고 보면 돼요. Input tag의 ref값이 image이기 때문에 this.$refs['image']로 접근해서 click()을 통해 클릭한 효과를 나타냅니다.

 

2. Input tag가 열리고 이미지를 선택하면 값이 변화된 것을 감지(change event)해서 uploadImage 메소드를 실행합니다.

→ 이미지 선택창이 열리고 이미지를 선택하면 값이 들어온 것을 감지한 후 바로 업로드하기 위해 change이벤트에 uploadImage를 바로 실행하도록 합니다.

 

3. uploadImage 메소드를 통해 이미지를 업로드합니다.

→ 메소드가 실행되면 1번과 같이 refs 속성을 통해 값에 접근하고 files값을 new Form 인스턴스에 append 해줘서 form data를 만듭니다. 이후 post방식으로 서버로 보내주면 됩니다. 서버 쪽 코드는 아래 있어요.

 

4. 업로드된 이미지의 url을 가지고 와서 img tag를 이용하여 나타냅니다.

→ 서버 쪽 코드를 통해 업로드에 성공하면 url을 가져오도록해서 data값인 images에 전달해주고, vue 측에서 images의 값이 변했으니 자동으로 감지해서 이미지를 띄워줍니다.

 

 네, 그렇습니다. 코드가 오히려 간결합니다. 설명이 길죠. 저는 짧은 것도 길게 설명할 수 있는 재주를 가졌어요. (쉽게 설명하는 재주를 가졌다면 강사로 가는 건데...)


 다음은 서버 쪽 코드입니다. nodejs의 web framework인 express를 사용하고 있으니 굳이 쓸 필요 없어서 생략합니다. 이번에 upload에 필요한 route는 upload(post) 하나니까 저것만 보고 갈게요.

 

 그리고 파일 업로드하는데 multer를 middleware로 사용했고, amazon S3에 업로드하는 코드는 따로 빼서 index.js가 너무 길어지지 않게 했어요. 사실 한 파일에 넣어도 상관없습니다.

 

▶ BACKEND

srv/index.js

import express from 'express'
import multer from 'multer'
let upload = multer({ dest: 'public/uploads/' })

import { uploadToStorage } from "./apis/uploadToStorage"

export default (app, http) => {
  
  // ... 생략 ...
  
  app.post('/upload', upload.single('image'), (req, res) => {
    uploadToStorage(req.file).then( (response) => {
      res.send(response.Location)
    }).catch(err=>console.log(err))
  })
}

srv/apis/uploadToStorage.js

import AWS from 'aws-sdk'
import fs from 'fs'

AWS.config.region = 'ap-northeast-2'

let s3 = new AWS.S3()

async function uploadToStorage(file) {
    let path = file.path
    let name = file.originalname
    let type = file.mimetype
    let image = fs.createReadStream(path)
    
    let parameters = {
        Bucket: 'my-bucket-name',
        Key: name,
        ACL: 'public-read',
        Body: image,
        ContentType: type,
    }

    return await s3.upload(parameters, function(err) {
        err ? console.log(err) : false
    }).promise()
}

export { uploadToStorage }

 

 자, 또 설명을 해보자면

1. FrontEnd에서 upload 요청이 오면, 요청이 온 파일을 local storage에 업로드합니다.

→ axios를 통해 전달된 form data에는 업로드해야 할 파일의 정보가 들어있습니다. upload 라우트에 middleware로 먼저 local storage에 파일을 업로드해주고, 업로드된 파일의 정보(가장 중요한 경로 포함)를 uploadToStorage에 인자로 보냅니다.

 

2. 업로드된 파일의 정보를 uploadToStorage에 보내서 amazon S3에 업로드합니다.

→ ACL의 경우 public-read를 통해 누구나 접근할 수 있도록 합니다. 그리고 return 값으로 들어가 있는 upload 메소드를 통하면 성공했을 경우 자동적으로 업로드된 이미지(S3에서는 객체라고 부르더군요)에 접근할 수 있는 url을 결과값으로 전달해줍니다.

※ S3를 생성하고 퍼블릭 액세스 차단 설정은 위와 같이 해주세요. 자세한 내용은 다음 포스팅에 하는 걸로! 또 S3를 aws_sdk를 이용해서 제어하기 위해서는 aws_access_key_id, aws_secret_access_key가 필요한데 나중에 따로 포스팅하는 걸로 하고 일단 생활코딩 링크를 남겨둘게요!

(S3에 대한 경험은 나중에 따로 포스팅하겠습니다.)

(그리고 포스팅되었습니다. 링크)

 

생활코딩 링크 ☞ opentutorials.org/course/2717/11768

 

3. 업로드에 성공하면 url 정보를 받아서 결과값으로 보내줍니다.

→ 업로드에 성공할 경우 uploadToStorage의 결과값인 url 정보를 express의 res.send를 통해 FrontEnd로 보내줍니다.


 뭔가 더 자세하게 설명하고 싶지만 중간에 코드 수정하면서 겪었던 오류들은 이미지 업로드의 주제와는 상관없어서 포스팅에서는 빼도록 하겠습니다. 내용이 이해가 안 되는 것이 있다면 언제든 댓글 남겨주세요!

 

 다음 포스팅은 amazon S3 생성과 설정에 관한 내용입니다. 생활코딩 영상이나 다른 사람들이 포스팅한 내용과 비교해서 큰 내용은 바뀐 게 없지만 UI가 살짝 바뀌어서 업데이트 정도의 의미는 있을 것 같아요. 그럼 2편에서 돌아오겠습니다!


코드를 작성하고 설명할게 더 많을 줄 알았다. 코드만 읽는 게 설명을 읽는 것보다 쉬운 건 비밀.
somebody && everybody

 


 저번 포스팅에 리스트 데이터 형식은 배열이 좋다는 말을 했었는데, 오늘 포스팅할 함수도 배열에 사용할 수 있는 함수입니다. 오늘은 두 가지의 함수를 비교하면서 알아보려고 합니다.

 

SOME vs EVERY

 

 이 두 가지의 함수는 배열 안에 값을 검증하는 용도로 자주 쓰이는데 살펴보도록 하겠습니다.


1. some

 배열 안에 있는 값들을 순회하면서 TRUE인 값이 하나라도 있으면 순회를 멈추고 함수는 TRUE를 리턴하고 종료됩니다. some이라는 단어에 어울리게 무엇인가 하나라도 있으면 참이 되는 함수입니다. 만약 참이 안 나오면 마지막 값까지 순회를 하고 끝까지 TRUE가 없으면 함수는 FALSE를 리턴하고 종료됩니다.

let numbers = [ 1, 2, 3, 4, 5 ]


// example 1
let result = numbers.some(function(value) {
    console.log(value)
    return false
})
console.log(`result is ${result}`)

// 결과
// 1
// 2
// 3
// 4
// 5
// result is false


// example 2
let result = numbers.some(function(value) {
    console.log(value)
    if ( value === 3 ) {
      return true
    }
    return false
})
console.log(`result is ${result}`)

// 결과
// 1
// 2
// 3
// result is true

 배열 안에 어떤 값이 하나라도 존재하는지 확인하는 함수로 리턴 타입은 BOOLEAN입니다. 하나라도 있으면 TRUE를 리턴하는 게 간절함이 느껴지는 함수네요.


1. every

 배열에 있는 값 중에 하나라도 FALSE가 있으면 FALSE를 리턴하고 종료됩니다. 모든 값이 TRUE여야만 함수가 TRUE를 리턴합니다. 모든 값이 참이어야만 참을 리턴하는, 모든 값이 조건을 만족하는지 체크하는 용도로 자주 쓰입니다.

let numbers = [ 1, 2, 3, 4, 5 ]


// example 1
let result = numbers.every(function(value) {
    console.log(value)
    return true
})
console.log(`result is ${result}`)

// 결과
// 1
// 2
// 3
// 4
// 5
// result is true


// example 2
let result = numbers.every(function(value) {
    console.log(value)
    if ( value === 3 ) {
      return false
    }
    return true
})
console.log(`result is ${result}`)

// 결과
// 1
// 2
// 3
// result is false

 중간에 만족하지 않는 값이 하나라도 있으면 FALSE를 리턴하고 종료됩니다. every라는 단어에 걸맞게 모든 값의 통일을 중시하네요. All or nothing! 극단적인 함수입니다.


 이번 포스팅에서는 some과 every함수를 다뤄보았습니다. 배열에서 사용할 수 있는, 값을 검증하는데 유용한 함수였습니다. 다음에는 값을 검증하는 것이 아닌 값을 찾는데 필요한 함수를 포스팅할게요!

 

내가 쓰는거 한 번쯤은 정리해보고 싶었다.
내쓸내정(내가 쓸 거 내가 정리한다) 시리즈의 시작

 

쓰려고 하면 헷갈려 이놈들

 


 자바스크립트로 코딩을 하다 보면 항상 배열 또는 문자열을 자르고 붙이는 일이 자주 생깁니다. 특히 배열이나 문자열을 자를 때는 slice 함수와 splice 함수는 항상 헷갈리게 되죠. paramete에 넣는 게 시작이 먼저인지 끝이 먼저인지, 반환 값이 배열인지 원래 배열을 수정하는 건지 매번 찾아보다 지쳐서 그냥 정리하게 되었습니다.

 

 남들도 많이 포스팅해서 차고 넘치지만 내가 쓸 거 내가 정리한다 시리즈 1탄.

 

slice() vs splice()


slice

▷ array.slice([begin[, end]])

  ☆ 반환 값 : 새로운 배열, 기존의 배열의 값을 수정하지 않는다.

 

  ① begin : 어디서부터 자를지 시작점을 정한다. 인덱스 값이다.

let arr = [1, 2, 3, 4, 5]

      - 0일 경우 : 시작점부터 잘라 가져온다.

let result = arr.slice(0)	//result : [1, 2, 3, 4, 5]

      - 양수일 경우 : 인덱스(start값의 자리에 있는 배열 값)를 포함하고 값을 가져온다.

let result = arr.slice(2)	//result : [3, 4, 5]

      - 음수일 경우 : 배열의 마지막 값이 (-1)이다. (-2) 일 경우 배열의 마지막 두 개의 값을 잘라서 가져온다.

let result = arr.slice(-2)	//result : [4, 5]

      - undefined인 경우 : 0일 경우와 같다.

let result = arr.slice(undefined)	//result : [1, 2, 3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : 빈 배열을 반환한다.

let result = arr.slice(9	//result : []

 

  ② end : 어디까지 자를지 정한다. 인덱스 값이다. 생략이 가능하다. 생략할 경우 시작점부터 배열의 마지막까지 가져온다.

      - 0일 경우 : 시작점이 어디든 빈 배열을 반환한다.

let result1 = arr.slice(1, 0)		//result1 : []
let result2 = arr.slice(-1, 0)		//result2 : []
let result3 = arr.slice(0, 0)		//result3 : []
let result4 = arr.slice(10, 0)		//result4 : []
let result5 = arr.slice(undefined, 0)	//result5 : []

      - 양수일 경우 : begin 인덱스 값은 포함, end 인덱스(end값의 자리에 있는 배열 값) 값은 제외하고 가져온다.

let result1 = arr.slice(0, 2)		//result1 : [1, 2]
let result2 = arr.slice(undefined, 2)	//result2 : [1, 2]
let result3 = arr.slice(-3, 5)		//result3 : [3, 4, 5]
let result4 = arr.slice(-3, 1)		//result4 : []
//result4의 경우 begin 인덱스의 위치보다 end 인덱스의 위치가 더 앞(왼쪽, 작은값)이라서 빈 배열 반환

      - 음수일 경우 : (-2)일 경우 배열의 마지막 두 개의 값을 잘라서 버리고 나머지만 가져온다.

let result1 = arr.slice(0, -2)		//result1 : [1, 2, 3]
let result2 = arr.slice(undefined, -2)	//result2 : [1, 2, 3]
let result3 = arr.slice(-3, -5)		//result3 : []
//result3의 경우 begin 인덱스의 위치보다 end 인덱스의 위치가 더 앞(왼쪽, 작은값)이라서 빈 배열 반환
let result4 = arr.slice(-3, 1)		//result4 : [3, 4]

      - undefined인 경우 : 배열의 시작점부터 마지막 값까지 포함해서 가져온다.

let result1 = arr.slice(0, undefined)		//result1 : [1, 2, 3, 4, 5]
let result2 = arr.slice(undefined, undefined)	//result2 : [1, 2, 3, 4, 5]
let result3 = arr.slice(-3, undefined)		//result3 : [3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : undefined일 경우와 같다.

let result1 = arr.slice(0, 7)		//result1 : [1, 2, 3, 4, 5]
let result2 = arr.slice(undefined, 7)	//result2 : [1, 2, 3, 4, 5]
let result3 = arr.slice(-3, 7)		//result3 : [3, 4, 5]

 splice

▷ array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

  ☆ 반환 값 : 제거한 요소 배열. 기존 배열의 값을 수정한다. 조심하자.

  ☆ 값을 가져올 경우 기존 배열에서 가져온 값이 삭제가 된다.

  ☆ 값을 추가할 경우 기존 배열에 값이 추가된다.

 

  ① start : 어디서부터 자를지 시작점을 정한다.

let arr = [1, 2, 3, 4, 5]
//spilce()는 기존 배열의 값을 바꾸기 때문에 연습코드마다 배열을 다시 정의해야 한다.

      - 0일 경우 : 시작점부터 잘라 가져온다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(0)
//result : [1, 2, 3, 4, 5]
//arr: []

      - 양수일 경우 : 인덱스(start값의 자리에 있는 배열 값)를 포함하고 이후 배열의 마지막 값까지 가져온다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(2)
//result : [3, 4, 5]
//arr: [1, 2]

      - 음수일 경우 : 배열의 마지막 값이 (-1)이다. (-2) 일 경우 배열의 뒤에서 두 번째 값을 나타낸다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(-2)
//result : [4, 5]
//arr: [1, 2, 3]

      - undefined인 경우 : 0일 경우와 같다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(undefined)
//result : [1, 2, 3, 4, 5]
//arr: []

      - 배열의 길이보다 숫자가 큰 경우 : 빈 배열을 반환한다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(7)
//result : []
//arr: [1, 2, 3, 4, 5]

 

  ② deleteCount : 몇 개를 가져올지 정한다. 생략이 가능하다. 생략할 경우 시작점부터 배열의 마지막까지 가져온다.

      - 0일 경우 : 시작점이 어디든 빈 배열을 반환한다. 가져올 개수가 0이기 때문이다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 0)
let result2 = arr.splice(1, 0)
let result3 = arr.splice(undefined, 0)
let result4 = arr.splice(-1, 0)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - 양수일 경우 : start 인덱스 값을 포함한 숫자만큼 값을 가져온다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 2)
//result1 : [1, 2]
//arr: [3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 2)
//result2 : [3, 4]
//arr: [1, 2, 5]

let arr = [1, 2, 3, 4, 5]
let result3 = arr.splice(undefined, 2)
//result3 : [1, 2]
//arr: [3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result4 = arr.splice(-2, 2)
//result4 : [4, 5]
//arr: [1, 2, 3]

      - 음수일 경우 : 0일 경우와 같다. 

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, -2)
let result2 = arr.splice(1, -2)
let result3 = arr.splice(undefined, -2)
let result4 = arr.splice(-1, -2)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - undefined인 경우 : 0일 경우와 같다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, undefined)
let result2 = arr.splice(1, undefined)
let result3 = arr.splice(undefined, undefined)
let result4 = arr.splice(-1, undefined)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : 배열의 마지막 요소까지 포함하여 가져온다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 7)
//result1 : [1, 2, 3, 4, 5]
//arr: []

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 7)
//result2 : [3, 4, 5]
//arr: [1, 2]

let arr = [1, 2, 3, 4, 5]
let result3 = arr.splice(undefined, 7)
//result3 : [1, 2, 3, 4, 5]
//arr: []

let arr = [1, 2, 3, 4, 5]
let result4 = arr.splice(-2, 7)
//result4 : [4, 5]
//arr: [1, 2, 3]

 

  ③ item... : 추가할 요소. 생략이 가능하다. 생략할 경우 요소를 제거하기만 한다.

      - 요소가 추가될 위치는 start 인덱스 값의 위치가 기준이다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 2, "a", "b", "c")
//result1 : [1, 2]
//arr: ["a", "b", "c", 3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 2, "a", "b", "c")
//result2 : [3, 4]
//arr: [1, 2, "a", "b", "c", 5]

      - start 값이 0 또는 양수일 경우 : 선택된 값 바로 다음 인덱스부터 차례로(오른쪽으로) 값이 쌓인다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(2, 0, "a", "b", "c")
//result : []
//arr: [1, 2, "a", "b", "c", 3, 4, 5]

      - start 값이 음수일 경우 : 선택된 값 이전 인덱스부터 차례로(오른쪽으로) 값이 쌓인다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(-2, 0, "a", "b", "c")
//result : []
//arr: [1, 2, 3, "a", "b", "c", 4, 5]

 


정리하고 보니 나는 이해가 되는데 남이 보기 어렵게 되어버린 거 같은데?
앵간하면 다 가능

nodejs + poppeteer


링크

웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

② 웹 크롤링 해보기 #2 - NodeJS, puppeteer

 

 이번 포스팅에는 저번에 이어서 한번 더 크롤링(crawling or scrapping)을 해볼까 합니다.

 

 이번 포스팅에서는 cheerio라는 라이브러리를 다뤘었는데요. 이번에는 웹에 들어가서 여러 가지 "javascript의 일"을 할 수 있는 라이브러리를  다뤄보려고 합니다. 바로 "꼭두각시"라는 뜻을 가지고 있는 듯한 "Poppeteer"입니다.

 

cheerio와 다른 점이라면 웹사이트에 로그인을 한다던가, 웹사이트 스크린샷을 찍어서 파일로 저장한다던가, PDF로 만들어서 저장한다던가 하는 일들을 할 수 있습니다.

 

 이번에 제가 보여드릴 예제는 "상품 긁어오기"입니다. 상품을 긁어오는데 cheerio면 충분할 줄 알았는데 복병이 있었습니다. 바로 네이버..

 

 네이버는 상품의 옵션을 보여줄 때 옵션 버튼을 클릭하기 전까지는 옵션의 엘리먼트들을 "hidden"처리를 해놓기 때문에 cheerio로는 긁어오는 것이 불가능합니다. 

 

삼다수를 제주도에서 시키면 4천원 배송비가 더 드는 현실

 요로코롬 눌러줘야 옵션 창이 뜹니다. 그래서 제가 이야기했던 "자바스크립트의 일"인 버튼을 클릭하는 일을 할 수 있는 라이브러리를 찾던 중에 Puppteer를 발견한 것이었습니다.

 

 이 라이브러리는 chromium의 렌더링 엔진을 사용해서 headless browser 제어를 도와줍니다.

 

 말이 어렵군요.. 제가 이해하는 방식으로 설명해보겠습니다.(저처럼 이해해도 사용하는데 문제가 "아직은" 없었으니까요.)

 

 일반적으로 우리가 사용하는 크롬처럼 GUI방식의 웹브라우저를 CLI(Command Line Interface) 방식으로 제어한다고 생각하면 쉬울 것 같습니다. 맥이면 터미널, 윈도우면 cmd창으로 브라우저를 제어한다는 의미입니다. 크롬 창을 띄우지 않았는데 크롬창을 띄운 것처럼 실행하고 반응하고 원하는 작업을 할 수 있는 것입니다.

 

 자, 그럼 대충 바로 시작해볼게요.

 

1. 설치를 해야겠죠?

// npm
npm install puppeteer

//yarn
yarn add puppeteer

 

2. 그리고 nodeJS 파일에 임포트

import puppeteer from 'puppeteer'

 

3. 그리고 비동기 함수를 만들어 주기

async function scrapeWeb {

    // headless browser start
    const browser = await puppeteer.launch()

    // open new page
    const page = await browser.newPage()

    // connect web link
    await page.goto('http://example.com')

    // wait page
    await page.waitForNavigation({timeout: 10000})
    
    // click event
    await page.click("a.a_class")
    
    // wait option popup
    await page.waitForSelector('ul.li_class', {timeout: 10000})

    // scrape web data
    data = await page.evaluate( () => {
    	
        // do something you want
    	title = document.querySelector("._3oDjSvLwq9").textContent

    	return {
     		title: title
        }
        
    })

    // close browser
    await browser.close()

}

 poppeteer.launch()는 기본값으로 headless로 실행이 됩니다.

 page.waitForNavigation({timeout:10000})은 페이지가 완전히 로딩되기를 기다립니다. 이 메소드의 실행 완료를 기다리지 않고 클릭 메소드를 실행해도 ul 엘리먼트가 감지 되지 않아서 꼭 필요합니다. timeout은 기본값이 30초인데, 페이지의 완전한 로딩을 30초 동안 기다려준다는 뜻입니다. 제가 성격이 느긋하다지만 오류가 생겨도 30초는 못기다리겠더라구요. 오류가 생겼으면 빨리 뱉으라는 심정으로 10초로 줄였습니다.(열리더라도 10초 넘게 기다려야되면 그것도 오류아닌가요 여러분?)

 페이지의 로딩을 기다리고 클릭도 하고 ul 엘리먼트가 생기는 것을 확인한 후에 page.evaluate() 메소드를 이용해서 하고 싶은 작업을 맘껏 하시면 됩니다.

 그리고 잊지 말고 browser.close() 하면 끝.

 

4. 끝

 끝입니다.


5. 번외 & 팁

  5-1. page.evaluate에 argument(인자)를 전달하고 싶을 때

page.evaluate( (name) => {

    const data = `${name}`

    return data
    
}, name)

 

  5-2. page.evauluate에 인자를 여러 개 전달하고 싶을 때

    5-2-1. 객체 법

page.evaluate( ({name, age, gender}) => {

    const data = `${name}${age}${gender}`

    return data
    
}, {name, age, gender})

    5-2-1. 직렬 법

page.evaluate( (name, age, gender) => {

    const data = `${name}${age}${gender}`

    return data
    
}, name, age, gender)

 원하시는 방법대로 인자를 전달하세요.

 

  5-3. page.evaluate() 안에서 console.log는 에러를 뱉어냅니다.

 console.log를 하면 에러( Evaluation failed: ReferenceError: _ac2‍ is not defined )를 뱉어냅니다. _ac2는 랜덤하게 바뀌는 문자열입니다. 정확한 원인을 찾지 못해서, 또는 찾은걸 해봤는데 안 돼서, 또는 찾았는데 이해가 안돼서 그냥 저는 값을 계속 return 시켜보면서 확인했습니다.

=>2021.3.13 추가

 역시 모르면 공부하면 알게되는데 귀찮아서 미루고 있다가 지금 알게되었습니다. evaluate는 저쪽 크롤링을 시전하고 있는 puppeteer쪽 크롬창에 명령을 한번에 전달하는 메소드입니다. 그래서 그 안에 console.log를 하면 저쪽 크롬창에 뜨게 되는 것이죠. puppeteer를 launch할 때 옵션으로 {headless: false}옵션을 주면 창이 하나 뜨면서 확인이 가능합니다. 물론 나중에는 다시 꺼놔야겠지요!

 

링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

② 웹 크롤링 해보기 #2 - NodeJS, puppeteer


5-3 해결 방법 아시는 분? 해답이 이제는 필요 없지만 찍히는 거 보고 싶어요.
모두가 필수 코스로 해본다는 크롤링, 제가 해보겠습니다.

 


링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

웹 크롤링 해보기 #2 - NodeJS, puppeteer

 

 오늘은 웹 크롤링(스크래핑)에 대한 내용입니다.

 

 저는 뭔가를 빠르게 구현해보고 싶을 때 PHP framework인 라라벨을 주로 사용하는데 웹 크롤링을 하기 위해 빠르게 비동기적으로 처리가 가능한 nodeJS를 선택했습니다. PHP는 언어의 특성상 크롤링을 하는데 적합하지 않다고 판단했습니다. 하나의 명령을 처리하고 그 명령을 완료하기 전에 다음으로 넘어가지 않아 많은 명령을 동시에 처리하지 못하는 한계를 가지고 있기 때문입니다. 물론, 그것을 보완하는 방법들도 있기는 하지만 그냥 nodeJS로 구현하면 쉬워져요.. 심지어 빠릅니다.

 

슈로록

 

 쿠팡에서 아이템의 수량을 주기적으로 파악해야 할 일이 생겨서 웹사이트를 들락날락하던 중에 클릭이 귀찮아져서 그냥 하나 만들어 봤습니다.

 

 자세히 보면 1번 행부터 순차적으로 진행되는 것이아니라 순서 없이 요청이 완료되는 순서대로 값이 보입니다. 이것이 nodeJS의 힘. 그럼 바로 하나씩 진행해 보겠습니다.

 

1. vue cli를 이용하여 프로젝트 만들기

 여기서 두가지 방법으로 나누어집니다.

 

 첫 번째는, 폴더를 두 개 만들어서(frontend, backend) frontend폴더에 vue-cli를 이용해서 프로젝트를 만들고, backend 폴더에 nodeJS, express 프로젝트를 만들어서 api 통신하게 만드는 방법!

 두 번째는 vue-cli로 프로젝트를 만들어서 express plugin을 이용하는 방법!

 

 첫 번째 방법은 다른 분들도 포스팅을 많이 하기도 했고, plugin을 사용해보고 싶어서 두 번째 방법으로 해보았습니다(vue-cli 버전이 3.x.x에서 작동합니다. vue --version으로 확인하세요).

 

vue create project-name

 먼저 폴더로 가서 프로젝트를 만들어 줍니다.

 

2. express plugin install

cd project-name
vue add express

 다음은 프로젝트 폴더로 들어가서 express를 add 해주면 끝. 간단하지요?

 

 package.json

"scripts": {
	...
	"express:watch": "vue-cli-service express:watch",
    ...
},

 개발을 하면서 코드 변경이 있으면 자동으로 서버를 재시작해주는 명령어입니다. 이거 없으면 기분이 안 좋아요. 꼭 하세요.

 

3. frontend, backend 실행

 

 

 폴더 구조는 다음과 같아지는데 src가 frontend, srv가 backend 폴더라고 보면 쉽습니다.

 

 그럼 실행!

// frontend start - http://localhost:8080
npm run serve


// backend start - http://localhost:3000
npm run express:watch

 이렇게 하면 시작할 수 있는데 저 같은 경우에는 port가 frontend는 8080, backend는 3000입니다. 개발환경에 따라 port는 달라질 수 있으니 확인하세요!

 

4. 라이브러리 설치

 이제 웹 크롤링에 사용할 cheerio 라이브러리를 설치!

npm install cheerio

 

Q. cheerio 말고 다른 라이브러리는 없나요?

A. 기본적으로 웹을 크롤링할 때는 목적이 있습니다. 저처럼 로그인이 필요 없고, 그냥 웹페이지의 내용만 가져올 때는 cheerio가 적합하죠. 가볍고 빠릅니다.

 하지만 웹에 들어가서 자동으로 로그인을 한다던가, 클릭해서 팝업창을 열어서 그 내용을 가져온다던가, 드래그 앤 드롭을 한다던가 하는 "javascript의 일"을 해줄 순 없습니다. 그래서 이런 "javascript의 일"을 하기 위해서 selenium, phantomJS, nightmareJS 등 다른 라이브러리를 이용하기도 하니까 목적이 다르다면 위 라이브러리를 검색해보세요.

 

5. 코드

 srv/index.js

import express from 'express'
import cors from 'cors'
import { checkProducts } from "./apis/checkProducts"

export default (app, http) => {

  app.use(express.json())
  app.use(cors({
    origin: 'http://localhost:8080',
    credentials: true,
  }))

  app.post('/check', (req, res) => {
    let id = req.body.id
    let link = req.body.link

    checkProducts(id, link).then( product => {
      res.json(product)
    }).catch( err => console.log(err) )
  })
  
}

 cors는 frontend에서 api 호출할 때 오류가 나서 추가했어요. 오류가 안 난다면 그냥 진행하셔도 무방합니다.

 

srv/apis/checkProducts.js

import axios from 'axios'
import cheerio from 'cheerio'

function checkProducts(id, url) {

    return axios.get( url ).then( ({data}) => {
        let $ = cheerio.load(data)
        let $prodName = $("div.prod-buy-header h2").text()
        let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')
        let $outOfStock = $('div.oos-label').text().trim()
        

        return {
            id: id,
            title: $prodName,
            salePrice: $salePrice,
            outOfStock: $outOfStock
        }
    })
    
}

export { checkProducts }

 코드는 모두 srv/index.js에 몰아넣어도 되는데, 한 파일에 코드량이 많아지면 읽기가 불편하더라구요, 그래서 그냥 파일을 나눈 것뿐입니다. 

 

 진행은 간단합니다.

1. "http://localhost:3000/check"라는 URL로 POST방식을 사용해서 id와 link 파라미터를 전달한다.

2. 전달된 link(url)에 있는 내용을 읽어와서 cheerio를 이용해 원하는 내용만 읽어낸다.

3. 읽어낸 내용을 return 한다(여기서는 frontend로 데이터를 보내준다).

 

cheerio 라이브러리는 jQuery를 써본 분들은 DOM에 접근하는 방식이 같아서 무난하게 사용할 수 있습니다. 그래도 처음 접하는 분들을 위해 설명을 붙여보자면

 

 제가 목이 말라서 방금 쿠팡에서 "물"을 검색해서 상품을 하나 골랐습니다. 이 상품의 가격에 접근하는 방법은 크롬 개발자 도구를 이용하면 더 쉽습니다. Elements 탭 가장 하단을 보시면 자기가 클릭한 내용이 어떤 트리로 구성되어있는지 쉽게 보여줍니다.

 

srv/apis/checkProducts.js

let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')

 이 부분만 떼어서 보겠습니다. 이 물의 가격에 접근할 때 span.total-price를 통해 접근 가능한데, 혹시 다른 곳에서 같은 태그와 클래스(span태그와 total-price클래스)를 사용할지도 모르니 더 정확하게 상위 태그와 클래스(div.prod-sale-price)를 추가했습니다.

 

 선택된 내용에서 텍스트만 추출(text())해서 따옴표("), 쉼표(,) 같은 녀석들은 정규표현식(replace(/[^0-9]/g,''))을 이용해서 걸러내 줍니다(정규표현식은 구글에서 검색하면 고수들이 알려주니 여기서는 외우거나 이해할 필요는 없습니다. 저도 몰라요).

 

 자 이렇게 구현은 끝났습니다. 정말이지 nodeJS는 정말 강력한 것 같습니다. 제가 들이는 업무의 시간이 1/10로 줄어들었거든요. 앞으로 확인해야 할 상품이 늘어날 걸 예상한다면 시간을 더 아끼는 셈이 되겠네요.

 

 여러분은 크롤링(스크래핑)을 이용해서 해결할 일이 있으신가요?

 

링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

 웹 크롤링 해보기 #2 - NodeJS, puppeteer


역시 코딩은 즐겁고, 결과물은 저를 행복하게 하는군요.

+ Recent posts