개발자취

TIL | 토이프로젝트 2 - 이미지 리사이징 서버 본문

개발/Dev | 웹개발

TIL | 토이프로젝트 2 - 이미지 리사이징 서버

hnhnhun 2022. 7. 27. 15:28

1. 프로젝트 설계

  본 프로젝트는 키워드로 검색하여 나온 이미지를 원하는 사이즈로 리사이징해서 돌려주는 서버를 구현하는 것이다.

1.1 프로젝트 개요

  본 프로젝트 구현에 앞서, /*개발을 수월하게 하기 위해*/ 이미지를 자유롭게 불러오는 웹 서비스가 필요하다. 따라서 본 프로젝트는 unsplash.com의 api를 통해 이미지 소스를 불러올 수 있도록 세팅하였다. 그리고 이미지 리사이징을 구현하기 위해 node의 sharp 패키지를 사용하였다. 본 패키지를 선택한 이유는 /*강의 영상에서 선택한 탓이 가장 크지만, */ 코어 부분이 c++로 짜여있어 이미지 리사이징, 포맷 컨버팅과 같은 동작을 훨씬 빠르게 하기 때문이다.  

1.2 Frontend UI 디자인

서버만 구현하므로 frontend ui 디자인은 없음.

1.3 Backend

2. 구현 flow

- main.js

- unsplash-js : unsplash api, 이미지를 가져올 때 사용.

- node-fetch : unsplash api로 이미지를 가져올 때 pipeline을 사용하여 요청/응답을 받아올 수 있음.

- sharp : 이미지 리사이징

- imagesize : 이미지 사이즈 값을 반환

- fs : 캐시 이미지를 리턴하기 위함. api가 demo 버전이어서 5,000 requests/hour 이다.

- stream: pipe를 기다리기 위해서 pipeline을 사용.

// @ts-check
const http = require('http')
const fs = require('fs')
const path = require('path')
const fetch = require('node-fetch') //node-fetch@2 버전으로 재설치 후 {default:fetch} > fetch로 변경.
const { createApi } = require('unsplash-js')
const { pipeline } = require('stream')
const { promisify } = require('util')
const sharp = require('sharp')
const imageSize = require('image-size')

require('dotenv').config()

const unsplash = createApi({ // 이미지를 가져오기위한 api 세팅, key값과 fetch를 세팅한다.
  accessKey: process.env.UNSPLASH_API_ACCESS_KEY,
  // @ts-ignore
  fetch: fetch.default,
})

// @param {string} query
async function searchImage(query) { //불러오는 이미지의 query를 의미한다. 예시) sea-salt
  const result = await unsplash.search.getPhotos({ query })
  if (!result.response) {} //응답이 없는 경우 예외처리
  const image = result.response.results[0] //많은 이미지 중에 첫번째 이미지를 선택함
  if (!image) {} //예외처리
  return { // 이미지(정보)를 가져옴
    description: image.description || image.alt_description,
    url: image.urls.regular,
  }
}

/**
 * 이미지를 Unsplash에서 검색하거나, 이미 있다면 캐시된 이미지를 반환.
 */

async function getCachedImageOrSearchedImage(query) { //파일 내에 존재하는가.
  const imageFilePath = path.resolve(__dirname, `../images/${query}`) //이미지 파일 절대 경로
  if (fs.existsSync(imageFilePath)) { //존재하면 반환하는 메세지
    return {}
  }

  const result = await searchImage(query) //이미지 검색
  const resp = await fetch.default(result.url) //이미지 url

  resp.body.pipe(fs.createWriteStream(imageFilePath)) // 이미지 파일 경로를 stream으로 pipe에 연결
													  // 요청한 서버에 1차로 응답해주는 것(?)
  await promisify(pipeline)(resp.body, fs.createWriteStream(imageFilePath)) // body를 pipeline에 넣어서 응답대기 
  const size = imageSize.default(imageFilePath) // size는 default 값으로

  return { //이미지(정보) 반환
    message: `Returning new image: ${query}, width: ${size.width}, height: ${size.height}`,
    stream: fs.createReadStream(imageFilePath),
  }
}
/**
 * @param {string} url
 * @returns
 */
function convertURLToImageInfo(url) {
  // URL을 어떻게 파싱할 것인가?
  const urlObj = new URL(url, `http://localhost:${PORT}`)

  /**
   * @param {string} name
   * @param {number} defaultValue
   * @returns
   */
  function getSearchParam(name, defaultValue) { // 파라미터를 받아오는 형태 지정
    const str = urlObj.searchParams.get(name)
    return str ? parseInt(str, 10) : defaultValue // 숫자로 변환, 없으면 defalutvalue
  }

  const width = getSearchParam('width', 400) 
  const height = getSearchParam('height', 400)

  return {
    query: urlObj.pathname.slice(1),
    width,
    height,
  }
}

const server = http.createServer((req, res) => {
  async function main() {
    if (!req.url) { } //서버 연결이 안된 경우 예외 처리
  
    const { query, width, height } = convertURLToImageInfo(req.url)
    try {
      const { message, stream } = await getCachedImageOrSearchedImage(query)
      console.log(message)
      await promisify(pipeline)( // promisify에 pipeline을 넣어서 응답을 받음
        stream, // 이미지 파일 정보
        sharp() // 이미지 리사이징 옵션들
          .resize(width, height, {
            fit: 'cover', // cover : 정해진 영역을 모두 채우는 것, conatain : 공백 생김
            background: '#ffffff',
          })
          .png(),
        res
      )
    } catch {
      res.statusCode = 400
      res.end()
    }
  }

  main()
})

const PORT = 5000
server.listen(PORT, () => {
  console.log(`The server is listening at port: ${PORT}`)
})

- images folder (캐시 이미지 저장용)

- .env (환경변수)

- package.json

{
  "engines": {
    "node": "14.16.1"
  },
  "scripts": {
    "server": "nodemon src/main.js"
  },
  "devDependencies": {
    "csv-parse": "^4.4.6",
    "eslint": "^8.21.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-node": "^11.1.0",
    "nodemon": "^2.0.19",
    "prettier": "^2.7.1",
    "typescript": "^4.7.4"
  },
  "dependencies": {
    "dotenv": "^16.0.1",
    "image-size": "^1.0.2",
    "node-fetch": "^2.6.7",
    "sharp": "^0.30.7",
    "unsplash-js": "^7.0.15"
  }
}

3. 구현 결과

- 이미지 원본

sea-salt

- 이미지 리사이징 결과 1

sea-salt, 500x700

- 이미지 리사이징 결과 2

sea-salt, 450x1200

4. 이슈

- node-fetch 패키지 오류 ; npm 패키지 버전에 따른 오류로 commonJS 와 ES modules가 혼용(?)되어 생긴 이슈가 있었다.

  패키지를 설치한 후 불러와서 const로 선언하는 것부터 문제가 있었다. 

> node-fetch@2로 재설치 후 {default:fetch} 를 fetch로 변경한 후 해결됨.

Comments