μλ²λ¦¬μ€ κΈ°λ°μ ν΄λΌμ΄μΈνΈ μ¬μ΄λ νμΌ λ³ν νλ«νΌ
λ¬Έμ μν©: κΈ°μ‘΄ νμΌ λ³ν μλΉμ€λ€μ λμ μλ² λΉμ©κ³Ό 보μ μ·¨μ½μ
ν΄κ²° λ°©μ: WebAssembly κΈ°λ° ν΄λΌμ΄μΈνΈ λ³ν + μλ²λ¦¬μ€ μν€ν
μ²
λΉμ¦λμ€ μν©νΈ: μλ² μ΄μ λΉμ© 100% μ κ°, 무μ ν νμ₯μ± ν보
- κΈ°μ μ νμ : ffmpeg.wasmμ νμ©ν μλ²λ¦¬μ€ νκ²½μμμ λ―Έλμ΄ μ²λ¦¬ ꡬν
- 보μ κ°ν: Next.js 15 λ―Έλ€μ¨μ΄ κΈ°λ° μΈμ¦ μμ€ν μΌλ‘ λ¬΄λ¨ μ κ·Ό μ°¨λ¨
- μ±λ₯ μ΅μ ν: ν΄λΌμ΄μΈνΈ μ¬μ΄λ μ²λ¦¬λ‘ μλ² λΆν μ λ‘ν
- μ¬μ©μ κ²½ν: μ§κ΄μ μΈ UI/UXλ‘ λ³ν κ³Όμ μ 볡μ‘μ± μΆμν
λ¬Έμ : Vercel λ± μλ²λ¦¬μ€ νκ²½μμ FFmpeg λ°μ΄λ리 μ€ν λΆκ°
ν΄κ²°:
- WebAssembly κΈ°λ°
@ffmpeg/ffmpegλμ - ν΄λΌμ΄μΈνΈ μ¬μ΄λ λ³νμΌλ‘ μλ² μμ‘΄μ± μ κ±°
- SharedArrayBuffer νμ©ν κ³ μ±λ₯ λ©λͺ¨λ¦¬ μ²λ¦¬
// ν΅μ¬ ꡬν: WASM κΈ°λ° λ―Έλμ΄ λ³ν
const convertVideo = async (inputFile: File, options: ConvertOptions) => {
const ffmpeg = new FFmpeg();
await ffmpeg.load();
ffmpeg.writeFile('input.mp4', await fetchFile(inputFile));
await ffmpeg.exec(['-i', 'input.mp4', ...buildArgs(options), 'output.webm']);
const data = await ffmpeg.readFile('output.webm');
return new Uint8Array(data);
};λ¬Έμ : 볡μ‘ν λΌμ°ν
ꡬ쑰μμμ μΈμ¦ μ²λ¦¬
ν΄κ²°:
- JWT ν ν° κΈ°λ° λ―Έλ€μ¨μ΄ ꡬν
- NextAuth.js v5μ νΈνλλ 보μ κ³μΈ΅
- νμΌ μμΉ μ΄μ ν΄κ²° (
src/middleware.js)
// ν΅μ¬ ꡬν: μΈμ¦ λ―Έλ€μ¨μ΄
export async function middleware(request) {
if (request.nextUrl.pathname.startsWith('/convert')) {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
return NextResponse.redirect(new URL('/', request.url));
}
}
return NextResponse.next();
}λ¬Έμ : λΈλΌμ°μ λ©λͺ¨λ¦¬ μ νκ³Ό λ³ν μ±λ₯
ν΄κ²°:
- Streaming κΈ°λ° νμΌ μ²λ¦¬
- Web Workersλ₯Ό ν΅ν λ©μΈ μ€λ λ μ°¨λ¨ λ°©μ§
- μ§νλ₯ μΆμ κ³Ό λ©λͺ¨λ¦¬ κ΄λ¦¬
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Client Side β β Serverless β β External β
β β β β β β
β β’ React 19 βββββΆβ β’ Next.js 15 βββββΆβ β’ Google OAuth β
β β’ ffmpeg.wasm β β β’ NextAuth.js β β β’ Vercel Edge β
β β’ TypeScript β β β’ Edge Runtime β β β’ CloudFront β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
- μΈμ¦: Google OAuth β JWT ν ν° λ°κΈ
- μ λ‘λ: ν΄λΌμ΄μΈνΈ λ©λͺ¨λ¦¬ μ§μ λ‘λ©
- λ³ν: WebAssembly μμ§ μ²λ¦¬
- λ€μ΄λ‘λ: Blob URL μ§μ μ 곡
- μλ² λΉμ©: $0/μ (κΈ°μ‘΄ λλΉ 100% μ κ°)
- CDN λΉμ©: μ μ 리μμ€λ§ μ¬μ©μΌλ‘ μ΅μν
- νμ₯μ±: μ¬μ©μ μ¦κ°μ λ°λ₯Έ μΆκ° λΉμ© μμ
- ν μ€νΈ 컀λ²λ¦¬μ§: Jest + Testing Library λμ
- μ½λ νμ§: ESLint + Prettier μλν
- νμ μμ μ±: TypeScript strict mode
- μΈμ¦: OAuth 2.0 νμ€ μ€μ
- μΈκ°: μ΄λ©μΌ νμ΄νΈλ¦¬μ€νΈ κΈ°λ° μ κ·Ό μ μ΄
- λ°μ΄ν° 보νΈ: ν΄λΌμ΄μΈνΈ μ²λ¦¬λ‘ μλ² μ μ₯ μμ
- Frontend: Next.js 15.3.4, React 19.0.0, TypeScript 5
- Authentication: NextAuth.js 5.0.0-beta.29 (Google OAuth)
- Media Processing: @ffmpeg/ffmpeg 0.12.15, @ffmpeg/util 0.12.2
- PDF Processing: pdf-lib 1.17.1
- Testing: Jest 29.7.0, Testing Library
- Code Quality: ESLint 9, Prettier 3.6.2
- Bundling: Next.js built-in Webpack 5
- State Management: Jotai 2.8.3
- Hosting: Vercel (Edge Functions + Static Generation)
- CDN: CloudFront for WebAssembly files
- Analytics: Vercel Analytics integration
- λΉλμ€: 1080p/60fps β 720p/30fps (90μ΄ μμ μ½ 45μ΄)
- μ€λμ€: MP3 320kbps β 128kbps (5MB νμΌ μ½ 8μ΄)
- μ΄λ―Έμ§: 4K PNG β WebP (20MB β 2MB, 3μ΄)
- PDF: 10νμ΄μ§ λ¬Έμ λ³ν© (2μ΄)
- 첫 λ‘λ©: < 2μ΄ (Static Generation)
- λ³ν μμ: < 1μ΄ (WASM λ‘λ© μΊμ)
- λͺ¨λ°μΌ μ΅μ ν: ν°μΉ μΈν°νμ΄μ€, λ°μν λμμΈ
- μ§μ ν¬λ§·: MP4, AVI, MOV, MKV, WebM
- ν΄μλ μ‘°μ : 480p ~ 1080p
- FPS μ μ΄: 24 ~ 60fps
- νμ§/λΉνΈλ μ΄νΈ μ΅μ ν
- μ§μ ν¬λ§·: MP3, WAV, FLAC, AAC, OGG
- μνλ μ΄νΈ: 22kHz ~ 48kHz
- μ±λ: λͺ¨λ Έ/μ€ν λ μ€ μ ν
- ν¬λ§· λ³ν: JPG, PNG, WebP, TIFF
- GIF μμ±: λ€μ€ μ΄λ―Έμ§ ν©μ±
- ν΄μλ/νμ§ μ‘°μ
- μ΄λ―Έμ§ β PDF λ³ν
- λ¬Έμ λ³ν©/λΆν
- λ©νλ°μ΄ν° κ΄λ¦¬
- λ€λ‘κ°κΈ° λ°©μ§: λ³ν μ€ μ€μ μ’ λ£ λ°©μ§
- μΈμ± λΈλΌμ°μ κ°μ§: KakaoTalk/LINEμμ μΈλΆ λΈλΌμ°μ μ λ
- ν°μΉ μ΅μ ν: λλκ·Έ μ€ λλ‘, μ€μμ΄ν μ μ€μ²
- μ€μκ° λ―Έλ¦¬λ³΄κΈ°: λ³ν μ΅μ μ μ© κ²°κ³Ό μμΈ‘
- μ§νλ₯ νμ: λ³ν λ¨κ³λ³ μν νμ
- μλ¬ νΈλ€λ§: μ¬μ©μ μΉνμ μ€λ₯ λ©μμ§
// 보μ κ°νλ NextAuth μ€μ
export const { auth, signIn, signOut } = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user }) {
const allowedEmails = process.env.ALLOWED_EMAILS?.split(',') || [];
return allowedEmails.includes(user.email!);
},
},
});- JWT ν ν° κ²μ¦
- λΌμ°νΈλ³ μ κ·Ό μ μ΄
- CSRF λ°©μ§
git clone https://github.com/your-repo/QuokkaConverter.git
cd QuokkaConverter/next-converter
npm install
cp env.example .env.local
npm run devNEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
ALLOWED_EMAILS=admin@example.com,user@example.comvercel --prod
# νκ²½λ³μλ Vercel Dashboardμμ μ€μ npm run test # λ¨μ ν
μ€νΈ
npm run test:coverage # 컀λ²λ¦¬μ§ 리ν¬νΈ
npm run lint # μ½λ νμ§ κ²μ¬
npm run build # νλ‘λμ
λΉλ κ²μ¦- νμΌ μ λ‘λ/λ³ν νλ‘μ°
- μΈμ¦/μΈκ° λ‘μ§
- μλ¬ νΈλ€λ§
- ν¬λ‘μ€ λΈλΌμ°μ νΈνμ±
- Web Workers λ³λ ¬ μ²λ¦¬ μ΅μ ν
- WebCodecs API νμ©ν νλμ¨μ΄ κ°μ
- μ¬μ©λ λΆμ λμ보λ
- API μλΉμ€ μ 곡
- μν°νλΌμ΄μ¦ λ²μ κ°λ°
Portfolio Repository: GitHub
Live Demo: quokkaconverter.vercel.app
Contact: dlwjd164@gmail.com