배너 생성기 리팩터링

개발

배너 생성기 리팩터링
최종 수정일:

상태 관리

export default class CanvasStore extends EventTarget {
    canvas?: HTMLCanvasElement;
    width: number;
    height: number;
    text: string;
    font: string;
    fontSize: number;
    fontColor: string;
    backgroundColor: string;
    backgroundImage?: HTMLImageElement;
    backgroundOpacity: number;
 
    constructor() {
        const backgroundColor = getRandomColor();
        const fontColor = getProperColor(backgroundColor);
 
        super();
        this.width = 700;
        this.height = 700;
        this.text = "Sample Text";
        this.font = "sans-serif";
        this.fontSize = 64;
        this.fontColor = fontColor;
        this.backgroundColor = backgroundColor;
        this.backgroundOpacity = 0.5;
        bindMethods(this);
    }
 
    #update() {
        this.dispatchEvent(new CustomEvent("update"));
    }
 
    setCanvas(canvas: HTMLCanvasElement) {
        this.canvas = canvas;
    }
 
    setWidth(width: number) {
        this.width = width;
        this.#update();
    }
 
    // ...
}

EventTarget의 자식 클래스를 제작해 전역 상태를 관리했습니다.
dispatchEvent를 통해 store의 상태에 의존하는 컴포넌트에 재렌더링해야 할 시점을 간편히 알려줄 수 있었습니다.

export default function Preview() {
    const { setCanvas } = canvasStore;
    const canvas = el("canvas", {
        className: "canvas",
        width: canvasStore.width,
        height: canvasStore.height,
    });
    const ctx = canvas.getContext("2d");
    const render = () => {
        // ...
    };
 
    canvasStore.addEventListener("update", render);
    render();
    setCanvas(canvas);
 
    return el("div", {}, canvas);
}

render는 canvas에 결과물을 그리는 함수입니다.
작성한 텍스트, 폰트 설정 등이 변경되면 render를 다시 호출해 결과물을 그리도록 작업했습니다.

const WIDTH = <HTMLInputElement>document.getElementById("width");
const HEIGHT = <HTMLInputElement>document.getElementById("height");
const TEXT = <HTMLInputElement>document.getElementById("text");
const BG = <HTMLInputElement>document.getElementById("bgColor");
const TEXTCOLOR = <HTMLInputElement>document.getElementById("textColor");
const SIZE = <HTMLInputElement>document.getElementById("fontSize");
const TRANS = <HTMLInputElement>document.getElementById("transparency");
const img = new Image();
 
let input = 0;
let font = "sans-serif";

예전엔 이렇게 input 요소 등과 상태 모두 전역에 두고 input 요소 모두에 일일이 change등 이벤트 핸들러 추가해둔 다음에 값 변경될 때마다 canvas를 다시 그렸는데, 다시 보니 새삼 끔찍하네요.

DOM 생성 방식

import { canvasStore } from "../store";
import el from "../utils/el";
 
export default function Textarea() {
    const { setText } = canvasStore;
    const handleChange = ({ target }: Event) => {
        if (!(target instanceof HTMLTextAreaElement)) {
            return;
        }
 
        setText(target.value);
    };
 
    return el(
        "div",
        { className: "text" },
        el("textarea", {
            className: "text__input",
            placeholder: "Type Here!",
            ariaLabel: "Type Here!",
            rows: 2,
            events: {
                change: handleChange,
                keydown: handleChange,
                keyup: handleChange,
            },
        }),
        el("div", { className: "text__line text__line--top" }),
        el("div", { className: "text__line" })
    );
}

상기 코드들에서도 볼 수 있듯 이번에도 함수 하나 만들어 활용했습니다.
style과 dataset도 다룰 수 있도록 업데이트하는 등 사용성을 많이 개선했고, eln이라는 함수도 추가로 만들어 SVG도 다룰 수 있도록 했습니다.

여전히 click 이벤트에서 event의 타입이 Event라는 점, SVGElement를 생성하려면 eln을 사용해야 한다는 점 등의 아쉬움이 남아있어 개선할 여지가 많긴 합니다.
상태가 변경됐을 때 DOM을 업데이트하는 부분도 너무 수동으로 해줘야 하는 작업이 많은 것 같아 아쉽네요.
천천히 개선해나가면 되지 않을까 합니다.

기타 개선점

맺으며

3년이란 세월이 흘러 묵은 때가 많이 쌓인 코드를 고치는 데 성공했고, 결과물이 썩 마음에 들어 여러모로 만족스럽네요.
끔찍한 코드 때문에 꽤 고통도 많이 받았지만, 제가 성장했단 증거기도 하고 오랜 세월 작업물을 방치한 벌이기도 한 것 같아 최대한 인내하며 작업했습니다.
역시 언제나 그래 왔듯 결과물을 보면 도대체 언제 힘들었나 싶네요.

물론 제 실력이 모자란 탓이 크겠지만, 이렇게 바닐라로만 작업하니 결과물의 규모가 크게 한정된단 느낌이 강하게 듭니다.
사이드 프로젝트로 해보고 싶은 것들이 몇 개 생겼는데 규모가 좀 있는 것들이 있기도 하고, 새로운 도전도 해보고 싶어 아마 이렇게 바닐라로 진행하는 건 당분간 사이드에 사이드로 두지 않을까 싶네요.
물론 그렇다고 공부를 게을리하진 않을 예정이니, 여정을 끝내고 돌아왔을 때 다시금 제게 기분 좋은 고통을 선사해주지 않을까 기대하고 있습니다.

Report an issue