쓸 때는 몰랐지만 많은 기술이 숨어 있었다!


이미지 업로드하기 #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]

 


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

+ Recent posts