menulogo

슬랙/디스코드 이모지 생성기 제작 프로젝트 회고

@corinthioniaOctober 17, 2024

저는 최근 허밍버즈 🐳 에 단기간 합류하여 슬랙 이모지 생성기를 개발했습니다.


허밍버즈는 조직문화 개선을 위한 서비스를 제공하는 스타트업으로, 이번 프로젝트는 허밍버즈의 서비스에 추가될 슬랙/디스코드 이모지 생성기를 개발하는 것이었습니다. 프로젝트의 요구사항은 사용자가 텍스트를 입력하면 해당 텍스트에 애니메이션을 적용하고, 이를 gif로 변환하는 것이었습니다. 애니메이션을 다룰 뿐만 아니라 이를 gif로 변환해야 하는 작업이 다른 프로젝트에서 진행할 수 없는 새로운 도전이라 느꼈습니다.

이번 프로젝트를 진행하면서 역시나 새롭게 도전해 본 기술과 경험이 굉장히 많았습니다. 여러 시행 착오 끝에 무사히 프로젝트를 마칠 수 있었습니다. 프로젝트의 전체적인 과정을 회고하며 기록해 보겠습니다.


0. 기술 스택 선정

개발을 본격적으로 시작하기에 앞서 프로젝트를 위해 필요한 기술 스택이 무엇인지 생각해 보았습니다.

이모지 생성기는 단일 페이지로 구성되었기도 하고, Next.js 특성상 EC2를 이용하여 배포하는 것은 관리 포인트를 하나 더 증가시키는 일이라 생각하여 정적 페이지로 만들어 프로젝트를 배포하기로 결정했습니다. 지금 보고 계시는 이 블로그도 Next.js를 이용하여 정적 페이지 형태로 배포했기 때문에 정적 배포를 위한 설정 단계는 금방 끝낼 수 있었습니다.


그 다음 가장 중요한 문제는 입력한 텍스트에 애니메이션 효과를 주고, gif로 변환하는 것이었습니다.
가장 처음에 들었던 생각은 CSS로 애니메이션을 구현한 후, 서드파티 라이브러리를 이용하여 gif로 변환하면 될 것이라 생각했습니다. gif 생성을 위한 라이브러리로는 gif.js 가 있습니다. 다만, 2013년에 제작된 라이브러리이고 마지막 업데이트가 5년 전인 것을 보아 해당 라이브러리를 사용하는 것에 대해 많은 고민을 했지만, 여전히 높은 사용률을 보이고 있어 해당 라이브러리를 채택했습니다.


글자/이미지를 이용한 애니메이션을 구현하기 위해서는 단순 CSS를 넘어 Canvas API를 사용해야 합니다.
html2canvas 라는 HTML DOM을 Canvas로 손쉽게 변환해 주는 라이브러리도 존재하지만, 비교적 간단한 수준의 Canvas만 다루기도 하고 이참에 공부해 볼겸 Canvas API를 사용했습니다. Canvas에 대한 학습은 MDN Web Docs를 이용했고, 단계별로 튜토리얼이 잘 나와 있어 재미있게 공부하고 적용할 수 있었습니다.


1. Canvas API 사용하기

프로젝트의 목표는 입력한 텍스트에 Scale, Bounce, Slide, Shake, Roll과 같은 다양한 애니메이션을 적용하는 것입니다. 이를 위해 Canvas API를 사용하여 텍스트에 애니메이션을 적용하고 gif로 변환하는 작업을 진행했습니다. Canvas API는 HTML5에서 추가된 기능으로, JavaScript를 이용하여 2D 그래픽을 그리고나 픽셀 단위의 세밀한 조작이 필요한 작업에 사용됩니다.

애니메이션을 구현하기에 앞서, 텍스트를 canvas 내 원하는 위치에 그리는 함수를 구현했습니다.

이해를 돕기 위해 drawOnCanvas라는 간단한 함수로 표현해 보았습니다. drawOnCanvas는 canvas에 표시할 텍스트와 canvas 내에서 텍스트를 표시할 위치에 해당하는 x, y 좌표값을 인자로 받아 canvas에 텍스트를 그립니다. dxdy는 텍스트의 이동 방향을 나타내는 값이고, 기본값은 0으로 설정했습니다.

const drawOnCanvas = ({ text, x, y, dx = 0, dy = 0 }) => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillText(text, x, y);

  return { x: x + dx, y: y + dy };
};

