velog를 사용하는 여러분은 썸네일용 배너를 어떻게 만들고 계신가요?
저는 매번 배너 만들기가 귀찮아서(...) 간단한 배너 생성기를 만들었습니다.

React로 스태틱 배너 생성기를 만들면서 삽질했던 내용을 기록해 보았습니다.
필수적인 기능만 빠르게 구현했기 때문에 나머지는 내일의 내가 처리해줄 거예요...🙈

create-react-app

create-react-app v2로 새로운 프로젝트를 생성했습니다.

create-react-app banner-maker

State

최소한으로 필요한 정보는 배너의 배경 색상, 입력한 텍스트, 다운로드할 이미지의 데이터 링크입니다.
간단한 내용이므로 Redux등을 사용하지 않고 App.js에서만 상태 관리를 하겠습니다.

  // App.js
  state = {
    color: "#ccc",
    text: "Sample Text",
    href: ""
  };

Color Picker

배너 색을 고르기 위한 컬러 피커는 react-color 라이브러리를 사용했습니다.
image.png

깔끔해 보이는 Circle을 기본으로 정하고, 좀 더 자유롭게 색상을 선택할 수 있도록 아래에 HuePicker도 추가합니다. 배경 색상만 먼저 지정할 수 있게 하고 나중에 상태를 추가해서 글자 색 변경과, hex값을 입력할 수 있게 할 것입니다.

    // App.js
    state = {
        color: "#ccc"
    }
    handleChange = color => {
        this.setState({ color: color.hex });
      };

    ...

    <Palette color={color} onChange={this.handleChange} />
    // Palette.js
    import { SketchPicker } from 'react-color';

    ...

    <div className="paletteWrapper">
        <div className="circlePicker">
            <CirclePicker color={color} onChange={onChange} />
        </div>
        <div className="huePicker">
            <HuePicker color={color} onChange={onChange} />
        </div>
    </div>

hex 값과 현재 선택한 색상을 보여주는 작은 박스 하나도 만들어서 다음과 같이 배치했습니다.

image.png

Text Input

글자 입력을 위해 Input을 처리하는 컴포넌트를 하나 만들어주고, text 라는 state로 관리합니다.

// App.js
handleTextChange = e => {
    this.setState({ text: e.target.value });
  };
...
<TextInput onChange={this.handleTextChange} />
// TextInput.js
 <input className="textInput"
        onChange={this.props.onChange}
        type="text"
        size="40"
        placeholder="Type text here!" />

Canvas

color와 text 값을 얻었으니 이제 이를 canvas에 그려보겠습니다.

React로 canvas를 다루는 것은 DOM을 건드려야 해서 살짝 까다롭게 느껴집니다. 하지만 ref를 이용해 canvas의 context를 얻어 직접 조작할 수 있습니다.

이것이 이미 잘 구현된 라이브러리도 있지만, 간단한 기능만 추가할 것이므로 다른 라이브러리를 사용하지 않고 만들어 봅니다.

먼저 Preview.js 컴포넌트를 생성한 다음 React.createRef() 함수로 canvas에 접근할 수 있는 ref를 얻습니다.

// Preview.js
class Preview extends Component {
  constructor(props) {
    super(props);
    this.canvasRef = React.createRef();
  }

  ...

// render()
 <canvas ref={this.canvasRef} className="previewCanvas" width="700" height="350"/>
}

canvas에 텍스트나 색상을 그리는 과정은 다음과 같이 componentDidUpdate에서 처리했습니다. 나중에 폰트와 글씨 크기도 바꿀 예정이니 setFont라는 함수로 미리 만들어 두었습니다.

// Preview.js
componentDidUpdate() {
    const canvas = this.canvasRef.current;
    const ctx = canvas.getContext("2d");
    const { color, text } = this.props;

    ctx.fillStyle = color;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    this.setFont(canvas, text, {
      color: "white",
      size: "40",
      font: "Arial"
    });
  }

setFont = (canvas, text, args) => {
    const ctx = canvas.getContext("2d");
    const { color, size, font } = args;
    ctx.font = `${size}px ${font}`;
    ctx.fillStyle = color;
    ctx.fillText(text, canvas.width / 2, canvas.height / 2);
  };
// App.js
// render()
<Preview color={color} text={text} href={href} />

이제 글자를 입력하거나 색상이 변경되면 componentDidUpdate에서 canvas가 변경됩니다.

참고
Techniques for Canvas in React
https://reactjs.org/docs/refs-and-the-dom.html

글자 가운데 정렬

글자 사이즈 + 글자 크기에 따라 가운데 위치를 조절 해줘야 합니다.
텍스트 위치를 캔바스 가운데 좌표로 계산하면 (canvas.width/2, canvas.height/2) 글자 시작점은 한 가운데이긴 하지만 그림처럼 약간 어긋나게 됩니다.
image.png

텍스트 크기와 길이로 offset을 계산해야 하나 했는데 context 속성이 있어서 간단히 해결했습니다.

    context.textAlign = "center";    // 가로 가운데 정렬
    context.textBaseline = "middle"; // 세로 가운데 정렬

image.png
여기까지 구현이 되었습니다!

초기 배너 색상을 랜덤하게

초기 배경 색상을 랜덤하게 주고 싶어졌습니다. (갑자기?) 임의의 hex 색상을 뽑고 컴포넌트 마운트 후에 state의 color값을 변경해줍니다. 이제 새로고침할 때마다 배너의 색이 다르게 보입니다.

