Next.js와 Fabric.js를 사용한 City Boy 밈 생성기 만들기 (워터마크 없음!)

발행: (2026년 1월 17일 오후 07:11 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

저는 최근에 “City Boy” 밈 포맷을 위한 무료 온라인 밈 생성기를 만들었으며, 올바른 캔버스 라이브러리를 선택하는 것부터 까다로운 React 재렌더링 문제를 해결하는 것까지 기술적인 여정을 공유하고 싶습니다.

🔗 Live Demo: https://cityboymeme.com

대부분의 밈 생성기 문제점

  • 다운로드 이미지에 귀찮은 워터마크
  • 다운로드 전에 강제 회원가입
  • 투박하고 반응이 느린 인터페이스
  • 광고가 과다한 경험

나는 더 나은 것을 만들고 싶었다: 빠르고, 무료이며, 타협이 전혀 없는.

항목선택
프레임워크Next.js 16 (App Router)
UI 라이브러리React 19
언어TypeScript
캔버스 조작Fabric.js 5.3
스타일링Tailwind CSS 4
아이콘React Icons
✅ 회원가입 불필요

왜 Fabric.js인가?

처음에는 일반 Canvas API나 html2canvas를 고려했지만, 여러 이유로 Fabric.js를 선택했습니다:

// With Fabric.js, object manipulation is incredibly simple
const text = new fabric.Text('City Boys Be Like', {
  left: 300,
  top: 300,
  fontSize: 48,
  fill: '#FFFFFF',
  stroke: '#000000',
  strokeWidth: 3,
});

canvas.add(text);
canvas.setActiveObject(text); // Instantly interactive!

장점

  • 내장된 객체 조작 기능 (드래그, 크기 조정, 회전)
  • 향상된 텍스트 렌더링 품질
  • 즉시 사용할 수 있는 이벤트 처리
  • 보다 쉬운 상태 관리

가장 답답한 버그

사용자가 어디든 클릭하거나 텍스트를 업데이트할 때마다 전체 캔버스가 깜박이며 초기화되었습니다.

❌ 나쁜 구현 (매 렌더마다 캔버스를 재초기화)

const handleTextSelect = (id: string) => {
  setSelectedTextId(id);
};

useEffect(() => {
  // Canvas gets destroyed and recreated!
  const canvas = new fabric.Canvas(canvasRef.current);
  return () => canvas.dispose();
}, [onTextSelect]); // This dependency changes every render!

✅ 좋은 구현 (안정적인 콜백)

// Stable function reference
const handleTextSelect = useCallback((id: string | null) => {
  setSelectedTextId(id);
}, []);

const handleCanvasReady = useCallback(() => {
  setCanvasReady(true);
}, []);

// Only initialize canvas once
useEffect(() => {
  const canvas = new fabric.Canvas(canvasRef.current);
  // ...setup code
  return () => canvas.dispose();
}, []); // Empty dependencies – runs once!

핵심 교훈: React에서 콜백을 컴포넌트에 전달할 때(특히 캔버스 라이브러리 사용 시) 불필요한 재렌더링을 방지하려면 항상 useCallback을 사용하세요.

지속적인 텍스트 스타일 수정

텍스트 스타일(윤곽선 → 채움 → 굵게)을 전환할 때, 이전 스타일 속성이 제대로 초기화되지 않았습니다.

❌ 나쁨: 이전 스타일이 남아 있음

if (updates.style !== undefined) {
  const style = getTextStyle(updatedElement);
  textObj.set(style);
}

✅ 좋음: 먼저 이전 스타일을 초기화

if (updates.style !== undefined) {
  // Clear all style‑related properties first
  textObj.set({
    stroke: undefined,
    strokeWidth: 0,
    backgroundColor: '',
    padding: 0,
  });

  const style = getTextStyle(updatedElement as TextElement);
  const { originX, originY, ...styleOnly } = style;
  textObj.set(styleOnly);
}

SEO 및 검색 가능성

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "City Boy Meme Generator - Free Online Meme Maker",
    template: "%s | City Boy Meme",
  },
  description: "Create hilarious City Boy memes instantly...",
  alternates: {
    canonical: "https://cityboymeme.com/",
  },
  openGraph: {
    type: "website",
    locale: "en_US",
    url: "https://cityboymeme.com",
    siteName: "City Boy Meme Generator",
    images: [
      {
        url: "https://cityboymeme.com/logo.png",
        width: 1200,
        height: 630,
      },
    ],
  },
};
// app/sitemap.ts
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://cityboymeme.com",
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1,
    },
  ];
}
# public/robots.txt
User-agent: *
Allow: /

Sitemap: https://cityboymeme.com/sitemap.xml

파비콘 생성 (macOS sips)

sips -z 16 16 logo.png --out favicon-16.png
sips -z 32 32 logo.png --out favicon-32.png
sips -z 180 180 logo.png --out apple-icon.png
sips -z 192 192 logo.png --out icon-192.png
sips -z 512 512 logo.png --out icon-512.png

콘텐츠 전략

  • 목표 키워드 밀도: 3‑5%
  • 전체 단어 수: 800+ 단어
  • 키워드: “City Boy meme”, “meme generator”, “free meme maker”
  • 구현: FAQ, 기능, 소개 섹션 전반에 걸쳐 자연스러운 언어 사용

Architecture Overview

