Sandi Maulana Juhana

Web Development

14 items

May 7, 2026 at 10:00 AM

Ketika Setiap File Tahu Tugasnya

Tentang clean code dan desain modular — bagaimana saya memutuskan siapa yang bertanggung jawab atas apa, kapan harus memecah file, dan kapan lebih baik tidak.

Og Image

Catatan tentang Kode yang Bertahan Lama

Ada momen — biasanya di tengah-tengah proyek — ketika sebuah file mulai sulit untuk dikerjakan.

Bukan karena rusak. Bukan karena logikanya salah. Hanya karena sudah terlalu penuh.

Kita membukanya untuk memperbaiki satu hal kecil, lalu mendapati diri sedang menavigasi sesuatu yang jauh lebih besar dari yang ingin kita perbaiki. State, side effect, rendering, event handler — semua di tempat yang sama, karena di awal proyek, itulah tempat yang paling nyaman.

File-nya tidak salah. Hanya sudah tidak jujur tentang apa yang ia lakukan.

Itulah momen yang tepat untuk mulai memikirkan struktur.

Bukan rewrite besar. Bukan rencana migrasi. Hanya satu pertanyaan sederhana: siapa yang bertanggung jawab atas apa?

1. Satu Tanggung Jawab, Diberi Nama dengan Jelas

File yang melakukan lebih dari satu hal bukan hanya lebih sulit dibaca. Ia juga lebih sulit dipercaya.

Ketika sebuah komponen sekaligus mengelola state dan merender UI, setiap perubahan membawa dua jenis risiko. Perubahan logika bisa merusak tampilan. Perubahan tampilan bisa mengacaukan logika. Ketergantungan itu tidak terlihat, tapi selalu ada.

Aturan praktis yang saya pegang: sebuah file harus punya nama yang menjelaskan dengan tepat apa yang ia lakukan. Bukan kira-kira. Dengan tepat.

useWindowManager.ts — mengelola cara window membuka, menutup, terminimize, dan fokus. FilterMenu.tsx — merender dropdown filter dengan opsi yang bisa di-toggle.

Ketika saya bisa memberi nama sebuah file setepat itu, saya percaya dengan apa yang ada di dalamnya.

Ketika saya tidak bisa — saya mulai bertanya apa yang ia lakukan yang seharusnya tidak ia lakukan.

2. Shared Type Adalah Bahasa Bersama

Salah satu masalah yang sering luput perhatian dalam codebase yang berkembang adalah struktur yang terduplikasi.

Bukan kode yang terduplikasi — tapi intent yang terduplikasi. Bentuk yang sama muncul di banyak tempat, masing-masing didefinisikan sendiri, masing-masing sedikit berisiko berbeda dari yang lain seiring waktu.

Contoh yang konkret. Beberapa komponen bergantung pada set prop yang sama. Definisikan prop itu tujuh kali di tujuh file, dan Anda punya sebuah definisi yang harus diperbarui tujuh kali, di tujuh tempat, setiap kali antarmukanya berubah.

Sebuah shared type menyelesaikan ini tanpa beban abstraksi berlebihan:

export type BaseWindowProps = {
  isOpen: boolean;
  onClose: () => void;
  onMinimize: () => void;
  onExpand: () => void;
  isExpanded?: boolean;
  zIndex?: number;
  cascadeIndex?: number;
  onFocus?: () => void;
};

Lalu setiap komponen spesifik menggunakannya sebagai dasar:

type FinderWindowProps = BaseWindowProps & {
  items: FinderItem[];
  onOpenItem?: (item: FinderItem) => void;
};

Intersection ini langsung menyampaikan sesuatu kepada pembaca: delapan prop pertama adalah kontrak bersama. Semua yang setelah & hanya milik komponen ini.

Shared type bukan sekadar cara menghindari pengulangan. Ini cara mengkomunikasikan intent. Pembaca tidak perlu menebak prop mana yang standar dan mana yang spesifik — strukturnya sudah membuatnya terlihat.

