kodingup.camp

Beranda/Siap Kerja US: Interview Teknis

Interview Front-End

Modul 9 dari 116 menit bacaFront-End Interviews

Isi modul

    Kenapa ini penting

    Kalau kamu melamar posisi front-end atau full-stack dengan fokus UI, banyak perusahaan menambahkan (atau mengganti sebagian) ronde algoritma dengan ronde front-end khusus. Front End Interview Playbook dari GreatFrontEnd (yang juga direkomendasikan Tech Interview Handbook untuk front-end system design) mendaftar enam format: algorithmic coding, JavaScript coding (implementasi fungsi atau struktur data yang berkaitan dengan front-end, seperti debounce dan Promise.all), user interface coding (membangun komponen, widget, atau app), system design, quiz (pertanyaan konsep berjawaban singkat), dan behavioral. Kombinasi persisnya berbeda per perusahaan, jadi tanyakan ke recruiter.

    Kabar baiknya: ini wilayahmu. Kamu sudah memakai debounce di input pencarian, Promise.all untuk fetch paralel, dan event emitter di Node.js. Bedanya, di interview kamu harus bisa menulisnya dari nol dan menjelaskan detailnya (this, urutan hasil, error handling, cleanup), sambil berbicara.

    Konsep inti

    Utility yang paling sering diminta

    Utility Inti konsep Detail yang sering ditanya
    debounce(fn, wait) jalankan setelah input "diam" wait ms clearTimeout, meneruskan this dan argumen, cancel
    throttle(fn, wait) paling banyak sekali per wait ms leading vs trailing call
    Promise.all tunggu semua, gagal kalau satu gagal urutan hasil sesuai input, array kosong, nilai non-promise
    Event emitter on, off, once, emit unsubscribe saat emit, listener dipanggil berurutan
    Deep clone salin bersarang Date, Map, Set, referensi melingkar, structuredClone bawaan

    Debounce vs throttle dengan analogi lift: debounce seperti pintu lift yang menunggu sampai tidak ada orang masuk selama 3 detik, baru menutup. Throttle seperti lift yang berangkat paling sering sekali per menit, siapa pun yang menekan tombol. Debounce cocok untuk input pencarian (kirim request setelah user berhenti mengetik); throttle cocok untuk event scroll atau resize (update posisi secara berkala).

    React live coding

    Soal umum: autocomplete/typeahead, todo list dengan filter, tabs, accordion, modal, star rating, infinite scroll, progress bar, data table dengan sort. Yang dinilai bukan hanya "jalan", tapi:

    Pertanyaan konsep yang sering muncul

    Topik Contoh pertanyaan Inti jawaban
    Event loop "What's the output order of setTimeout vs Promise.then?" microtask (Promise) dijalankan sebelum task berikutnya (setTimeout)
    Rendering "What happens when you type a URL and press Enter?" DNS, TCP/TLS, HTTP, parsing HTML, CSSOM, render tree, layout, paint, composite
    Event "Explain event delegation." pasang satu listener di induk, manfaatkan bubbling, cek event.target
    Keamanan "How do you prevent XSS?" escape output, hindari dangerouslySetInnerHTML/innerHTML dengan input user, CSP
    Aksesibilitas "How would you make a custom dropdown accessible?" elemen semantik dulu, ARIA role/state, fokus keyboard, label
    Performa "How would you improve LCP?" optimasi gambar hero, preload, kurangi render-blocking resource, SSR/SSG
    React "Why do keys matter in lists?" key stabil membantu reconciliation; index sebagai key bermasalah saat urutan berubah
    Next.js "When would you use a Server Component?" data fetching di server, mengurangi JS di client; Client Component untuk interaktivitas

    Jawab konsep dengan pola: definisi singkat → contoh konkret dari pengalamanmu → trade-off. Contoh: "Event delegation means attaching one listener to a parent instead of one per child. In my last project, a table with a thousand rows used one click handler on the tbody. The trade-off is that you need to check event.target carefully, and some events like focus don't bubble the same way."

    Contoh kode

    Lima utility JavaScript

    // 1. debounce: jalankan fn hanya setelah "diam" selama wait ms
    function debounce(fn, wait) {
      let timer = null;
      function debounced(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), wait);   // arrow function menangkap this dari debounced
      }
      debounced.cancel = () => clearTimeout(timer);
      return debounced;
    }
    
    // 2. throttle: paling banyak sekali per wait ms (leading call, sisanya dibuang)
    function throttle(fn, wait) {
      let last = -Infinity;
      return function (...args) {
        const now = Date.now();
        if (now - last >= wait) { last = now; fn.apply(this, args); }
      };
    }
    
    // 3. Promise.all buatan sendiri
    function promiseAll(items) {
      return new Promise((resolve, reject) => {
        const results = new Array(items.length);
        let remaining = items.length;
        if (remaining === 0) return resolve([]);
        items.forEach((item, i) => {
          Promise.resolve(item).then(value => {       // nilai biasa juga boleh
            results[i] = value;                       // jaga urutan sesuai input, bukan urutan selesai
            if (--remaining === 0) resolve(results);
          }, reject);                                 // gagal satu = gagal semua
        });
      });
    }
    
    // 4. Event emitter
    class EventEmitter {
      #events = new Map();
      on(name, cb) {
        if (!this.#events.has(name)) this.#events.set(name, new Set());
        this.#events.get(name).add(cb);
        return () => this.off(name, cb);              // kembalikan fungsi unsubscribe
      }
      off(name, cb) { this.#events.get(name)?.delete(cb); }
      once(name, cb) {
        const wrapper = (...args) => { this.off(name, wrapper); cb(...args); };
        return this.on(name, wrapper);
      }
      emit(name, ...args) {
        const listeners = this.#events.get(name);
        if (!listeners) return false;
        for (const cb of [...listeners]) cb(...args); // salin dulu: aman kalau ada off() saat emit
        return true;
      }
    }
    
    // 5. deep clone dengan dukungan Date, Map, Set, Array, dan referensi melingkar
    function deepClone(value, seen = new WeakMap()) {
      if (value === null || typeof value !== 'object') return value;
      if (seen.has(value)) return seen.get(value);
      if (value instanceof Date) return new Date(value);
      if (value instanceof Map) { const m = new Map(); seen.set(value, m); value.forEach((v, k) => m.set(deepClone(k, seen), deepClone(v, seen))); return m; }
      if (value instanceof Set) { const s = new Set(); seen.set(value, s); value.forEach(v => s.add(deepClone(v, seen))); return s; }
      const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
      seen.set(value, copy);
      for (const key of Reflect.ownKeys(value)) copy[key] = deepClone(value[key], seen);
      return copy;
    }
    
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    
    async function main() {
      // debounce: 5 panggilan cepat -> 1 eksekusi dengan argumen terakhir
      const calls = [];
      const search = debounce(q => calls.push(q), 50);
      for (const q of ['r', 're', 'rea', 'reac', 'react']) search(q);
      await sleep(80);
      console.log('debounce:', calls);
      // → debounce: [ 'react' ]
    
      // throttle: dipanggil tiap 10ms selama ~100ms, wait 40ms
      let fired = 0; const onScroll = throttle(() => fired++, 40);
      for (let i = 0; i < 10; i++) { onScroll(); await sleep(10); }
      console.log('throttle dipanggil 10x, jalan:', fired, 'kali (sekitar 3, tergantung timer)');
      // → throttle dipanggil 10x, jalan: 3 kali (sekitar 3, tergantung timer)
    
      // promiseAll: urutan hasil mengikuti input walau yang pertama paling lambat
      console.log(await promiseAll([sleep(30).then(() => 'lambat'), 'langsung', Promise.resolve(42)]));
      // → [ 'lambat', 'langsung', 42 ]
      console.log(await promiseAll([]));
      // → []
      try { await promiseAll([sleep(10), Promise.reject(new Error('boom'))]); }
      catch (e) { console.log('ditolak:', e.message); }
      // → ditolak: boom
    
      // EventEmitter: on, once, unsubscribe
      const bus = new EventEmitter(); const log = [];
      const unsub = bus.on('msg', m => log.push('A:' + m));
      bus.once('msg', m => log.push('B:' + m));
      bus.emit('msg', 1); bus.emit('msg', 2); unsub(); bus.emit('msg', 3);
      console.log(log, bus.emit('nothing'));
      // → [ 'A:1', 'B:1', 'A:2' ] false
    
      // deepClone: objek bersarang, Set, Date, referensi melingkar
      const original = { name: 'Ana', tags: new Set(['js']), born: new Date('2000-01-01'), nested: { a: [1, { b: 2 }] } };
      original.self = original;
      const c = deepClone(original);
      c.nested.a[1].b = 99; c.tags.add('ts');
      console.log(original.nested.a[1].b, c.nested.a[1].b, original.tags.size, c.tags.size, c.self === c, c.born instanceof Date);
      // → 2 99 1 2 true true
      const sc = structuredClone(original);          // bawaan browser modern dan Node 17+
      console.log('structuredClone:', sc.self === sc, sc.born instanceof Date);
      // → structuredClone: true true
    }
    main();
    

    Jumlah eksekusi throttle bergantung pada ketepatan timer sistem; di run ini hasilnya 3. Itu sebabnya test berbasis waktu nyata sebaiknya memakai fake timers (misalnya vi.useFakeTimers() di Vitest) di proyek sungguhan. Tentang deepClone: di interview, sebutkan bahwa di kode produksi kamu akan memakai structuredClone bawaan, lalu jelaskan batasannya (tidak bisa menyalin fungsi atau DOM node, dan prototype class tidak dipertahankan).

    Live coding React: Autocomplete

    Komponen ini memakai debounce sebagai hook, AbortController untuk membuang respons basi, status lengkap, dan atribut ARIA dasar untuk combobox.

    import { useEffect, useId, useState } from 'react';
    
    // Hook kecil: nilai yang baru berubah setelah user berhenti mengetik
    function useDebouncedValue(value, delay) {
      const [debounced, setDebounced] = useState(value);
      useEffect(() => {
        const t = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(t);          // cleanup: batalkan timer lama
      }, [value, delay]);
      return debounced;
    }
    
    export function Autocomplete({ fetchSuggestions, delay = 300 }) {
      const [query, setQuery] = useState('');
      const [items, setItems] = useState([]);
      const [status, setStatus] = useState('idle');   // idle | loading | success | error
      const debounced = useDebouncedValue(query.trim(), delay);
      const listId = useId();
    
      useEffect(() => {
        if (!debounced) { setItems([]); setStatus('idle'); return; }
        const controller = new AbortController();
        setStatus('loading');
        fetchSuggestions(debounced, controller.signal)
          .then(result => { setItems(result); setStatus('success'); })
          .catch(err => { if (err.name !== 'AbortError') setStatus('error'); });
        return () => controller.abort();       // cegah race condition: respons lama dibuang
      }, [debounced, fetchSuggestions]);
    
      return (
        <div>
          <label htmlFor={listId + '-input'}>Cari framework</label>
          <input
            id={listId + '-input'}
            value={query}
            onChange={e => setQuery(e.target.value)}
            role="combobox"
            aria-expanded={items.length > 0}
            aria-controls={listId}
            aria-autocomplete="list"
          />
          {status === 'loading' && <p role="status">Memuat…</p>}
          {status === 'error' && <p role="alert">Gagal memuat. Coba lagi.</p>}
          {status === 'success' && items.length === 0 && <p>Tidak ada hasil</p>}
          <ul id={listId} role="listbox">
            {items.map(item => <li key={item} role="option" aria-selected="false">{item}</li>)}
          </ul>
        </div>
      );
    }
    

    Komponen di atas diuji dengan React 19.3.1 + jsdom 30.1.3: fetchSuggestions palsu dengan jeda 20 ms, debounce 30 ms, lalu mensimulasikan user mengetik. Hasil nyata run tersebut (format: label | isi list | pesan status):

    # → setelah ketik "re" (belum lewat debounce) | (kosong) |
    # → lewat debounce, sedang fetch | (kosong) | Memuat…
    # → fetch selesai | React,Redux,Remix |
    # → ketik "n" | Next.js,Nuxt |
    # → ketik "zzz" | Tidak ada hasil
    # → server error | (kosong) | Gagal memuat. Coba lagi.
    # → aria-expanded: false | label terhubung: true
    

    Yang bisa kamu tawarkan sebagai pengembangan kalau waktu masih ada: navigasi keyboard (panah atas/bawah, Enter, Escape) dengan aria-activedescendant, highlight bagian yang cocok, cache hasil per query, dan pesan "Memuat…" yang tidak menghapus hasil lama. Untuk combobox yang lengkap, rujuk pola combobox di ARIA Authoring Practices Guide (APG) dari W3C.

    Kesalahan umum

    1. Debounce yang kehilangan this atau argumen. Cara benar: simpan ...args dan pakai fn.apply(this, args) di dalam arrow function.
    2. Promise.all yang mengisi hasil dengan push. Urutan jadi mengikuti siapa yang selesai duluan. Cara benar: results[i] = value.
    3. Lupa cleanup di useEffect. Timer dan request tetap berjalan setelah komponen unmount atau query berubah. Cara benar: kembalikan fungsi cleanup (clearTimeout, controller.abort()).
    4. Menyimpan data turunan di state. Misalnya filteredItems disimpan terpisah lalu lupa disinkronkan. Cara benar: hitung saat render, atau useMemo kalau mahal.
    5. <div onClick> sebagai tombol. Tidak bisa difokus keyboard dan tidak diumumkan screen reader. Cara benar: <button>; gunakan ARIA hanya kalau elemen semantik tidak ada.

    Latihan

    1. Mudah, Tulis once(fn): fungsi yang hanya menjalankan fn pada panggilan pertama dan mengembalikan hasil yang sama di panggilan berikutnya.
    2. Sedang, Tulis allSettled(items) seperti Promise.allSettled: tidak pernah reject, hasilnya { status, value } atau { status, reason }.
    3. Menantang, Tulis promisePool(tasks, limit): jalankan array fungsi async dengan paling banyak limit berjalan bersamaan, hasil sesuai urutan input.
    Lihat solusi
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    
    // 1. Simpan flag dan hasil di closure
    function once(fn) { let done = false, result; return function (...args) { if (!done) { done = true; result = fn.apply(this, args); } return result; }; }
    let n = 0; const init = once(() => ++n);
    console.log(init(), init(), init(), n);
    // → 1 1 1 1
    
    // 2. Ubah setiap promise menjadi promise yang selalu resolve, lalu Promise.all
    function allSettled(items) {
      return Promise.all(items.map(p => Promise.resolve(p).then(
        value => ({ status: 'fulfilled', value }),
        reason => ({ status: 'rejected', reason: reason.message ?? reason }))));
    }
    
    // 3. Buat 'limit' worker yang mengambil tugas berikutnya dari indeks bersama
    async function promisePool(tasks, limit) {
      const results = new Array(tasks.length); let next = 0, running = 0, maxRunning = 0;
      async function worker() {
        while (next < tasks.length) {
          const i = next++; running++; maxRunning = Math.max(maxRunning, running);
          try { results[i] = await tasks[i](); } finally { running--; }
        }
      }
      await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
      return { results, maxRunning };
    }
    
    (async () => {
      console.log(JSON.stringify(await allSettled([1, Promise.reject(new Error('x')), sleep(5).then(() => 'ok')])));
      // → [{"status":"fulfilled","value":1},{"status":"rejected","reason":"x"},{"status":"fulfilled","value":"ok"}]
      const tasks = [30, 10, 20, 10, 5].map((ms, i) => () => sleep(ms).then(() => i));
      console.log(JSON.stringify(await promisePool(tasks, 2)));
      // → {"results":[0,1,2,3,4],"maxRunning":2}
    })();
    

    promisePool aman dari race condition karena JavaScript single-threaded: next++ tidak pernah dijalankan dua worker secara bersamaan. Ini poin bagus untuk dijelaskan. Kasus nyata: membatasi upload paralel atau request ke API dengan rate limit.

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    How To Prepare For Front-End Coding InterviewsConner Ardman · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.