벨로그·티스토리 썸네일(배너) 만들기, blog banner generator

Hyewon Kang·2023년 8월 1일
1
post-thumbnail

기존 github.io에서 벨로그로 개발 블로그를 이전하면서 포스트마다 통일성 있는 썸네일을 주고 싶었다. 사실 재작년에 벨로그를 잠시 이용했었는데 당시에는 포스트를 작성할 때마다 파워포인트로 템플릿을 하나 만들어놓고 텍스트만 고쳐서 사진 파일로 저장하곤 했었다. 그러나 단순히 썸네일 이미지 제작을 위해 ppt 파일을 열었다 닫았다, 텍스트를 지우고 고쳤다 저장했다 하는 것이 비효율적이었고 시간을 쓰는 것이 아까웠다. 그래서 언젠가는 아주아주 간단한 나만의 썸네일 제작 제너레이터를 구현해야겠다는 생각을 가졌었고 블로그 이전을 계기로 빠르게 개발해보았다.

blog-banner-generator는 벨로그 썸네일용 사이즈와 티스토리 배너용 사이즈(1:1)를 모두 지원하도록 했다. 또한, 나의 필요에 의해 만들기는 했지만 나와 같이 예쁘게 썸네일은 주고 싶지만, 직접 만들기는 귀찮은 사람들을 위해 다양한 템플릿을 지원하는 것도 구현에 포함시켰다. 템플릿의 경우는 여러 레퍼런스들을 보면서 베이직한 디자인으로 구현했다.


사이트 이용 방법 ✨

1. 배너 사이즈 고르기

배너 사이즈는 현재 velog와 tistory 썸네일 용 사이즈만을 제공한다. 따라서 사용자의 개발 블로그 종류에 따라 선택하면 된다.

2. 배너 템플릿 고르기

2023/08/02 현재 사이트에는 8개의 템플릿을 올려두었다. 원하는 템플릿을 선택하면 해당 템플릿의 제목과 소제목의 배치 및 주변 선이나 테두리와 같은 동일한 레이아웃을 이용할 수 있다. 이때 예시 이미지의 배경, 폰트, 색상은 유지되지 않고, 이는 다음 작업에서 사용자가 선택할 수 있다.

3. 배너 내부 요소 채우기!

선택한 배너 템플릿의 배경 및 폰트 스타일을 설정하고 텍스트를 입력하고 배치하는 단계다.

배경 설정

가장 먼저 배너의 배경은 다음과 같은 세 가지 타입으로 이용 가능하다.

  • 그라디언트: 그라디언트의 경우 webgradients에서 제공하는 색상 조합들을 이용했다.
  • 단색: 단색 배경은 사이트에 제시된 팔레트 색상들을 이용하거나 가장 마지막 컬러 요소를 통해 color picker도 이용 가능하다.
  • 이미지: 이미지는 랜덤 이미지 생성 버튼(Generate)을 통해 unsplash 랜덤 이미지를 받아올 수 있고, 혹은 사용자가 직접 원하는 이미지 주소를 입력 창에 넣어 배경으로 사용도 가능하다.

폰트 스타일 설정

  • 텍스트의 좌측/중앙/우측 정렬
  • 폰트 및 테두리, 선의 색상 지정
  • 텍스트의 크기 large/medium
  • 폰트 선택

텍스트 입력

현재는 제목과 소제목 두 가지 입력이 가능하다. 소제목은 주로 원하는 태그들을 문자열로 입력하여 사용할 수 있다. 이때 제목과 소제목에 입력한 텍스트가 길어져 사용자 임의로 줄바꿈을 주고 싶다면 input에 <br /> 문자열을 추가하면 줄바꿈이 가능하다. 예: # JavaScript<br /># html2canvas

코드 설명 💡

1. Preview

사용자가 배경을 바꾸고, 폰트를 변경하고, 텍스트를 입력하면 미리보기(preview)에 바로 반영되어야 한다. 이러한 preview에는 배경 타입, 배경색상, 배경이미지, 폰트색상, 제목, 소제목 등 여러 상태들이 저장 및 관리되어야 한다. 그래서 Preview 클래스를 만들어 여러 상태들을 프로퍼티로 갖도록 만들었고, 상태 관리를 위한 메서드들을 추가했다.