전체 앱은 백엔드 없이 브라우저에서 실행됩니다.

Advantages

  • 즉시 배포 (Vercel / Netlify)
  • 서버 비용 없음
  • 데이터베이스 불필요
  • 프라이버시 우선 (데이터 수집 없음)
  • 사용자에게 더 빠름 (API 호출 없음)

Trade‑offs

  • 클라우드에 밈을 저장할 수 없음
  • 사용자 계정 없음
  • 제한된 분석 기능

밈 생성기에서는 이러한 선택이 적합합니다. 사용자는 속도와 프라이버시를 원합니다.

Project Structure

app/
├── layout.tsx          # SEO metadata, fonts
├── page.tsx            # Main entry point
├── globals.css         # Global styles
└── sitemap.ts         # Dynamic sitemap

components/
├── MemeEditor.tsx      # Main editor component
└── FabricCanvas.tsx   # Canvas abstraction

public/
├── logo.png           # Source image
├── favicon.ico        # Multiple sizes
└── robots.txt         # SEO
import Image from 'next/image';
;
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap", // Prevent layout shift
});
// Export canvas as Data URL for download
export const exportAsDataURL = () => {
  canvas.discardActiveObject();
  canvas.renderAll();
  return canvas.toDataURL({
    format: "png",
    quality: 1,
    multiplier: 2, // 2× resolution for crisp downloads
  });
};

Canvas + React: 호환성 문제

  • React는 모든 것을 상태(state)로 제어하려고 합니다.
  • Canvas는 DOM을 직접 조작합니다.
  • 재렌더링 시 캔버스 상태가 파괴될 수 있습니다.

해결책: 캔버스를 “제어되지 않는 컴포넌트(uncontrolled component)” 로 취급하고, refs와 콜백을 통해 관리합니다.

// Example: initializing Fabric canvas only once
useEffect(() => {
  const canvas = new fabric.Canvas(canvasRef.current);
  // ...setup (add listeners, objects, etc.)
  return () => canvas.dispose();
}, []); // runs once

Fabric.js는 타입스크립트 정의를 제공하여 이 통합을 타입‑안전하고 개발자 친화적으로 만들어 줍니다.

커스텀 인터페이스

export interface TextElement {
  id: string
  text: string
  color: string
  font: string
  style: 'bold' | 'filled' | 'outlined'
  size: number
}

export interface FabricCanvasRef {
  addText: (element: TextElement) => void
  updateText: (id: string, updates: Partial) => void
  removeText: (id: string) => void
  exportAsDataURL: () => string
}

SEO 최적화 (SPA에서도)

  • Proper meta tags
  • Structured data (JSON‑LD)
  • Semantic HTML
  • Keyword optimisation
  • Fast loading times

초기 기능 위시리스트

  • 이미지 업로드
  • 스티커 라이브러리
  • 고급 필터
  • 사용자 계정

Realisation: 단순함이 핵심 기능입니다. 사용자는 텍스트를 추가하고 다운로드하기만 원합니다. 완료.

고려 중인 아이디어

  • Template Gallery: 더 많은 밈 템플릿 추가
  • Quick Templates: 미리 만든 텍스트 레이아웃
  • Color Picker: 프리셋을 넘어선 사용자 정의 색상
  • Export Formats: JPEG, WebP 옵션
  • History / Undo: 캔버스 상태 관리
  • PWA: 오프라인 지원

Deployment (Vercel – Zero Config)

npm run build   # Build output
# Push to GitHub
# Vercel auto‑deploys

Build output includes:

  • Static HTML / CSS / JS
  • Optimised images
  • Automatic sitemap generation
  • Edge CDN distribution

Lessons Learned

  1. Start simple: 핵심 기능을 먼저 만들고, 나중에 복잡성을 추가하세요.
  2. Choose the right tools: Fabric.js 덕분에 개발 기간을 몇 주 단축할 수 있었습니다.
  3. Test on real devices: 모바일 경험이 중요합니다.
  4. SEO from day one: 사후 생각으로 여기지 마세요.
  5. Performance matters: 사용자는 즉각적인 피드백을 기대합니다.
  6. No ads, no BS: 때때로 최고의 수익 모델은 전혀 없을 때입니다.

🚀 Live Demo: https://cityboymeme.com
전체 프로젝트가 브라우저에서 실행됩니다 – 회원가입 없이, 워터마크 없이, 완전히 무료입니다.

내가 얻은 것

  • React 성능 최적화
  • 최신 프레임워크에서의 Canvas 조작
  • 싱글 페이지 앱을 위한 SEO
  • 단순함의 가치

비슷한 것을 만들고 있다면, 제가 겪은 함정을 피하는 데 도움이 되길 바랍니다!

질문? 댓글? 아래에 남겨 주세요 – 구현의 어떤 부분이든 기꺼이 논의하겠습니다.

도움이 되었나요? ❤️를 눌러 동료 개발자와 공유해 주세요!

Tags: #nextjs #react #javascript #webdev #typescript #canvas #seo #tutorial

Back to Blog

관련 글

더 보기 »

오픈소스 개발자 포트폴리오

깨끗하고 프로덕션 준비가 된 Next.js 포트폴리오 오픈소스로, 자체 개발자 사이트를 만들 때 참고용으로 사용할 수 있습니다. 개요: 개발자를 위한 사이트를 구축하고 있다면...