Javascript Modules Import Export Dynamic Import Structure
Definisi
Module di JavaScript adalah unit kode yang bisa dipisahkan ke file lain lalu dipakai kembali lewat import dan export. Intinya sederhana: satu file tidak harus menampung semua hal. Saat aplikasi masih kecil, satu file besar sering terasa cepat. Saat proyek tumbuh, modularisasi membuat kode lebih mudah dibaca, diuji, dipindahkan, dan dirapikan.
Di praktik harian, module biasanya dipakai untuk:
- memisahkan tanggung jawab fungsi, konstanta, komponen, helper, dan service
- menghindari duplikasi logika
- mengontrol dependency antarbagian aplikasi
- memuat kode secara bertahap saat dibutuhkan
- menjaga struktur proyek tetap bisa di-scan oleh manusia
Kalau diabaikan, file biasanya membesar tanpa arah: import saling silang, helper dipakai di mana-mana, dan perubahan kecil ikut memicu banyak file lain.
Kapan module dipakai
Pakai module saat satu file mulai punya lebih dari satu tanggung jawab yang jelas, misalnya:
- file helper campur parsing, formatting, dan request
- file UI campur presentasi, state, dan akses API
- file konfigurasi ikut berisi logic runtime
- beberapa bagian proyek memakai logika yang sama
Module juga berguna ketika kamu ingin memisahkan layer berdasarkan peran:
- UI layer: komponen, view, presentational logic
- domain layer: aturan bisnis, transformasi data, validasi
- data layer: fetch, adapter API, repository, storage
- shared layer: utilitas umum, konstanta, tipe, helper kecil
Prinsipnya bukan “semua harus dipisah”. Prinsipnya: pisahkan kalau ada batas tanggung jawab yang jelas, atau kalau file mulai sulit dipelihara.
Cara kerja import/export
JavaScript module memakai dua arah utama:
exportuntuk mengeluarkan nilai dari sebuah fileimportuntuk memakai nilai itu di file lain
Named export
Named export cocok untuk banyak hal dalam satu file.
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}Lalu dipakai seperti ini:
import { PI, add } from './math.js';Ciri utama named export:
- harus diimpor dengan nama yang sesuai, kecuali kamu pakai alias
- satu file boleh punya banyak named export
- bagus untuk utility, konstanta, dan kumpulan fungsi kecil
Kalau ingin mengganti nama saat import:
import { add as sum } from './math.js';Ini berguna kalau nama lokal bentrok, atau kamu ingin nama yang lebih jelas di context pemakaian.
Default export
Default export cocok saat file memang punya satu nilai utama.
// formatDate.js
export default function formatDate(date) {
return new Intl.DateTimeFormat('id-ID').format(date);
}Pemakaian:
import formatDate from './formatDate.js';Ciri utama default export:
- satu file hanya punya satu default export
- nama saat import bebas, jadi penamaan ada di tangan pemakai
- cocok untuk komponen utama, class utama, atau modul yang memang punya “satu wajah”
Perbedaan praktis default vs named
| Aspek | Named export | Default export |
|---|---|---|
| Jumlah per file | banyak | satu |
| Cara import | pakai kurung kurawal | tanpa kurung kurawal |
| Nama import | harus cocok dengan export | bisa bebas |
| Cocok untuk | utility, konstanta, beberapa fungsi | satu entitas utama |
| Dampak refactor | lebih eksplisit | lebih fleksibel, tapi bisa kurang seragam |
Secara referensial, named export sering lebih enak untuk codebase besar karena lebih jelas saat dibaca dan lebih sulit “disalahpahami”. Default export lebih nyaman saat file memang hanya punya satu tanggung jawab utama.
Kapan memilih named export
Pilih named export kalau:
- satu file berisi beberapa helper yang saling berkaitan
- kamu ingin import yang eksplisit dan mudah di-search
- file itu berpotensi menambah export lain di masa depan
- kamu ingin meminimalkan nama yang berubah diam-diam saat refactor
Contoh yang sehat:
// validators.js
export function isEmail(value) {
return /@/.test(value);
}
export function isRequired(value) {
return value != null && value !== '';
}Pemakaian:
import { isEmail, isRequired } from './validators.js';Di proyek besar, pola ini biasanya lebih mudah dirawat karena pembaca langsung tahu dependency apa yang dipakai.
Kapan memilih default export
Pilih default export kalau:
- file mewakili satu unit utama, misalnya satu komponen, satu service, atau satu kelas
- nama module sudah sangat jelas dari nama file
- kamu ingin import yang pendek dan natural
Contoh:
// UserCard.jsx
export default function UserCard({ user }) {
return <div>{user.name}</div>;
}Pemakaian:
import UserCard from './UserCard.jsx';Namun default export perlu disiplin. Kalau satu folder berisi banyak default export yang namanya mirip, orang bisa lebih mudah tersesat saat tracing dependency.
Dynamic import
Dynamic import dipakai saat module tidak perlu dimuat sejak awal. Bentuknya memakai import() dan mengembalikan Promise.
async function loadChart() {
const module = await import('./chart.js');
module.renderChart();
}Kalau module punya default export:
async function loadFormatter() {
const { default: formatDate } = await import('./formatDate.js');
console.log(formatDate(new Date()));
}Kapan dynamic import dipakai
Dynamic import cocok untuk:
- fitur yang jarang dipakai
- halaman atau panel yang berat
- code splitting di aplikasi frontend
- plugin system
- conditional loading berdasarkan environment atau permission
Contoh yang realistis:
- chart library baru dimuat saat panel laporan dibuka
- editor markdown dimuat hanya ketika user masuk mode edit
- validator berat dimuat setelah user menekan submit
Kenapa dynamic import penting
Dynamic import membantu menunda biaya loading. Ini tidak selalu membuat aplikasi “lebih cepat” secara keseluruhan, tapi bisa membuat pengalaman awal lebih ringan.
Yang diuntungkan biasanya:
- bundle awal lebih kecil
- first load terasa lebih cepat
- kode berat hanya ditarik saat benar-benar diperlukan
Cara merapikan dependency
Module yang sehat bukan cuma soal memindahkan kode ke file lain. Yang lebih penting adalah merapikan arah dependency.
1) Buat dependency mengalir satu arah
Idealnya, layer atas memakai layer bawah, bukan sebaliknya.
Contoh arah yang sehat:
- UI memanggil service
- service memanggil repository
- repository memanggil adapter API
- util dipakai oleh layer yang membutuhkan, bukan sebaliknya
Kalau arah dependency bolak-balik, tanda pertamanya biasanya file mulai saling import dalam lingkaran.
2) Pisahkan berdasarkan tanggung jawab, bukan hanya berdasarkan jenis file
Jangan cuma membuat folder utils/ besar lalu menumpuk semua hal di sana. Itu sering berubah jadi gudang sampah terselubung.
Lebih sehat kalau pemisahannya mengikuti peran:
features/auth/features/profile/shared/format/shared/http/entities/user/
Dengan begitu, lokasi file ikut menjelaskan siapa yang boleh memakai apa.
3) Buat API module kecil dan jelas
Satu module sebaiknya punya permukaan API yang kecil. Kalau sebuah file mengekspos terlalu banyak hal, pemakai jadi tergoda mengambil semuanya.
Contoh kurang sehat:
import { a, b, c, d, e } from './big-module.js';Kalau pola ini sering terjadi, biasanya module itu sudah terlalu gemuk.
4) Hindari dependency yang “naik level” sembarangan
Contoh masalah umum:
- util kecil mengimpor komponen UI
- domain logic mengimpor detail DOM
- layer data mengimpor helper presentasi
Kalau helper kecil butuh tahu terlalu banyak hal di atasnya, struktur biasanya mulai retak.
Alur dependency module
Diagram berikut menunjukkan alur dependency yang sehat: layer atas memakai layer bawah, shared module dipakai lintas fitur, dan module berat dimuat belakangan dengan import().
Bacanya begini:
- entry point memanggil route atau page
- route memecah ke fitur
- fitur memakai service/domain
- service memakai repository atau adapter
- shared module dipakai lintas fitur
- module berat dimuat belakangan dengan dynamic import
Pemecahan file yang sehat
Pemecahan file yang sehat biasanya mengikuti aturan sederhana:
Pecah kalau:
- satu file mulai punya lebih dari satu alasan untuk berubah
- isi file sulit dicari karena terlalu panjang
- dependency import makin banyak dan campur aduk
- satu bagian bisa dipakai ulang di tempat lain
- ada logika yang jelas bisa diuji sendiri
Jangan pecah terlalu cepat kalau:
- pemisahan hanya memindahkan tiga baris ke file baru tanpa nilai tambah
- nama file baru tidak menjelaskan tanggung jawabnya
- file jadi terlalu kecil dan justru menyebar konteks
Pola pecahan yang biasanya sehat
constants.jsuntuk nilai tetap yang dipakai lintas filehelpers.jsuntuk fungsi kecil yang benar-benar umumformatters.jsuntuk transformasi presentasivalidators.jsuntuk validasi yang bisa dipakai ulangservice.jsuntuk orkestrasi logicrepository.jsuntuk akses data eksternal
Contoh struktur yang lebih rapi:
features/
auth/
login-form.jsx
auth.service.js
auth.validators.js
profile/
profile-page.jsx
profile.service.js
shared/
format/
format-date.js
http/
api-client.jsStruktur seperti ini membantu pembaca menebak lokasi file tanpa harus membuka semua folder.
Jebakan umum saat proyek membesar
1) Circular dependency
Circular dependency terjadi saat A mengimpor B, lalu B mengimpor A, langsung atau tidak langsung.
Akibatnya bisa macam-macam:
- nilai belum terinisialisasi saat dipakai
- behavior runtime aneh dan sulit dilacak
- refactor kecil memicu bug yang terasa “acak”
Gejala yang sering muncul:
- import terasa benar, tapi nilai
undefined - module jalan di satu tempat tapi gagal di tempat lain
- testing jadi tidak stabil
2) Barrel export yang kebablasan
Barrel export biasanya memakai file indeks untuk re-export banyak module.
Contoh:
// index.js
export * from './validators.js';
export * from './formatters.js';Ini berguna, tapi kalau terlalu agresif bisa menyembunyikan asal dependency dan memperbesar risiko circular dependency.
Pakai barrel export secukupnya. Kalau semua hal selalu lewat satu pintu, tracing dependency bisa jadi susah.
3) Utility folder yang jadi tempat buangan
Folder utils/ sering dimulai dengan niat baik, lalu berakhir jadi campur aduk:
- fungsi string
- helper date
- helper DOM
- fetch wrapper
- formatter UI
Kalau isinya terlalu umum, lebih baik pecah sesuai domain atau fitur.
4) Default export tanpa konsistensi
Default export memang praktis, tapi kalau dipakai tanpa pola yang seragam, hasilnya sering membingungkan.
Masalah yang sering muncul:
- nama import tidak sama dengan nama file
- refactor rename file tidak selalu membuat konteks lebih jelas
- satu folder penuh default export terasa tidak eksplisit
5) File terlalu gemuk
File besar bukan masalah utama. File besar yang campur tanggung jawab itu masalah.
Tanda file sudah terlalu gemuk:
- sulit menentukan fungsi utamanya
- scroll jauh untuk menemukan satu helper
- import semakin bertambah tanpa pola
- test sulit dipisah per unit
Contoh pola yang sehat
Named export untuk utility kecil
// string.js
export function trimLower(value) {
return String(value).trim().toLowerCase();
}
export function capitalize(value) {
const text = String(value);
return text.charAt(0).toUpperCase() + text.slice(1);
}Default export untuk satu komponen utama
// Button.jsx
export default function Button({ children, ...props }) {
return <button {...props}>{children}</button>;
}Dynamic import untuk module berat
async function openEditor() {
const { default: Editor } = await import('./Editor.jsx');
return Editor;
}Catatan implementasi
- Gunakan ekstensi file yang konsisten jika environment-mu membutuhkannya, terutama di ESM.
- Jangan campur CommonJS dan ESM tanpa alasan yang jelas.
- Nama file sebaiknya mencerminkan isi dan peran, bukan hanya jenis file.
- Kalau sebuah module sering dipakai bersama module lain, pertimbangkan apakah mereka sebenarnya satu unit domain.
- Saat dependency sulit dibaca, tarik garis boundary terlebih dulu sebelum memindahkan kode.
Kesalahan umum
- memakai module hanya karena ingin “memecah file”, padahal boundary-nya belum jelas
- membuat helper umum yang diam-diam tahu terlalu banyak detail aplikasi
- mengimpor dari barrel export yang terlalu dalam lalu menambah circular dependency
- menaruh logic berat di file UI
- memakai dynamic import untuk semua hal tanpa alasan performa atau boundary yang jelas
- membiarkan folder
utils/menjadi tempat buangan
Ringkasan praktis
Kalau mau pakai module dengan sehat, pegang urutan ini:
- mulai dari tanggung jawab
- pisahkan berdasarkan boundary
- pilih named export untuk banyak API kecil
- pilih default export untuk satu entitas utama
- pakai dynamic import untuk kode yang bisa ditunda
- jaga dependency tetap satu arah
- hindari barrel export berlebihan dan circular dependency
Kalau modul membuat kode lebih mudah di-scan, lebih mudah diuji, dan lebih mudah diubah, berarti struktur itu sudah bekerja.