export default class Preview {
    constructor() {
        this.size = '1600x900'; // 1600x900 | 800x800
        this.title = '';
        this.subtitle = '';
        this.textAlign = 'preview-text-center';
        this.font = 'font-noto-sans';
        this.fontLarge = true;

        this.gradient = null;
        this.selectedColor = null;
        this.template = '0';

        this.preview = document.querySelector('.preview');
        this.previewTitle = document.querySelector('.preview-title');
        this.previewSubtitle = document.querySelector('.preview-subtitle').querySelector('span');

        this.updateTitle = _.debounce(this.updateTitle.bind(this), 100);
        this.updateSubTitle = _.debounce(this.updateSubTitle.bind(this), 100);
    }
	...
}

사용자가 배경 이미지를 변경하면 preview에 이를 렌더링 시켜주어야 한다. 그래서 Preview가 갖는 프로토타입 메서드들은 모두 DOM 요소에 접근해 재렌더링을 시켜주는 역할을 한다.

...
	setColor(color) {
        this.selectedColor = color;
    }

    updateTemplate(val) {
        this.preview.classList.replace(`template-${this.template}`, `template-${val}`);
        this.template = val;
    }

    updateSize(size) {
        this.size = size;
        this.preview.classList.toggle('preview-size-1600x900', size === '1600x900');
        this.preview.classList.toggle('preview-size-800x800', size === '800x800');
    }
...

2. Control

미리보기에 사용자의 입력을 바로 적용시키기 위해서는 사용자의 행위에 의해 이벤트를 발생시켜야 하고, 각 이벤트에 이벤트 핸들러를 부착하는 과정이 필요하다. 사이트를 보면 알 수 있듯이 클릭 이벤트가 일어날 요소들이 매우 매우 많다. 이러한 코드들은 모두 control.js에 작성했다.

이벤트 핸들러로는 앞서 구현한 Preview 클래스의 인스턴스를 먼저 생성한 후, 해당 인스턴스가 가지는 프로토타입 메서드들을 전달한다.

아래는 step1의 배너 사이즈를 고르는 로직의 일부다.

...

/**
 * size
 */

const bannerSizeWrapper = document.querySelector('.banner-size-select-section');
const bannerSizeElements = bannerSizeWrapper.querySelectorAll('.banner-size-template');
bannerSizeWrapper.addEventListener('click', onChangeBannerSize);

function onChangeBannerSize(e) {
    const clickedItem = e.target;
    const selectedClass = 'banner-size-selected';
    const sizeAttributeName = 'aria-button-name';

    if (clickedItem.classList.contains(selectedClass)) return;

    bannerSizeElements.forEach((item) => {
        const isClickedItem = clickedItem === item;
        item.classList.toggle(selectedClass, isClickedItem);

        if (isClickedItem) {
            const size = item.getAttribute(sizeAttributeName);
            preview.updateSize(size); // preview 메서드 호출
            templateContainer1.classList.toggle('visible');
            templateContainer2.classList.toggle('visible');
        }
    });
}

...

3. Stepper

Stepper는 배너 제작을 진행하는 3가지 단계 및 현재 단계를 보여주기 위한 UI로 이 역시도 클래스로 구현했다.

class Stepper {
    constructor() {
        this.currentIndex = 0;
        this.steps = document.querySelectorAll('.step');
        this.nextBtn = document.querySelector('.next-btn');
        this.prevBtn = document.querySelector('.prev-btn');
        this.submitBtn = document.querySelector('.submit-btn');
        this.contents = document.querySelectorAll('.content');

        this._listen();
    }

    _listen() {
        this.nextBtn.addEventListener('click', this.next.bind(this));
        this.prevBtn.addEventListener('click', this.previous.bind(this));
    }

    show(el) {
        el.classList.add('visible');
    }