drawOnCanvas 함수는 textx, y 좌표에 그리고, 다음 위치 좌표값을 반환합니다. drawOnCanvas는 단순히 한 프레임, 즉 정적 이미지를 그리는 함수이기 때문에, drawOnCanvas가 리턴하는 다음 위치 좌표값과 setInterval을 이용하여 일정 시간 간격으로 텍스트를 이동시키는 함수를 구현하기 위함입니다.

2. 애니메이션 구현하기

2-1) 애니메이션을 구현하는 방법

요소에 애니메이션을 적용하는 방법으로는 CSS를 이용한 방법과 JavaScript를 통해 직접 애니메이션을 구현하는 방법이 있습니다. 두 방법의 차이점을 간단히 요약하자면 CSS로 구현한 애니메이션은 브라우저의 GPU를 이용하여 빠르게 처리되지만, JavaScript로 구현한 애니메이션은 더 다양한 효과를 적용할 수 있다는 장점이 있습니다.

이번 프로젝트에서는 사용자가 입력한 텍스트에 다양하고 세밀한 애니메이션 효과를 적용해야 했기 때문에 CSS를 이용하는 방법보다는 JavaScript를 이용하여 직접 애니메이션을 구현하는 방법을 선택했습니다.


2-2) 애니메이션 효과를 구현하기

애니메이션은 여러 장의 이미지(프레임)를 연속으로 이어붙여 움직임을 표현합니다. 따라서, 각 애니메이션별로 요소의 이동방향이나 크기를 계산하는 함수를 구현하고, 이를 프레임 단위로 나누어 Canvas에 그리는 작업을 통해 애니메이션을 구현할 수 있겠다고 생각했습니다.


먼저, 여러 애니메이션 효과 중 가장 쉬워 보이는 Slide 애니메이션을 구현했습니다. Slide 애니메이션은 아래 이미지처럼 텍스트가 단순히 오른쪽에서 왼쪽으로 이동하는 효과를 나타냅니다.


그리고 텍스트를 왼쪽으로 dx 만큼 이동시키는 slideAnimation 함수를 구현합니다. 함수 내부에서 호출되는 drawOnCanvas를 통해 텍스트가 화면에 표시되며, 함수가 리턴하는 다음 좌표값을 바탕으로 반복해서 텍스트를 화면에 표시합니다. 이 과정은 setInterval을 이용하여 일정 시간 간격으로 반복되며, 텍스트가 화면을 벗어나면 위치를 초기화하는 로직을 추가했습니다.

const text = '아기고래';

const slideAnimation = ({ text, dx = 3, interval = 16 }) => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  let x = 0;
  const y = canvas.height / 2;

  const animationInterval = setInterval(() => {
    const nextPosition = drawOnCanvas({ text, x, y, dx });
    x = nextPosition.x;

    if (x > canvas.width) {
      x = -ctx.measureText(text).width;
    }
  }, interval);

  return animationInterval;
};

다시 한번 말씀드리지만 실제 코드가 아닌, 이해를 돕기 위해 간략히 작성한 코드입니다. 😅

나머지 애니메이션도 이와 같은 방식으로 한 프레임에서 다음 프레임으로 넘어갈 때 텍스트의 크기나 위치를 계산하는 함수를 구현하고, setInterval을 이용하여 애니메이션을 구현했습니다. 추후에는 setInterval 부분을 requestAnimationFrame을 변경했는데, 이에 대한 내용은 아래에서 자세히 다뤄 보겠습니다.


3. 애니메이션 최적화하기

그런데, setInterval을 이용하여 위와 같은 애니메이션을 구현하면, 움직임이 부드럽게 보이지 않는 문제가 발생합니다. setInterval은 일정 시간 간격으로 코드를 실행하는 함수이기 때문에, 브라우저의 성능에 따라 애니메이션의 부드러움이 달라질 수 있기 때문입니다.

이때 setInterval 대신 requestAnimationFrame을 사용하면 애니메이션 효과를 최적화할 수 있습니다. requestAnimationFrame은 브라우저의 성능에 따라 최적화된 프레임을 제공하여 부드러운 애니메이션을 표현합니다.

다만 setInterval을 이용할 때에는 시간 간격을 원하는 만큼 설정할 수 있었지만, requestAnimationFrame을 이용할 때에는 시간 간격을 직접 설정할 수는 없습니다. 따라서 requestAnimationFrame을 이용할 때에는 애니메이션의 진행 상태를 나타내는 변수를 이용하여 애니메이션의 진행 상태를 제어해야 합니다.