그런데 가끔 배경 색이 흰색으로 초기화 되는 것 같습니다...기억해 두었다가 고치도록 합시다. 🐒

참고: Random Hex Color Code Generator in JavaScript

    // App.js
    componentDidMount() {
        this.setState({ color: this.getRandomColor() });
      }

    getRandomColor = () => {
        return "#" + Math.floor(Math.random() * 16777215).toString(16);
      };

이미지 다운로드

이미지를 PNG형식으로 다운받는 버튼을 만듭니다.

canvas로부터 받은 데이터의 URL 정보와, 파일 이름을 a태그의 Attribute로 설정하면 하면 다운로드 링크를 클릭해서 이미지로 다운받을 수 있습니다.

참고: 개비스콘, 내팔자야 짤 생성기


    // href에는 이런 캔버스 데이터 URL이 들어갈 거예요.
    // const href = canvas.toDataURL();

    // App.js
    // render()
    <a href={href} className="downbutton" download="sample.png">Download</a>

캔바스 데이터를 다운로드 링크로 만들기

canvas에는 onChange 등의 이벤트가 없습니다. 따라서 componentDidUpdate 함수에서 캔버스에 변화가 일어나게 되면 캔버스 데이터 url로 href를 업데이트하면 됩니다.

그런데 여기서 새로운 url을 얻을 때마다 setState를 해버리면 부모인 App.js에서 일어난 업데이트 때문에 컴포넌트인 Preview.js 가 다시 업데이트 되면서 무한 반복에 빠지게 됩니다.

애초에 canvas 접근 방법이 잘못되었던 건 아닌지 한참 삽질했는데, 간단히 href !== url 조건을 추가하면 무한 업데이트를 방지할 수 있습니다. (Thanks to DoonDoony님)

이제 이미지를 다운로드 받을 데이터 링크 생성이 되었습니다. 다운로드 버튼을 클릭하면 sample.png 라는 이름으로 파일을 저장할 수 있습니다.

// App.js
handleCanvasChange = href => {
    this.setState({ href });
};
// render()
<Preview color={color}
         text={text}
         href={href}
         updateCanvas={this.handleCanvasChange} />
// Preview.js
componentDidUpdate() {
  const canvas = this.canvasRef.current;
  const ctx = canvas.getContext("2d");
  const { color, text, updateCanvas, href } = this.props;

  ctx.fillStyle = color;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  this.setFont(canvas, text, {
    color: "white",
    size: "40",
    font: "Arial"
  });

  const url = canvas.toDataURL();
  href !== url && updateCanvas(url);
}

SASS 설정

react-create-app v2에서는 sass가 지원된다고 하니 이참에 sass도 써보기로 했습니다. sass를 사용하기 위해 App.css 파일을 App.scss로 변경하면 node-sass 모듈을 먼저 설치해야 한다고 나옵니다.

npm install node-sass

sass는 다음에 좀 더 깊이 파기로 하고 필요한 문법 몇개만 적용해보고 넘어갑니다.
font-face로 글꼴을 하나 추가해주고, 다운로드 버튼에 hover할 때 shade를 주는 등...

$primaryColor: salmon;

@font-face {
  font-family: "SF Pro";
  font-weight: 200;
  src: url("fonts/SF-Pro-Display-Light.otf");
}

@function shade($color) {
  @return mix(black, $color, 30%);
}

.downbutton {
  width: 200px;
  height: 50px;
  color: #fff;
  background-color: $primaryColor;
  margin: 5px;
  font-size: 1.3em;
  font-weight: 200;
  text-decoration: none;
  line-height: 50px;
  border-radius: 10px;
  transition: 0.3s;
  &:hover,
  &:visited,
  &:active {
    background-color: shade($primaryColor);
    color: #ccc;
    text-decoration: none;
  }
}
...

기타...

헤더 타이틀과 Google Analytics같은 것도 넣어보고 싶어졌습니다.
<head>에 들어갈 내용을 쉽게 추가하기 위해 Helmet 라이브러리를 설치합니다.

구글 콘솔에 가서 새로운 GA속성을 추가하고 GA추적 코드(gtag.js)를 복사해 둡니다. 타이틀과 gtag.js를 추가하다 보면, 추적 코드에 즉시 실행 함수와 변수가 여러 줄로 포함되어 있으므로 {``}로 감싸줍니다. head 다음에 추가하라고 되어 있는 스크립트는 <Helmet> 다음에 넣어주었습니다.

  <Helmet>
    <meta charSet="utf-8" />
    <title>Banner Maker</title>
    <script> <!-- [ gtag.js ] --> </script>
  </Helmet>

  <script>
    {` 긴 스크립트 태그는 이런 식으로 넣어줍니다.`}
  </script>

필수 사항은 아니라 생략했지만, 글꼴과 글자 크기를 선택할 수 있도록 Ant Design UI를 설치하고 각각 Select 박스를 넣어 추가로 작업해 주었습니다.

끝! 이제 배포만 하면 됩니다.

배포하기

요즘 Github pages 대신에 Netlify를 사용해보고 있습니다. Github 계정으로 로그인 한 후에 저장소와 연결만 하면 바로 스태틱 사이트를 배포할 수 있습니다. 마침 .dev 도메인도 샀겠다, 커스텀 도메인 연결도 해주었습니다.

와우!

banner-maker.gif
최소한의 기능을 하는 앱 하나를 완성했습니다. 만들다보니 배경 이미지나 로고 업로드도 하고 싶고, App에 때려넣었던 코드도 점점 길고 복잡해져서 많은 수정과 개선이 필요할 것 같습니다...🙊