    hide(el) {
        el.classList.remove('visible');
    }

    _setFinishButton() {
        this.hide(this.nextBtn);
        this.show(this.submitBtn);
    }

    _setNextButton() {
        this.hide(this.submitBtn);
        this.show(this.nextBtn);
    }

    _setNextItem(next) {
        this.steps[next].className = 'step step-item-process';
    }

    _setNextContent(next) {
        this.hide(this.contents[this.currentIndex]);
        this.show(this.contents[next]);
        this._setNextItem(next);
        this.currentIndex = next;
    }

    next() {
        if (this.currentIndex === this.steps.length - 1) {
            this._setFinishButton();
            return;
        }
        const nextIndex =
            this.currentIndex + 1 < this.steps.length
                ? this.currentIndex + 1
                : this.steps.length - 1;
        this.steps[this.currentIndex].className = 'step step-item-finish';
        this._setNextContent(nextIndex);

        if (this.currentIndex === 1) this.show(this.prevBtn);
        else if (this.currentIndex === this.steps.length - 1) this._setFinishButton();
    }

    previous() {
        const prevIndex = this.currentIndex - 1 >= 0 ? this.currentIndex - 1 : 0;
        this.steps[this.currentIndex].className = 'step step-item-wait';
        this._setNextContent(prevIndex);

        if (this.currentIndex === 0) this.hide(this.prevBtn);
        if (this.currentIndex !== this.steps.length - 1) this._setNextButton();
    }
}

const stepper = new Stepper();

4. html2canvas

사용자가 완성한 배너, 즉 화면에 띄워진 미리보기를 png 형식으로 다운받기 위해 html2canvas 라이브러리를 활용했다. html2canvas 라이브러리는 자바스크립트 브라우저 화면 캡쳐 라이브러리로 캡처 하고 싶은 DOM(.preview)을 html2canvas() 함수의 파라미터로 전달해 호출하면 Promise 객체를 리턴받고, 이를 통해 특정 영역을 포함한 canvas 객체를 받을 수 있다.

html2canvas()에 두번째 인수로 전달한 객체는 options로 아래와 같은 이유로 추가했다.

  • allowTaint, useCORS → background 이미지를 정상적으로 caputre 하기 위함
  • scale → scale 값을 기본 1에서 4로 설정하여 다운받는 이미지 사이즈를 4배 키워 출력. 이를 통해 이미지 해상도 개선.
const submitBtn = document.querySelector('.submit-btn');

submitBtn.addEventListener('click', captureExport);

function captureExport() {
    html2canvas(document.querySelector('.preview'), {
        allowTaint: true,
        useCORS: true,
        scale: 4,
    }).then((canvas) => {
        saveAs(canvas.toDataURL(), 'bloggyBanners');
    });
}

function saveAs(uri, filename) {
    let link = document.createElement('a');
    if (typeof link.download === 'string') {
        link.download = filename;
        link.href = uri;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    } else {
        window.open(uri);
    }
}

(전체 코드는 깃헙에서 확인 가능합니다)


해당 사이트는 얼른 벨로그를 시작하고 싶은 마음을 담아 빠르게 개발했다. 현재 리팩토링 할 만한 요소들이 몇 가지 남아있어서 꾸준히 개선해보려고 한다. 앞으로 썸네일 디자인에 애쓰지 않고 간단하게 만들 수 있을 것 같아 완성된 것이 뿌듯하다-! 다음엔 어떤 토이 프로젝트를 해볼까 ..


blog-banner-generator 사이트 방문하기

사이트를 이용하시다가 에러나 개선했으면 하는 부분을 발견하신다면 이슈 혹은 댓글에 남겨주시면 감사하겠습니다 💪 사이트에 추가되었으면 하는 디자인 또한 이슈에 남겨주시면 최대한 빠르게 추가해드립니다!

많은 피드백 부탁드립니다😻

profile
강혜원의 개발일지 입니다.

1개의 댓글

comment-user-thumbnail
2023년 8월 8일

아이디어 짱 좋은디요?! 🥰

답글 달기