4. gif로 변환하기

텍스트에 애니메이션을 적용한 후 gif로 변환하여 사용자가 다운로드 받을 수 있게 구현해야 합니다.
Canvas를 캡처하여 gif로 변환하기 위해 gif.js 라이브러리를 사용했습니다. gif.js는 내부적으로 웹 워커를 사용하여 개발되었기 때문에 gif 이미지를 생성하는 동안 브라우저가 멈추는 현상을 방지할 수 있습니다. 👍🏻

gif.js를 이용하여 Canvas에 그려진 이미지를 gif로 변환하는 코드는 아래와 같습니다.

const gif = new GIF({
  workers: 2,
  quality: 10,
  workerScript: '/gif.worker.js',
});

const captureFrame = () => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);

  gif.addFrame(frame, { delay: 1000 / 60 });
};

gif.addFrame 메서드는 각 프레임을 캡처하는 메서드이고, 반복문 등을 실행하여 gif에 여러 프레임을 추가합니다. 그후 gif.render 메서드를 이용하면 gif 이미지를 생성할 수 있습니다. 즉, gif.js를 이용해서 gif로 변환하려면 애니메이션의 시작 프레임부터 끝 프레임까지 모두를 화면에 렌더링하고 이를 캡처해야 합니다.

gif로 변환하는 작업은 무거운 작업이기 때문에 사용자가 '다운로드' 버튼을 클릭할 때 변환 작업을 수행하도록 구현했습니다. 그런데 만약 모든 프레임이 렌더링 되기 전에 사용자가 다운로드 버튼을 클릭하면, 애니메이션이 온전히 적용되지 않은 gif 이미지가 다운로드되는 문제가 발생합니다.


이를 해결하기 위해 DOM에는 존재하지만 화면에 표시되지 않는 canvas 엘리먼트를 추가하고 실제 애니메이션 속도와는 별개로 모든 프레임을 렌더링 하는 방식을 생각했습니다. 즉, 오로지 gif 변환을 위해 사용되는 Canvas를 추가하여 사용자가 다운로드 버튼을 클릭하면 시작 프레임부터 끝 프레임까지의 프레임 수만큼 반복문을 돌리고, 반복문 내부에서 gif.addFrame 메서드로 프레임을 캡처하는 방식을 생각했습니다.


간략한 회고

사실 애니메이션 구현 부분 외에도 이번 프로젝트에서 새롭게 도전한 기술과 경험이 많았습니다.

AWS Lambda를 이용하여 서버리스 함수를 작성하고, S3와 DynamoDB를 이용하여 이미지 업로드 및 다운로드 기능을 구현하기도 했습니다. 이에 대해서는 다른 글로 작성하는 게 좋을 것 같아 따로 정리해야겠습니다.

단순 프로젝트가 아니라, 하나의 프로덕트를 처음부터 끝까지 혼자서 개발해 보는 경험은 이번이 처음이었습니다. 기술 스택 선정부터 구현 방식까지 자유도가 높다는 장점은 존재했지만, 제가 사용해 본 적 없는 다양한 기술들을 주어진 시간 내에 공부하고 비교하며 적용해야 한다는 부담감도 느끼기도 했습니다.

그렇지만 하나의 기능을 구현할 때에도 그곳에 필요한 다양한 기술들과 개념들에 대해 깊게 고민하고 학습할 수 있었습니다. 예를 들어, 이전에는 CORS 문제가 발생하면 단순히 Access-Control-Allow-Origin 헤더를 추가하면 해결된다고 생각했지만, 이번 프로젝트를 통해 AWS Lambda 함수를 직접 구성하고 이를 사용할 때 발생하는 CORS 문제를 해결하면서 근본적으로 발생하는 원인과 해결 방법에 대해 공부하고 적용할 수 있었습니다.

또한, 이번에 새로운 기술들을 많이 접하고 적용해 보면서 새로운 기술을 학습할 때 어떻게 하면 효율적으로 학습할 수 있을지 저에게 맞는 방식을 찾을 수 있었습니다. 이러한 경험들은 앞으로의 프로젝트나 업무에도 큰 도움이 될 것이라 생각합니다.


← 이전 글CORS에 대해 알아보기