Next.js와 Fabric.js를 사용한 City Boy 밈 생성기 만들기 (워터마크 없음!)
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
- Start simple: 핵심 기능을 먼저 만들고, 나중에 복잡성을 추가하세요.
- Choose the right tools: Fabric.js 덕분에 개발 기간을 몇 주 단축할 수 있었습니다.
- Test on real devices: 모바일 경험이 중요합니다.
- SEO from day one: 사후 생각으로 여기지 마세요.
- Performance matters: 사용자는 즉각적인 피드백을 기대합니다.
- No ads, no BS: 때때로 최고의 수익 모델은 전혀 없을 때입니다.
🚀 Live Demo: https://cityboymeme.com
전체 프로젝트가 브라우저에서 실행됩니다 – 회원가입 없이, 워터마크 없이, 완전히 무료입니다.
내가 얻은 것
- React 성능 최적화
- 최신 프레임워크에서의 Canvas 조작
- 싱글 페이지 앱을 위한 SEO
- 단순함의 가치
비슷한 것을 만들고 있다면, 제가 겪은 함정을 피하는 데 도움이 되길 바랍니다!
질문? 댓글? 아래에 남겨 주세요 – 구현의 어떤 부분이든 기꺼이 논의하겠습니다.
도움이 되었나요? ❤️를 눌러 동료 개발자와 공유해 주세요!
Tags: #nextjs #react #javascript #webdev #typescript #canvas #seo #tutorial