Next.js 14로 무료 명함 생성기를 만들었습니다 - 배운 점
Source: Dev.to

지난 주에 FreeCard.app을 출시했습니다 – 완전히 무료인 명함 및 QR 코드 생성기입니다. 이번 포스트에서는 프로젝트 진행 과정, 기술 선택, 직면한 도전 과제, 그리고 배운 교훈들을 공유하려고 합니다.
🎯 문제
- ❌ 대부분은 디자인을 보기 위해서도 회원가입을 요구합니다
- ❌ “무료” 도구는 내보내기에 워터마크를 추가합니다
- ❌ 프리미엄 기능이 $10‑20 / 월 결제 장벽 뒤에 가려져 있습니다
- ❌ 단순 작업에 비해 인터페이스가 지나치게 복잡합니다
나는 단순히 작동하는 것을 원했다: 이름을 입력하고, 템플릿을 선택하고, 다운로드. 끝.
💡 솔루션
FreeCard.app – 진정으로 무료인 명함 생성기, 특징:
- ✅ 25개 이상의 전문 템플릿
- ✅ 자동 QR 코드 생성 (vCard 형식)
- ✅ 맞춤 색상 및 글꼴
- ✅ PNG, PDF, vCard 내보내기
- ✅ 이메일 서명 생성기
- ✅ 회원가입 불필요
- ✅ 워터마크 전혀 없음
비즈니스 모델? 간단합니다 – Google AdSense. 프리미엄 티어도, 업셀도 없습니다.
🛠️ Tech Stack
| Layer | Technology |
|---|---|
| 프론트엔드 | Next.js 14 (App Router) |
| 언어 | TypeScript |
| 스타일링 | Tailwind CSS + shadcn/ui |
| 상태 관리 | Zustand |
| 데이터베이스 | MongoDB + Mongoose |
| 인증 | NextAuth.js v5 |
| 내보내기 | html-to-image + jsPDF |
| QR | qrcode.react |
| 호스팅 | Vercel |
왜 이 스택을 선택했나요?
- Next.js 14 App Router – 기본적으로 서버 컴포넌트를 사용해 초기 로드가 더 빠릅니다. 새로운 라우터는 프로덕션에 충분히 안정적입니다.
- Zustand over Redux – 이와 같은 도구에서는 Zustand의 단순함이 승리합니다. 보일러플레이트 없이 바로 사용할 수 있습니다:
// store/useCardStore.ts
import { create } from 'zustand';
interface CardStore {
card: CardData;
setField: (field: string, value: string) => void;
setTemplate: (templateId: string) => void;
}
export const useCardStore = create((set) => ({
card: initialCard,
setField: (field, value) =>
set((state) => ({ card: { ...state.card, [field]: value } })),
setTemplate: (templateId) =>
set((state) => ({ card: { ...state.card, template: templateId } })),
}));
- shadcn/ui – 컴포넌트 라이브러리가 아니라 복사‑붙여넣기 가능한 컴포넌트 모음입니다. 완전한 제어권, 훌륭한 기본값, 접근성도 기본 제공됩니다.
🎨 템플릿 시스템
가장 까다로운 부분 중 하나는 유연한 템플릿 시스템을 구축하는 것이었습니다. 각 템플릿은 다음을 만족해야 합니다:
- 동일한 props (이름, 직함, 색상 등)를 받아야 합니다
- 디자인에 따라 다르게 렌더링되어야 합니다
- PNG/PDF 형식으로 내보낼 수 있어야 합니다
간단한 예시:
// templates/ModernDark.tsx
interface TemplateProps {
card: CardData;
showQR?: boolean;
}
export function ModernDark({ card, showQR = true }: TemplateProps) {
const { fullName, title, email, phone, colors } = card;
return (
<div style={{ backgroundColor: colors?.background ?? '#fff' }}>
<h1>{fullName || 'Your Name'}</h1>
<h2>{title || 'Your Title'}</h2>
{/* …more fields… */}
{showQR && <QRCode value={generateVCard(card)} />}
</div>
);
}
📤 내보내기 도전
HTML을 PNG/PDF로 내보내는 것은 실제로 시도해 보기 전까지는 간단해 보입니다. 제가 사용해 본 방법을 공유합니다.
PNG 내보내기
import { toPng } from 'html-to-image';
export async function exportToPNG(elementId: string) {
const element = document.getElementById(elementId);
if (!element) throw new Error('Element not found');
const dataUrl = await toPng(element, {
quality: 1,
pixelRatio: 3, // High resolution
backgroundColor: '#ffffff',
});
// Trigger download
const link = document.createElement('a');
link.download = 'business-card.png';
link.href = dataUrl;
link.click();
}
PDF 내보내기 (표준 명함 크기)
import jsPDF from 'jspdf';
import { toPng } from 'html-to-image';
export async function exportToPDF(elementId: string) {
const element = document.getElementById(elementId);
const dataUrl = await toPng(element, { pixelRatio: 3 });
// Standard business card: 3.5" × 2" (88.9 mm × 50.8 mm)
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: [88.9, 50.8],
});
pdf.addImage(dataUrl, 'PNG', 0, 0, 88.9, 50.8);
pdf.save('business-card.pdf');
}
vCard 생성
QR 코드는 vCard 데이터를 인코딩하므로 사람들은 스캔하여 연락처를 저장할 수 있습니다:
export function generateVCard(card: CardData): string {
return [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${card.fullName}`,
`ORG:${card.company || ''}`,
`TITLE:${card.title || ''}`,
`EMAIL:${card.email || ''}`,
`TEL:${card.phone || ''}`,
`URL:${card.website || ''}`,
'END:VCARD',
].join('\n');
}
🐛 도전 과제 및 해결책
내보내기 시 폰트가 렌더링되지 않음
문제: PNG 내보내기에서 사용자 정의 폰트가 시스템 폰트로 대체되었습니다.
해결책: toPng을 호출하기 전에 폰트가 완전히 로드되었는지 확인합니다:
await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100)); // tiny delay
const dataUrl = await toPng(element);
QR 코드 크기 조정
문제: 고해상도에서 QR 코드가 흐릿하거나 잘려 보였습니다.
해결책: QR 코드를 더 큰 크기(pixelRatio = 3)로 렌더링한 뒤 PDF/PNG 내보내기에서 축소합니다. 이렇게 하면 시각적 영역은 작게 유지하면서 선명한 가장자리를 보존할 수 있습니다.
컬러 피커 성능
문제: 연속적인 색상 변경 시 실시간 미리보기가 지연되었습니다.
해결책: 디바운스된 업데이트.
const debouncedSetColor = useMemo(
() => debounce((color: string) => setColor('primary', color), 50),
[]
);
📊 결과 (첫 주)
- 🚀 Product Hunt에 출시
- 📈 500+ 고유 방문자
- 💾 200+ 개의 카드 생성
- 🔗 BetaList, AlternativeTo에서 백링크
- 💰 $0 수익 (AdSense 승인 대기 중)
💸 비용 내역
| 항목 | 월 비용 |
|---|---|
| Vercel Hosting | $0 (Hobby) |
| MongoDB Atlas | $0 (Free tier) |
| 도메인 (.app) | ~ $1.25 (≈ $15/yr) |
| 총계 | ~ $1.25 |
그게 전부입니다—연간 약 $15/년에 완전한 기능을 갖춘 SaaS와 유사한 제품입니다.
🎓 Lessons Learned
- 빠르게 출시하고 나중에 반복 – 5개의 템플릿으로 시작했으며, 이제는 25개가 되었습니다. 첫 번째 버전은 “충분히 괜찮았다”; 사용자들이 실제로 원하는 것을 알려주었습니다.
- “무료”는 기능이다 – 공격적인 업셀링이 있는 “프리미엄” 도구가 넘쳐나는 시장에서, 진정으로 무료인 것이 차별점입니다.
- Zustand > Redux (소규모 프로젝트용) – 상태가 한 파일에 들어갈 정도라면 Redux가 필요 없습니다. Zustand의 API는 사용하기 즐겁습니다.
- shadcn/ui는 놀랍다 – 복사‑붙여넣기 컴포넌트는 코드를 직접 소유한다는 의미입니다. 라이브러리 추상화와 싸울 필요가 없습니다.
- App Router는 준비되었습니다 – “Pages를 사용할까, App Router를 사용할까?” 라는 고민을 몇 달간 했지만, 그냥 App Router를 사용하세요. 안정적이며 개발자 경험(DX)이 뛰어납니다.
🔮 다음은 무엇인가요
- 더 많은 템플릿 (목표 50+)
- NFC 명함 지원
- 공개 공유 링크 (
freecard.app/c/username) - LinkedIn 프로필 가져오기
- 템플릿 마켓플레이스 (사용자 제출 디자인)
🙏 사용해 보기
명함이 필요하다면 한 번 시도해 보세요:
가입 필요 없고, 워터마크도 없고, 허튼소리도 없습니다. 무료 명함만 제공합니다.
어떻게 생각하시나요? 댓글로 여러분의 피드백을 듣고 싶습니다. 어떤 기능이 더 유용할까요?
이 내용이 도움이 되었다면 다음을 고려해 주세요:
- ⭐ Product Hunt에서 업보팅
- 🐦 더 많은 인디‑해커 콘텐츠를 위해 Twitter에서 팔로우
- 📧 명함이 필요한 사람에게 공유
태그
nextjs typescript react webdev opensource indiehacker buildinpublic sideproject