3. Ikon Adalah Kosakata

SVG inline mudah ditulis sekali.

Paste path-nya, berhasil, ikon muncul, lanjut kerja. Tapi ketika ikon yang sama perlu muncul di tempat lain — sidebar, notification badge, tooltip, hasil pencarian — tiba-tiba kita mencari sebuah bentuk yang kita tahu ada, tapi tidak mudah ditemukan.

Kita harus copy SVG-nya lagi, atau menelusuri ke mana awalnya ditempel.

Pola yang bekerja sederhana: satu file, semua ikon, semua diekspor dengan nama.

export function BellIcon({ className }: { className?: string }) { ... }
export function LockIcon({ className }: { className?: string }) { ... }
export function VolumeIcon({ className }: { className?: string }) { ... }

Bernama, bisa diimpor, bisa digunakan ulang. Library ikon yang milik proyek sendiri.

Ketika sebuah ikon ada di file bersama, ia punya nama. Ketika ia punya nama, ia punya makna. Ketika ia punya makna, ia bisa digunakan di mana saja tanpa harus berburu path SVG aslinya.

SVG inline adalah bahan mentah. Komponen ikon bernama adalah kosakata.

4. Logic dan UI Tinggal di Tempat yang Berbeda

Pemisahan paling berguna dalam React juga yang paling sering diabaikan.

Mengelola state dan merender UI adalah pekerjaan yang berbeda. Keduanya berubah karena alasan yang berbeda. Logic berubah ketika behavior aplikasi berubah. Rendering berubah ketika desain visual berubah. Menyimpan keduanya dalam satu file berarti setiap perubahan menyentuh lebih dari yang seharusnya.

Polanya adalah mengekstrak logic ke dalam hook dan membiarkan komponen sebagai pure renderer.

Hook menjawab satu pertanyaan: apa state aplikasi saat ini?

export function useOrchestrator(props) {
  // state, effects, callbacks, sub-hooks
  return { /* semua yang dibutuhkan renderer */ };
}

Komponen menjawab pertanyaan lain: dengan state ini, seperti apa tampilan layarnya?

export function Shell(props) {
  const state = useOrchestrator(props);
  return (
    <>
      <Header ... />
      <Sidebar ... />
      <Content ... />
    </>
  );
}

Ini pertanyaan yang berbeda. Ketika keduanya tinggal dalam satu file, kedua jawaban menjadi lebih susah ditemukan. Ketika dipisah, masing-masing file bisa dibaca dan diubah secara mandiri.

Satu manfaat tambahan: logic dalam hook yang terisolasi lebih mudah diaudit. Dependency useCallback yang hilang, yang mudah terlewat dalam file 900 baris, menjadi jelas ketika callback-nya ada dalam konteks yang lebih fokus.

5. Komposisi, Bukan Akumulasi

Mengekstrak logic ke dalam hook bukan berarti hook itu harus menyerap segalanya.

Hook yang tumbuh dengan mengumpulkan setiap concern baru pada akhirnya akan mengalami masalah yang sama dengan komponen monolitik yang ia gantikan. Terlalu besar, terlalu saling bergantung, terlalu sulit dinavigasi tanpa membaca semuanya.

Alternatifnya adalah komposisi. Sebuah hook koordinator yang mendelegasikan ke sub-hook yang fokus, masing-masing memiliki domain-nya sendiri.

useOrchestrator
├── useRegistry       (apa yang terbuka, terminimize, ditumpuk)
├── useManager        (cara sesuatu membuka, menutup, fokus)
├── useContentLoader  (data, dokumen, routing)
├── useExternalData   (cuaca, kalender, API pihak ketiga)
└── ...

Setiap sub-hook memiliki domain-nya sepenuhnya. Orkestrator mengkomposisikan mereka dan menyediakan antarmuka yang terpadu ke renderer di atasnya.

Ketika concern baru muncul — sumber data baru, tipe komponen baru — ia mendapat hook-nya sendiri. Orkestrator tumbuh dengan menghubungkan hal-hal baru, bukan dengan menyerapnya.

