본문 바로가기
개발 기록

썸네일 생성기를 만들었어요 (Thumbnail generator)

by 유세지 2021. 7. 10.

 

오랜만의 토이 프로젝트입니다. 그동안 블로그 포스트를 작성하며 매번 썸네일을 직접 만들었는데, 대단한 퀄리티는 아니었지만 매번 이미지 편집 프로그램을 이용하는 과정이 조금 번거로웠습니다. 이 정도의 간단한 이미지 정도라면 프로그램으로 만들어 사용하는게 낫지 않을까? 라는 생각이 들어 썸네일을 생성해주는 프로젝트를 진행해보기로 하였습니다.

간단한 텍스트로 이루어진 썸네일들

 

구상

 

가장 익숙한 기술 스택인 자바스크립트와 노드를 이용하여 제작할 예정이었으니, 어떤 방식으로 작동할지 먼저 구상하고 그에 맞는 라이브러리를 찾기로 계획했습니다.

 

먼저 우리가 만들 썸네일 생성기는 이미지를 배경으로 하고, 가운데의 대제목과 그 밑에 조그만한 부제목으로 이루어진 간단한 형태의 썸네일을 만들어주는 방식으로 작동할것입니다. 그렇다면 먼저 css를 사용해 원하는 이미지의 형태를 잡아주고, 이 모양을 그대로 이미지 파일로 변환하여 사용자에게 제공하는 구조로 동작하면 충분할 것 같습니다.

 

 

 

 

입력받은 데이터를 토대로 화면을 구성하는것은 별다른 라이브러리가 필요하지 않으니 모두 css로 처리해주겠습니다. 다만 화면에 띄운 요소들을 이미지로 변환해서 사용자에게 반환해주는건 라이브러리를 사용해야겠습니다. 이러한 기능을 가진 라이브러리로는 html2canvas가 있으니 이걸 이용해보겠습니다.

 

구상이 끝났으니 바로 구현에 들어가보겠습니다.

 

 

구현

먼저 node.js 프로젝트를 생성하고 express-generator를 이용해 간단히 웹을 구축해주겠습니다.

 

 

설치가 끝났다면 npm run start 명령어로 localhost:3000에 정상적으로 접근이 되는것을 확인해봅시다.

 

 

성공적으로 설치가 확인되었으니 메인 화면부터 구현해보겠습니다.

먼저 form 양식을 이용하여 썸네일 이미지 생성에 필요한 데이터들을 받아줍시다.

필요한 데이터들은 썸네일 배경에 사용될 이미지, 제목, 부제목, 배경 색상, 폰트 색상 정도입니다.

 

<form name="form" action="/img" method="post">

      <label for="image_url">이미지 주소</label>
      <input id="image_url" type="text" name="image_url" placeholder="배경 이미지 주소">

      <label for="title_main">제목</label>
      <input id="title_main" type="text" name="title_main" placeholder="제목">

      <label for="title_sub">부제목</label>
      <input id="title_sub" type="text" name="title_sub" placeholder="부제목">

      <label for="bg_color">배경 색상</label>
      <input id="bg_color" type="color" name="bg_color" placeholder="">

      <label for="font_color">폰트 색상</label>
      <input id="font_color" type="color" name="font_color" placeholder="">

      <input id="submit" type="submit" name="submit" value="확인">

    </form>

 

일단 /img 경로에 데이터를 구성해서 보내주기로 하고, 라우팅은 잠시 후에 해주겠습니다.

 

이제 폼에 데이터를 넣어서 확인 버튼을 누르면, /img 경로에 Post 방식으로 요청을 보낼 수 있게 되었습니다.

그럼 이 데이터를 통해 썸네일을 구성해줄 차례입니다.

 

routes 디렉토리에 이미지를 구성할 라우터 image.js를 추가해줍니다.

 

 

const express = require('express');
const router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('잘못된 요청입니다.');
});

router.post('/', function(req, res, next) {
  const image_url = req.body.image_url;
  const title_main = req.body.title_main;
  const title_sub = req.body.title_sub;
  const bg_color = req.body.bg_color;
  const font_color = req.body.font_color;

  res.render('img', {
    image_url: image_url,
    title_main: title_main,
    title_sub: title_sub,
    bg_color: bg_color,
    font_color: font_color
  });
});

 

우리는 post 방식으로 데이터를 받기 때문에 get 방식으로 오는 요청은 무시하겠습니다.

post로 요청이 들어오면 req.body 에 담겨져있는 데이터들을 같은 이름의 변수로 렌더링 해줍시다.

 

구성한 라우터는 app.js에서 꼭 등록해주세요.

 

var indexRouter = require('./routes/index');
var imageRouter = require('./routes/image'); // 이미지 라우터 추가

var app = express();

...

