6 Hal yang Aku Pelajari Waktu Iseng Bikin Photobooth
Awalnya cuma proyek iseng: bikin web photobooth, pikabooth.web.id. Ternyata sepanjang jalan nemu beberapa hal teknis yang menurutku sayang kalau nggak dicatat — mulai dari infra storage sampai kenapa video call itu nggak sesederhana kelihatannya. Ini catatannya.
1. Setup CDN Cloudflare
Di sini kan aku bikin fitur frame editor. Di mana di situ nanti bisa upload background atau image untuk mempercantik framenya. Jadi, frame akan sering dilihat, yang mana di framenya juga mengandung gambar, jadi aku kepikiran buat kalo naruh CDN di depan bucket storagenya, biar nggak semua request nyampe ke origin. Awalnya karena ngakalin supaya request ke Railway (Hosting gratisan) itu ga banyak-banyak, jadi billingnya ga kurang begitu banyak lah. Makanya aku kasih CDN itu.
Yang paling kerasa manfaatnya: biaya bandwidth ke origin storage turun jauh -- mungkin karena gaada yang pakai ya? HAHAHAHAH. Tapi tapi tapi latency ke user juga lebih baik karena kena edge cache, jadi lebih sat set wat wet.
2. Coba RustFS buat self-host S3, lumayan lebih enak dari MinIO
Awalnya pakai MinIO karena emang lebih sering pakai itu buat ngatur Storage, tapi kali ini mau coba switch ke RustFS . Ya bukan karena apa, karena dia pakai rust aja yang digadang-gadang cepet. Tapi JUJURRR, emang iya cepet, dan memory usagenya dikit bangettt. Terus beberapa hal yang bikin lebih nyaman dipakai:
- Startup lebih cepat, dan konfigurasinya terasa lebih straightforward.
- Tetap compatible sama S3 API standar, jadi SDK/tools yang biasa dipakai (AWS SDK, aws-cli, presigned URL, dll) langsung jalan tanpa perlu adjust banyak.
Catatan: MinIO tetap lebih matang dari sisi ekosistem dan community size, jadi kalau butuh fitur enterprise-grade (replication kompleks, ILM policy lengkap) mungkin masih lebih aman pakai MinIO. Tapi untuk skala proyek kecil-menengah, RustFS lebih dari cukup dan operasionalnya lebih ringan di kepala.
3. Implementasi SWR di Next.js
Buat data yang sering berubah tapi nggak butuh realtime ketat (contoh: list session photobooth, status upload), SWR ("stale-while-revalidate") ternyata pas banget.
Pola dasarnya:
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((res) => res.json())
function SessionList() {
const { data, error, isLoading, mutate } = useSWR('/api/sessions', fetcher, {
revalidateOnFocus: true,
refreshInterval: 5000,
})
if (error) return <div>Gagal load</div>
if (isLoading) return <div>Loading...</div>
return (
<ul>
{data.map((s: any) => (
<li key={s.id}>{s.name}</li>
))}
</ul>
)
}
Yang bikin enak:
- Cache-nya otomatis, jadi navigasi bolak-balik antar halaman nggak perlu fetch ulang dari nol — user langsung lihat data lama (stale) sambil di-refresh di background.
- revalidateOnFocus berguna banget buat kasus kayak "user balik dari tab lain setelah upload foto selesai di background."
- mutate() bikin optimistic update gampang — misal update UI duluan pas user hapus foto, baru sync ke server.
Dibanding fetch manual pakai useEffect + useState, kompleksitas state (loading, error, race condition antar request) jadi jauh lebih sedikit yang perlu di-handle manual.
4. WebRTC dan kenapa butuh TURN server
Ini bagian yang paling banyak "aha moment"-nya. Waktu bikin fitur share video/suara real-time, ternyata nggak cukup cuma connect dua device gitu aja.
TURN server ini nemu, karena pas pertama kali coba pake device dengan network sama sih aman ya. Tapi pas coba deploy dan beda network, hmmm, kok connecting terus, bahkan sampai error connecting. Kenapa NICHHHH!!!. Akhirnya setelah cari-cari, nemu. TERNYATA BUTUH TURN SERVER.
Analoginya kurang lebih begini: dua device yang mau saling kirim data itu ibarat dua orang saling lempar bola. Kalau berdua di lapangan yang sama (satu jaringan lokal), lempar langsung juga nyampe. Tapi kalau posisinya beda kompleks/beda kota, di antara mereka ada banyak "tembok" — ini adalah NAT (Network Address Translation) dan firewall yang hampir semua router rumah/kantor pakai. NAT ini yang bikin device di jaringan privat nggak punya IP publik langsung, jadi nggak bisa langsung "dilempar" dari luar.
Makanya WebRTC butuh beberapa komponen tambahan buat proses yang disebut NAT traversal:
- STUN server — tugasnya cuma ngasih tahu device "dari luar, IP publik & port kamu kelihatan kayak apa." Ini kayak titik yang bantu dua orang saling tahu posisi masing-masing dulu. Kalau ternyata NAT-nya "ramah" (full-cone/restricted), dua device bisa langsung connect peer-to-peer setelah tahu info ini — nggak butuh middleman lagi.
- TURN server — ini yang jadi "middleman" beneran kalau STUN aja nggak cukup, misal karena NAT-nya simetris atau firewall ketat (biasa terjadi di jaringan kantor/kampus/mobile carrier). Semua data video/suara di-relay lewat TURN server ini, jadi bukan lagi peer-to-peer, tapi lewat pihak ketiga yang keduanya bisa akses.
- ICE (Interactive Connectivity Establishment) — ini "orkestrator"-nya: nyoba semua kandidat jalur koneksi yang memungkinkan (direct, lewat STUN, sampai fallback ke TURN), lalu pilih yang paling optimal.
Konsekuensi praktisnya: kalau cuma andalin STUN, video call/kirim data bakal gagal connect di sebagian kasus jaringan (terutama corporate network atau NAT simetris). TURN server itu semacam "jaring pengaman" yang bikin koneksi tetap jalan walau apapun kondisi jaringan user — tapi konsekuensinya, karena semua traffic lewat server itu, butuh bandwidth dan compute server yang lumayan, beda jauh sama koneksi peer-to-peer murni yang server-nya cuma nemenin di awal doang.
Aku pakai https://turnix.io/ buat TURN servernya, karena gratis dan masih coba-coba. Nanti kalo udah gede dan banyak yang pakai, mau coba deploy pakai https://github.com/coturn/coturn sih.
5. Pakai Capacitor buat serve Next.js ke iOS & Android tanpa nulis ulang dari nol
Salah satu pertanyaan yang muncul waktu Pikabooth mulai jalan: gimana caranya punya versi mobile tanpa harus bikin app native (atau bahkan Flutter) dari nol, padahal UI web-nya sendiri udah cukup mateng. Cari-cari-cari, akhirnya dapat jawaban dari Reddit.
Jawabannya Capacitor — native runtime dari tim Ionic yang bisa "membungkus" web app (termasuk Next.js) jadi project iOS dan Android asli, lengkap dengan WebView sebagai container-nya. Beda sama sekadar buka website di WebView biasa, Capacitor ngasih akses ke native API (kamera, filesystem, share sheet, dll) lewat plugin, jadi app-nya tetap kerasa "native" walau UI-nya web.
Poin penting yang perlu diperhatikan buat kasus Next.js:
- Karena Capacitor butuh static asset yang di-serve dari WebView lokal (bukan lewat server Next.js yang jalan di Node), App Router harus di-export sebagai static export (output: 'export' di next.config.js). Konsekuensinya: fitur yang butuh server runtime seperti API Routes, Server Actions, atau middleware nggak bisa jalan langsung di dalam build mobile — harus dipindah ke backend terpisah yang diakses via fetch biasa.
- Setelah next build dengan static export, hasil di folder out/ itu yang di-point sebagai webDir di capacitor.config.ts, lalu npx cap sync buat nge-push perubahan ke project native iOS/Android.
- Untuk fitur yang butuh akses native (misal ambil foto langsung dari kamera device, bukan dari <input type="file"> browser), tinggal pasang plugin resmi kayak @capacitor/camera atau @capacitor/filesystem, dipanggil langsung dari komponen React yang sama.
- Routing client-side (Next.js router) tetap jalan normal di dalam WebView karena semuanya udah jadi static HTML/JS/CSS yang di-load lokal.
Efeknya lumayan besar buat proyek kayak Pikabooth: codebase UI tetap satu (Next.js), tapi bisa distribusi ke Play Store dan App Store tanpa maintain dua-tiga codebase terpisah.
6. Tauri buat versi desktop-nya
Konsisten sama filosofi "satu codebase, banyak platform," desktop app Pikabooth dibungkus pakai Tauri — mirip konsepnya sama Capacitor tapi buat target desktop (Windows, macOS, Linux), dan backend-nya pakai Rust, bukan Node/Electron.
Kenapa Tauri dan bukan Electron:
- Binary yang dihasilkan jauh lebih kecil (biasanya puluhan MB dibanding ratusan MB Electron), karena Tauri nggak nge-bundle Chromium sendiri — dia manfaatin WebView native yang udah ada di OS (WebView2 di Windows, WKWebView di macOS, WebKitGTK di Linux).
- Konsumsi RAM jauh lebih ringan waktu idle, karena nggak jalanin instance Chromium penuh per app.
- Sisi backend/native layer ditulis pakai Rust, jadi command-command yang butuh akses sistem (filesystem, akses kamera device, dsb) bisa diekspos ke frontend lewat invoke(), mirip konsepnya kayak IPC di Electron tapi lebih ringan dan type-safe kalau dipadukan sama TypeScript di sisi frontend.
Setup dasarnya kurang lebih:
npm create tauri-app@latest # pilih "Next.js" sebagai frontend framework, atau kalau udah ada project Next.js, # tinggal arahkan frontendDist ke hasil static export-nya
Sama seperti Capacitor, Next.js-nya perlu di-export sebagai static (output: 'export'), lalu tauri.conf.json di-arahkan ke folder hasil build (out/) sebagai frontendDist. Bagian yang butuh logic server tetap dipisah ke backend API yang diakses dari frontend seperti biasa.
Hasil akhirnya: satu codebase Next.js yang sama dipakai buat web, iOS, Android (via Capacitor), dan desktop (via Tauri) — dengan trade-off utama di bagian: semua fitur yang awalnya "server-side" di Next.js harus dipindah keluar jadi API terpisah, karena baik Capacitor maupun Tauri sama-sama butuh output static.
Segitu dulu catatan dari proses build Pikabooth. Kalau ada yang mau ditambahin atau dikoreksi soal detail teknisnya, kabarin ya — saya juga masih terus belajar juga di sini.
Cobain webnya bos https://pikabooth.web.id/