Hasilnya adalah sistem yang tetap mudah dinavigasi. Kita selalu tahu harus mencari di mana.

6. Panjang Tidak Sama dengan Salah

Ada versi pemikiran ini yang cepat melenceng: ide bahwa lebih pendek selalu lebih baik, bahwa setiap file panjang adalah masalah yang menunggu untuk diselesaikan.

Tidak begitu.

Beberapa file memang sah-sah saja panjang karena satu tanggung jawab mereka memang luas.

File yang mengumpulkan semua ikon dalam design system akan panjang. Komponen yang merender panel settings lengkap akan panjang. Terminal emulator dengan penanganan input, history command, dan format output akan panjang.

Panjang bukan sinyalnya.

Sinyalnya adalah apakah file itu punya satu pekerjaan atau banyak.

Pertanyaan yang saya ajukan: bisakah Anda mendeskripsikan apa yang file ini lakukan dalam satu kalimat? Jika ya — bahkan jika file-nya 600 baris — ia sedang mengerjakan tugasnya. Jika tidak — bahkan jika file-nya 80 baris — kemungkinan ia membawa sesuatu yang bukan urusannya.

7. Tes Satu Kalimat

Setelah satu putaran refactoring, saya melakukan pembacaan tenang melalui codebase.

Bukan mencari masalah. Hanya membaca.

Untuk setiap file, saya tanya: jika seseorang membuka ini untuk pertama kali, bisakah mereka mengatakan dalam satu kalimat apa yang file ini lakukan?

Beberapa file langsung menjawab. Yang lain butuh sejenak.

Yang butuh sejenak itu layak untuk dikembalikan. Bukan selalu harus dipecah — kadang hanya perlu nama yang lebih jelas, struktur ekspor yang lebih rapi, atau satu fungsi yang dipindah ke tempat yang lebih natural. Tapi keraguannya patut dicatat.

File yang sulit dideskripsikan biasanya sedang membawa sesuatu yang tidak seharusnya ia tanggung sendirian.

Audit ini tidak harus jadi proses formal. Ia hanya sebuah pertanyaan, ditanyakan secara rutin, seiring codebase berkembang.

8. Modular Tidak Berarti Banyak File

Tujuan desain modular bukan jumlah file yang banyak.

Tujuannya adalah batas yang jelas.

Di dalam sebuah batas, file bisa sepanjang yang dibutuhkan. Di seberang batas, antarmukanya harus sesederhana mungkin.

Proyek dengan tiga puluh file yang fokus lebih modular dari proyek dengan tiga ratus file yang longgar lingkupnya. Dan ketika memecah sebuah file akan menciptakan lebih banyak indirection daripada kejelasan — ketika batas barunya terasa dipaksakan daripada natural — keputusan yang tepat adalah membiarkannya.

Heuristik yang selalu saya pegang:

Pisahkan ketika pemisahan mengungkap sesuatu. Berhenti ketika pemisahan menyembunyikan sesuatu.

Penutup

Clean code bukan preferensi gaya.

Ini adalah komitmen berkelanjutan untuk membuat keputusan terlihat.

Keputusan bahwa file ini bertanggung jawab atas state, bukan rendering. Keputusan bahwa ikon ini seharusnya ada dalam kosakata bersama, bukan tertanam dalam satu komponen. Keputusan bahwa kontrak bersama ini harus ada, sehingga tujuh file tidak harus bersepakat secara informal tentang struktur yang semuanya bergantung padanya.

Keputusan-keputusan ini tidak harus dibuat sekaligus. Bisa dibuat dengan pelan, satu file dalam satu waktu, seiring codebase berkembang dan bentuknya menjadi lebih jelas.

Begitulah sebenarnya pekerjaan itu terlihat.

Bukan rewrite yang dramatis. Bukan rencana migrasi. Hanya perhatian yang konsisten terhadap apa yang setiap file lakukan, dan apakah yang ia lakukan itu jujur.