app.use('/', indexRouter);
app.use('/img', imageRouter); // 라우터 등록

 

이제 /img 경로에서 데이터를 받을 수 있게 되었습니다. 받은 데이터를 토대로 썸네일 화면을 구성해봅시다.

views 디렉토리에 img.ejs 를 추가해주겠습니다.

 

<body>
    <div id="container">
      <div id="wrapper" style="background-image:url(<%= image_url %>)">
        <div id="title">
          <p id="title_main"><%= title_main %></p>
          <p id="title_sub"><%= title_sub %></p>
        </div>
        <div id="titleBorder"></div>
      </div>
    </div>

    <p><%= image_url %></p>
    <p><%= title_main %></p>
    <p><%= title_sub %></p>
    <p><%= bg_color %></p>
    <p><%= font_color %></p>
    
    
    <script>
    	let value = "<%= bg_color %>";
        value = value.match(/[A-Za-z0-9]{2}/g);
        value = value.map(function(v) { return parseInt(v, 16) });
        value = "rgba(" + value.join(",") + ",1)";

        window.onload = function () {
            const titleMain = document.getElementById("title_main");
            const titleSub = document.getElementById("title_sub");
            titleMain.style.backgroundColor = value;
            titleSub.style.backgroundColor = value;
        }
    </script>
</body>

 

스크립트 부분에서 넘어온 색상 값을 가공하여 이미지에 적용시켜주었습니다.

CSS 부분은 적당히 썸네일처럼 보이도록 주었습니다.

 

#container {
  width: 600px;
  height: 600px;
}
#wrapper {
  position: absolute;
  top: 0;
  left: 0;
  width: 600px;
  height: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  object-fit: cover;
}

#title {
  position: absolute;
  top: 0;
  left: 0;
  width: 600px;
  height: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba( 255, 255, 255, 0.5 );
}

#titleBorder {
  position: absolute;
  top: 50px;
  left: 50px;
  width: 500px;
  height: 500px;
  border: 3px solid rgba(255, 255, 255, 0.5);
}

#title_main {
  display: inline-block;
  text-align: center;
  font-size: 36pt;
  font-weight: bolder;
  margin: 0;
  padding: 0 .5rem;
  color: <%= font_color %>;
  background-color: rgba( 0, 0, 0, 0 );
}

#title_sub {
  display: inline-block;
  text-align: center;
  font-size: 24pt;
  font-weight: bold;
  margin: 0;
  padding: 0 .5rem;
  color: <%= font_color %>;
  background-color: rgba( 0, 0, 0, 0 );
}

 

 

여기까지해서 화면에 어떻게 보이는지 확인해보겠습니다.

 

배경 이미지 출처 : pixabay

 

대략 이런 느낌으로 화면에 표시되었습니다. 이제 이 화면을 이미지로 추출해서 사용자에게 제공하면 될 것 같습니다.

html 상의 요소를 파일로 변환하는 라이브러리로는 html2canvas가 있습니다. npm 방식으로도 사용할 수 있지만, dom 요소를 직접 다루어야하기 때문에 CDN 방식으로 불러와서 사용하는게 편할 것 같아 인라인해서 사용해보겠습니다.

 

img.ejs 를 수정해주겠습니다.

 

<head>
	...
	<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.5.0-beta4/html2canvas.js"></script>
	...
</head>

...

<script>
	window.onload = function () {
            const titleMain = document.getElementById("title_main");
            const titleSub = document.getElementById("title_sub");
            const wrapper = document.getElementById("wrapper");
            titleMain.style.backgroundColor = value;
            titleSub.style.backgroundColor = value;
            window.setTimeout(capture, 2000);
        }

	function capture() {
            html2canvas(document.getElementById("wrapper"), {
                logging: true,
                letterRendering: 1,
                allowTaint: false,
                useCORS: true
            })
                .then(canvas => {
                let a = document.createElement('a');
                a.href = canvas.toDataURL("image/png");
                a.download = "out.png";
                a.click();
            });
        }
</script>

 

이제 dom 요소가 로드되고 약 2초 후에 html2canvas의 캡쳐 기능을 통해 png 형식으로 파일을 저장할 수 있게 되었습니다. 한번 확인해보겠습니다.

 

 

우리가 지정했던 out.png 파일이 다운로드 된 모습을 볼 수 있습니다.

 

 

 

성공입니다! /img 경로에서 확인했던 그대로 파일이 된 것을 확인할 수 있었습니다.

 

 

오류 발생

 

...이렇게 프로젝트가 마무리 되는듯 했으나, 테스트를 하던 중 한 가지 문제를 발견했습니다. 바로 CORS 이슈입니다.

 

제가 예시로 사용했던 pixabay의 경우, 모든 주소로부터의 접근을 허용하고 있어 이미지를 로딩하고 구성하는데 별 문제가 없었으나 네이버와 같이 외부 접근을 허용하지 않는 곳으로부터의 이미지를 사용했을경우 html2canvas가 이미지를 캡쳐하지 못하는 현상이 발생하였습니다.

 

CORS 에러로 인해 이미지를 불러오지 못한 html2canvas

 

배경이 캡쳐되지 않은 썸네일, 이미지 출처 : 늘같이의 여행 과 감성사진

 

테스트 화면에서는 불러오는데 성공했지만, html2canvas는 CORS 에러에 막혀 배경 이미지를 참조하지 못해 제목만 입력된 썸네일이 저장된 모습입니다.

 

이를 우회하기 위해 하루정도 꼬박 삽질을 해서 라이브러리 자체를 수정해보기도 하고, 스택오버플로우와 라이브러리 레포지토리 이슈탭에 있는 해결책등 많은 방법을 시도해보았지만 실패했습니다. 결국 방법을 찾지 못하고 고민하던 중 또 다른 헤더 리스 브라우저 라이브러리인 puppeteer 을 시도해보면 어떨까라는 생각이 들었습니다.

 

기존에 사용하던 html2canvas를 걷어내고 puppeteer 를 적용해보겠습니다.

 

먼저 npm install을 통해 puppeteer를 설치해주고, 라우터 image.js를 수정해줍니다.

 

router.post('/req', async function(req, res, next) {
  const image_url = req.body.image_url;
  const title_main = req.body.title_main;
  const title_sub = req.body.title_sub;
  const bg_color = req.body.bg_color;
  const font_color = req.body.font_color;

  const browser = await puppeteer.launch({
    args: ["--enable-features=NetworkService", "--no-sandbox"],
    ignoreHTTPSErrors: true
  });
  const page = await browser.newPage()

  // Post로 요청을 보내기 위해 요청을 Intercept 합니다.
  await page.setRequestInterception(true);

  await page.once("request", interceptedRequest => {
    interceptedRequest.continue({
      method: "POST",
      postData: 'image_url=' + image_url + '&title_main=' + title_main + '&title_sub=' + title_sub + '&bg_color=' + bg_color + '&font_color' + font_color,
      headers: {
        ...interceptedRequest.headers(),
        "Content-Type": "application/x-www-form-urlencoded"
      }
    });

    // 다른 요청을 처리하기 위해 Intercept를 해제합니다.
    page.setRequestInterception(false);
  });

  await page.goto('http://localhost:3000/img')

  const fileName = "capture-" + new Date().toISOString().substr(0, 10) + "-" + Math.floor(Math.random()*10000+10000) + ".png";

  const element = await page.$('#container');
  await element.screenshot({
    path: fileName
  })

  await browser.close()
  await res.sendFile(fileName, {root : '.'});
});

 

코드가 다소 길어졌습니다.

 

먼저 post 요청을 받는 /req 를 추가해주었습니다. 이 코드는 /img/req 경로로 들어온 요청을 통해 puppeteer를 작동시켜 /img 경로의 썸네일 부분만을 캡쳐해올것입니다.

 

get 방식의 통신일때는 필요하지 않지만, post 방식의 통신이기 때문에 page.setRequestInterception(true); 를 통해 잠시 인터럽트를 발생시켜주고, postData 부분에 전송받은 (index에서 전송해주는) 이미지 구성 데이터들을 묶어 함께 보내주도록 설정해주고, 설정이 끝나면 다시 page.setRequestInterception(false); 를 통해 인터럽트를 종료시켜줍니다.

 

인터럽트를 종료하지 않으면 그 다음 요청을 보내지 못하고 자동으로 timeout이 되기 때문에 반드시 인터럽트를 종료시켜주어야합니다.

 

그 후로 page.goto(url) 를 통해 지정된 경로 (/img) 로 puppeteer를 보내주고, 우리가 캡쳐하고 싶은 요소인 #container를 .screenshot 을 이용해 캡쳐하여 지정된 fileName으로 저장하게 됩니다.

 

캡쳐가 끝나면 브라우저를 종료시키고, 방금 캡쳐한 파일을 사용자에게 export 해주는것으로 마무리됩니다.

이렇게 라우터 작업은 끝났고, 마지막으로 index.ejs로 가서 form 데이터를 보내는 경로를 수정해주면 됩니다.

 

<form name="form" action="/img/req" method="post">

 

그럼 한 번 테스트 해보겠습니다.

 

 

CORS 에러에 막혀 보이지 않던 배경 이미지가 이제야 제대로 보이는것을 확인할 수 있었습니다.

오랜만에 해보는 토이 프로젝트라 삽질도 즐겁게 한 것 같네요.

 

전체 소스는 아래 깃허브 저장소에서 확인 가능합니다.

 

GitHub :: usageness/Thumbnail-Generator

반응형

댓글