Prog Academy
RU UA
Питання на співбесіді Front-End розробника: 37 питань із відповідями
Кар'єра в IT

Питання на співбесіді Front-End розробника: 37 питань із відповідями

Коротка відповідь: на співбесіді Junior Front-End розробника перевіряють не знання рідкісних фактів, а розуміння основ — семантики HTML, блокової моделі та розкладок CSS, типів, замикань і this у JavaScript, циклу подій, роботи браузера та базових речей у TypeScript і React. Нижче — 37 питань у тих формулюваннях, у яких їх ставлять, із короткими відповідями: спершу те, що варто сказати вголос, далі деталі й код. Питання згруповано в сім блоків; після них — практичні задачі з розв'язками, типові помилки та план підготовки на два тижні.

Я у фронтенд-розробці з 2014 року і понад дев'ять років викладаю. За моїми спостереженнями, початківці частіше спотикаються не на екзотиці, а на базі: можуть зібрати застосунок на React, але не можуть пояснити, чому setTimeout із нульовою затримкою спрацьовує пізніше за проміс. Тому в добірці найбільше місця відведено JavaScript, а кожен приклад коду можна запустити й перевірити самостійно.

Актуально на жовтень 2026 року

Усі приклади на JavaScript запущено в Node.js v22.14.0, і вивід у коментарях — справжній. Приклади на TypeScript перевірено компілятором TypeScript 5.9.3 у режимі strict. Фрагменти HTML, CSS, React і роботи з DOM — мінімальні стандартні приклади, у межах статті їх не запускали. Добір питань — вибір автора з досвіду викладання, а не статистика: у конкретній компанії перелік буде іншим.

Як проходить співбесіда Front-End розробника?

Зазвичай відбір складається з чотирьох етапів: коротка розмова з рекрутером, технічне інтерв'ю, практичне завдання і фінальна зустріч із командою або керівником. Порядок і кількість етапів залежать від компанії: десь тестове завдання надсилають до технічного інтерв'ю, десь його замінює live coding. Запитайте про це рекрутера вже на першому дзвінку.

ЕтапЩо перевіряютьЯк готуватися
Скринінг із рекрутеромМотивацію, очікування, англійську, відповідність резюме вакансіїПідготуйте розповідь про себе на 2–3 хвилини і відповідь на питання, чому фронтенд і чому ця компанія
Технічне інтерв'юТеорію з HTML, CSS, JavaScript, браузера та фреймворку з вакансіїВідповідайте на питання з цієї статті вголос: спершу коротка відповідь, потім приклад
Практичне завдання або live codingУміння писати код, читати чужий і пояснювати свої рішенняРозв'язуйте невеликі задачі з таймером і проговорюйте хід думки
Фінальна зустрічЯк ви працюєте в команді, реагуєте на зауваження і вчитесяПригадайте два-три приклади з навчальних проєктів: що не виходило і як ви це розв'язали

Якщо ви лише плануєте шлях у професію, почніть із матеріалу Як стати Front-End розробником, а медіанні зарплати за рівнями зібрано на сторінці зарплати Front-End розробників.

Які теми перевіряють на Junior і Middle?

Від джуна чекають упевненої бази з HTML, CSS і JavaScript та робочого знання одного фреймворку; від Middle — розуміння, чому все влаштовано саме так, і вміння обирати між рішеннями. Таблиця показує, де проходить межа, без оцінок частоти питань.

ТемаЩо перевіряютьРівень
HTML і доступністьСемантичні теги, форми, керування з клавіатури, текстові альтернативиJunior
CSSБлокова модель, специфічність, Flexbox і Grid, позиціювання, адаптивністьJunior
Основи JavaScriptТипи та приведення, область видимості, замикання, this, прототипиJunior
АсинхронністьПроміси, async/await, порядок виконання в циклі подійJunior; деталі черг — Middle
DOM і браузерПодії, делегування, сховища, CORS, шлях від URL до пікселівJunior; рендеринг у деталях — Middle
TypeScriptТипи та інтерфейси, unknown, звуження типів, дженерикиJunior — основи; Middle — дженерики та службові типи
ReactКомпоненти, props і state, хуки, ключі, ефекти, повторний рендерJunior; оптимізація рендеру — Middle
Інструменти та продуктивністьGit, збирач, Core Web VitalsJunior — Git і загальне розуміння; Middle — налаштування та вимірювання

Питання з HTML і доступності

Що таке семантична верстка і навіщо вона потрібна?

Семантична верстка — це вибір тегів за змістом вмісту, а не за зовнішнім виглядом: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, заголовки <h1>–<h6>, <button> для дій і <a> для переходів.

Вона потрібна з трьох причин. Програми екранного доступу будують за такими тегами карту сторінки й дають змогу стрибати між областями та заголовками. Пошукові системи краще розуміють структуру документа. І код читається простіше: <nav> зрозуміліший, ніж <div class="menu-wrapper">. Перелік елементів із призначенням кожного є в довіднику MDN.

Типова помилка: обирати рівень заголовка за розміром шрифту. Рівні задають структуру, а розмір налаштовується в CSS.

Чим кнопка button відрізняється від div з обробником кліку?

<button> одразу отримує фокус із клавіатури, спрацьовує на Enter і пробіл, оголошується скринрідером як кнопка і вміє бути вимкненою через disabled. <div> нічого з цього не вміє: доведеться вручну додавати tabindex="0", role="button" та обробники клавіш.

Правило просте: дія на сторінці — <button>, перехід на іншу адресу — <a href>. Окрема деталь, яку люблять уточнювати: усередині форми кнопка без атрибута type має тип submit і надсилає форму, тому звичайним кнопкам пишуть type="button".

Як пов'язати підпис із полем форми і яку перевірку дає сам HTML?

Підпис пов'язують із полем через пару for та id або вкладають поле всередину <label>. Тоді клік по підпису ставить фокус у поле, а скринрідер зачитує підпис під час переходу до нього. Атрибут placeholder підпису не замінює: він зникає під час введення.

<form action="/subscribe" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <button type="submit">Subscribe</button>
</form>

Вбудовану перевірку дають атрибути required, type (email, url, number), min, max, minlength, maxlength і pattern; тип поля ще й підказує мобільному браузеру потрібну клавіатуру. Докладніше — у посібнику MDN з валідації форм.

Що хочуть почути: перевірка в браузері — це зручність для користувача, а не захист. Дані однаково перевіряються на сервері.

Що таке доступність і що ви перевіряєте насамперед?

Доступність (accessibility, a11y) — це можливість користуватися сайтом людям із різними обмеженнями: без миші, зі скринрідером, зі слабким зором, на маленькому екрані. Вимоги описано у стандарті WCAG 2.2.

Мінімальна перевірка, яку можна зробити самостійно:

  • увесь сценарій проходиться з клавіатури, і фокус завжди видно;
  • змістовні зображення мають alt, у декоративних він порожній (alt="");
  • кожне поле форми має підпис;
  • контраст звичайного тексту і тла не нижчий за 4,5:1 — це рівень AA у WCAG;
  • заголовки йдуть по порядку і відображають структуру сторінки.

Про ARIA достатньо знати головне правило з настанов W3C: спершу нативний елемент HTML, а атрибути role та aria-* — лише коли відповідного елемента немає. Неправильна ARIA-розмітка гірша, ніж її відсутність.

Питання з CSS

Що таке блокова модель і що змінює box-sizing: border-box?

Кожен елемент — це прямокутник із чотирьох шарів: вміст, внутрішній відступ (padding), рамка (border) і зовнішній відступ (margin). Типово діє box-sizing: content-box: властивість width задає ширину лише вмісту, а padding і border додаються зверху. За border-box вони входять у задану ширину.

*,
*::before,
*::after {
  box-sizing: border-box;
}

.card {
  width: 300px;
  padding: 20px;
  border: 2px solid currentColor;
}
/* border-box: the card is 300px wide; content-box: 300 + 40 + 4 = 344px */

Варто згадати і схлопування відступів: вертикальні margin сусідніх блокових елементів не додаються, а береться більший із них. Опис моделі — у MDN: box-sizing.

Як обчислюється специфічність селекторів?

Специфічність — це вага селектора з трьох чисел: кількість ідентифікаторів, далі класів, атрибутів і псевдокласів, далі тегів і псевдоелементів. Порівняння йде зліва направо, тому один #id сильніший за будь-яку кількість класів. За однакової ваги перемагає правило, оголошене нижче.

p { color: black; }             /* 0-0-1 */
.note { color: blue; }          /* 0-1-0 */
p.note:hover { color: green; }  /* 0-2-1 */
#intro { color: red; }          /* 1-0-0 */

Інлайновий атрибут style сильніший за будь-який селектор, а !important порівнюється окремо і виграє у звичайних оголошень. Універсальний селектор і комбінатори ваги не додають, :where() завжди має нульову вагу, а :is() та :not() беруть вагу найспецифічнішого аргументу. Правила — у MDN: Specificity.

Що хочуть почути: війну специфічності не виграють через !important; стилі тримають пласкими — на класах, без ідентифікаторів і глибокої вкладеності.

Чим Flexbox відрізняється від Grid і коли що обирати?

Flexbox розкладає елементи вздовж однієї осі — у рядок або в колонку, а Grid керує рядками й колонками одночасно. Flexbox зручний, коли розміри диктує вміст: меню, панель кнопок, вирівнювання іконки з текстом. Grid — коли спершу є сітка, а вміст у неї вписується: макет сторінки, галерея карток.

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 24px;
}

Це не конкуренти: в одному макеті Grid зазвичай задає каркас сторінки, а Flexbox вирівнює вміст усередині комірок. Довідкові матеріали — MDN: Flexible box layout і MDN: Grid layout.

Які значення має position і чим вони відрізняються?

Значень п'ять: static, relative, absolute, fixed і sticky.

  • static — типове значення: елемент у звичайному потоці, властивості top, left і z-index на нього не діють.
  • relative — елемент зсувається відносно свого звичайного місця, а місце за ним зберігається.
  • absolute — елемент випадає з потоку і відраховує координати від найближчого предка з position, відмінним від static.
  • fixed — елемент прив'язаний до вікна браузера і не прокручується разом зі сторінкою.
  • sticky — елемент поводиться як звичайний, доки не дійде до заданого відступу (наприклад, top: 0), після чого «прилипає» в межах свого контейнера прокручування.

Типова помилка: поставити absolute і дивуватися, що елемент полетів у кут сторінки, — батьківському елементу забули вказати position: relative. Повний опис — у MDN: position.

Як ви робите адаптивну верстку?

Я починаю зі стилів для вузького екрана і розширюю їх медіазапитами min-width (підхід mobile-first), використовую відносні одиниці та розкладки, які перебудовуються самі. У <head> обов'язково стоїть <meta name="viewport" content="width=device-width, initial-scale=1">, інакше мобільний браузер відмалює сторінку на ширину настільного екрана і зменшить її.

.layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media (min-width: 768px) {
  .layout {
    grid-template-columns: 240px 1fr;
  }
}

h1 {
  font-size: clamp(1.75rem, 4vw, 3rem);
}

img {
  max-width: 100%;
  height: auto;
}

Додаткові інструменти: rem замість пікселів для шрифтів, функція clamp() для плавних розмірів, контейнерні запити (@container), коли компонент має реагувати на ширину батьківського елемента, а не вікна. Основа — MDN: Using media queries.

Питання з основ JavaScript

Які типи даних є в JavaScript?

У мові сім примітивних типів — string, number, bigint, boolean, undefined, null, symbol — і один тип для всього іншого, object. Масиви, функції, дати — це об'єкти. Примітиви незмінні й порівнюються за значенням, об'єкти — за посиланням.

console.log(typeof 42);             // number
console.log(typeof 'text');         // string
console.log(typeof true);           // boolean
console.log(typeof undefined);      // undefined
console.log(typeof 10n);            // bigint
console.log(typeof Symbol('id'));   // symbol
console.log(typeof null);           // object
console.log(typeof {});             // object
console.log(typeof []);             // object
console.log(typeof function () {}); // function
console.log(Array.isArray([]));     // true

Що хочуть почути: typeof null повертає 'object' — це історична особливість мови, а не ознака того, що null є об'єктом; масив перевіряють через Array.isArray(). Таблиця значень — у MDN: typeof.

Чим відрізняється == від ===?

Оператор === порівнює без перетворень: якщо типи різні, результат одразу false. Оператор == спершу приводить операнди до спільного типу, через що результат буває неочевидним. У робочому коді використовують ===.

console.log(1 == '1');            // true
console.log(1 === '1');           // false
console.log(0 == '');             // true
console.log(null == undefined);   // true
console.log(null === undefined);  // false
console.log(null == 0);           // false
console.log(NaN === NaN);         // false
console.log(Object.is(NaN, NaN)); // true
console.log('5' + 2);             // 52
console.log('5' - 2);             // 3

Два останні рядки — про неявне приведення: + із рядком склеює рядки, а - працює лише з числами і перетворює рядок на число. Єдиний загальноприйнятий виняток для == — перевірка value == null, яка ловить одразу null та undefined. Алгоритми порівняння описано в MDN: Equality comparisons and sameness.

Чим відрізняються var, let і const?

var видима в усій функції, і її можна оголосити повторно; let і const видимі лише всередині блоку {}, де оголошені. const забороняє присвоювати змінній нове значення, але не робить об'єкт незмінним.

function demo() {
  if (true) {
    var a = 1;
    let b = 2;
  }
  console.log(a);        // 1
  console.log(typeof b); // undefined
}
demo();

const user = { name: 'Olena' };
user.name = 'Ira';       // allowed: the binding is constant, not the object
console.log(user.name);  // Ira

try {
  user = {};
} catch (e) {
  console.log(e.name + ': ' + e.message); // TypeError: Assignment to constant variable.
}

Що хочуть почути: типово — const, let — коли значення справді змінюється, var у новому коді не використовують.

Що таке hoisting (підняття)?

Підняття — це те, що оголошення обробляються до виконання коду у своїй області видимості. Оголошення функції доступне повністю ще до рядка, де воно написане. Змінна var існує від початку функції, але до присвоєння дорівнює undefined. Змінні let, const і класи теж «підняті», але до рядка оголошення перебувають у тимчасовій мертвій зоні, і звернення до них кидає ReferenceError.

console.log(a); // undefined
var a = 1;

console.log(sum(2, 3)); // 5
function sum(x, y) {
  return x + y;
}

try {
  console.log(b);
} catch (e) {
  console.log(e.name + ': ' + e.message); // ReferenceError: Cannot access 'b' before initialization
}
let b = 2;

Функціональні вирази та стрілкові функції підпорядковуються правилам змінної, у яку записані: викликати їх до оголошення не можна. Термін розібрано в глосарії MDN.

Що таке замикання?

Замикання — це функція разом зі змінними зовнішньої області видимості, у якій її було створено. Функція зберігає доступ до цих змінних, навіть коли зовнішня функція вже завершилася.

function createCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2

const other = createCounter();
console.log(other()); // 1

Змінна count недоступна ззовні, але живе, доки жива функція next; other має власну копію. На замиканнях побудовано приватний стан, обробники подій, debounce і хуки React.

Класичне уточнювальне питання — що виведе цикл із таймером:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var:', i), 0);
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log('let:', j), 0);
}
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2

Із var змінна одна на весь цикл, і на момент спрацювання таймерів вона вже дорівнює 3. Із let на кожній ітерації створюється нова прив'язка, і кожна функція замикає свою. Докладніше — у MDN: Closures.

Як визначається значення this?

У звичайної функції this визначається тим, як її викликано, а не де оголошено. Виклик як методу (user.hello()) — this дорівнює об'єкту перед крапкою. Виклик через call, apply або bind — тому, що передали явно. Виклик із new — новому об'єкту. Простий виклик fn() у суворому режимі — undefined. Стрілкова функція власного this не має: вона бере його з навколишнього коду.

'use strict';

const user = {
  name: 'Olena',
  tags: ['js', 'css'],
  hello() {
    return 'Hi, ' + this.name;
  },
  describe() {
    return this.tags.map((tag) => this.name + ':' + tag);
  },
};

console.log(user.hello()); // Hi, Olena

const fn = user.hello;
try {
  fn();
} catch (e) {
  console.log(e.name); // TypeError
}

const bound = user.hello.bind(user);
console.log(bound()); // Hi, Olena
console.log(user.hello.call({ name: 'Ira' })); // Hi, Ira
console.log(user.describe()); // [ 'Olena:js', 'Olena:css' ]

Типова помилка: передати метод як колбек (button.addEventListener('click', user.hello)) і втратити контекст. Рішення — bind або стрілкова функція-обгортка. Усі випадки перелічено в MDN: this.

Як працює прототипне успадкування і що таке класи?

Кожен об'єкт має приховане посилання на інший об'єкт — прототип. Якщо властивості немає в самому об'єкті, рушій шукає її в прототипі, потім у прототипі прототипа — і так до null. Класи — це зручніший синтаксис поверх того самого механізму: методи класу лежать у prototype, а extends зв'язує прототипи в ланцюжок.

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + ' makes a sound';
  }
}

class Dog extends Animal {
  speak() {
    return super.speak() + ': woof';
  }
}

const dog = new Dog('Rex');
console.log(dog.speak());                                  // Rex makes a sound: woof
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.hasOwn(dog, 'name'));                   // true
console.log(Object.hasOwn(dog, 'speak'));                  // false
console.log(typeof Dog);                                   // function

Властивість name належить самому об'єкту, а метод speak знайдено за ланцюжком прототипів. Докладно — у MDN: Inheritance and the prototype chain.

Чим поверхнева копія об'єкта відрізняється від глибокої?

Поверхнева копія ({ ...obj }, Object.assign, [...arr]) копіює лише верхній рівень: вкладені об'єкти залишаються спільними з оригіналом. Глибока копія дублює всю структуру; вбудований спосіб — structuredClone().

const original = { name: 'Olena', tags: ['js'], address: { city: 'Kyiv' } };

const shallow = { ...original };
shallow.address.city = 'Lviv';
console.log(original.address.city); // Lviv

const deep = structuredClone(original);
deep.address.city = 'Odesa';
deep.tags.push('ts');
console.log(original.address.city); // Lviv
console.log(original.tags);         // [ 'js' ]

try {
  structuredClone({ run() {} });
} catch (e) {
  console.log(e.name);              // DataCloneError
}

structuredClone розуміє дати, Map, Set і циклічні посилання, але не копіює функції — на них він кидає помилку. Питання важливе для React: стан не можна змінювати на місці, його замінюють копією. Опис — у MDN: structuredClone().

Питання з асинхронного JavaScript

Що таке event loop (цикл подій)?

JavaScript виконує код в одному потоці: у кожен момент працює одна функція зі стека викликів. Цикл подій — це механізм, який, коли стек порожній, бере наступне завдання з черги і запускає його. Завдяки йому таймери, мережеві запити й події користувача не блокують сторінку: очікування відбувається поза потоком JavaScript, а в чергу потрапляє лише колбек.

Один оберт циклу в браузері має такий вигляд:

  1. взяти з черги одне завдання (скрипт, колбек таймера, обробник події) і виконати його до кінця;
  2. виконати всі накопичені мікрозавдання, зокрема ті, що додалися в процесі;
  3. за потреби перемалювати сторінку;
  4. перейти до наступного завдання.

Що хочуть почути: довга синхронна операція блокує все — і обробку кліків, і відмальовування, бо цикл не може перейти до наступного кроку. Модель описано у стандарті HTML.

Чим мікрозавдання відрізняються від макрозавдань?

Макрозавдання (у стандарті — просто завдання) — це колбеки setTimeout і setInterval, обробники подій, мережеві колбеки. Мікрозавдання — обробники промісів (then, catch, finally), продовження функції після await і queueMicrotask(). Після кожного макрозавдання черга мікрозавдань виконується повністю, тому проміс завжди випереджає setTimeout(fn, 0).

console.log('1: sync start');

setTimeout(() => console.log('2: timeout (macrotask)'), 0);

Promise.resolve().then(() => console.log('3: promise (microtask)'));

queueMicrotask(() => console.log('4: queueMicrotask (microtask)'));

console.log('5: sync end');
1: sync start
5: sync end
3: promise (microtask)
4: queueMicrotask (microtask)
2: timeout (macrotask)

Другий приклад показує, що мікрозавдання виконуються між двома таймерами, а не після всіх:

setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => console.log('microtask inside timeout 1'));
}, 0);

setTimeout(() => console.log('timeout 2'), 0);

Promise.resolve()
  .then(() => console.log('microtask 1'))
  .then(() => console.log('microtask 2'));

console.log('sync');
sync
microtask 1
microtask 2
timeout 1
microtask inside timeout 1
timeout 2

Розбір черг — у MDN: Using microtasks.

Що таке проміс і які в нього стани?

Проміс — це об'єкт, який представляє результат асинхронної операції. Він має три стани: очікування (pending), виконано (fulfilled) і відхилено (rejected). Вийти з очікування можна лише один раз, після цього стан і значення не змінюються.

Для кількох промісів є чотири статичні методи, і про різницю між ними питають часто:

const ok = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));

async function main() {
  console.log(await Promise.all([ok('a', 20), ok('b', 10)])); // [ 'a', 'b' ]

  try {
    await Promise.all([ok('a', 20), fail('boom', 10)]);
  } catch (e) {
    console.log('all rejected:', e.message); // all rejected: boom
  }

  const settled = await Promise.allSettled([ok('a', 20), fail('boom', 10)]);
  console.log(settled.map((r) => r.status)); // [ 'fulfilled', 'rejected' ]

  console.log(await Promise.race([ok('slow', 30), ok('fast', 10)])); // fast
  console.log(await Promise.any([fail('boom', 10), ok('first success', 30)])); // first success
}

main();
  • Promise.all — чекає всі і падає на першій помилці; результати йдуть у порядку вихідного масиву, а не в порядку завершення.
  • Promise.allSettled — чекає всі і ніколи не падає, повертає статус кожного.
  • Promise.race — повертає перший завершений, успішно чи з помилкою.
  • Promise.any — повертає перший успішний.

Довідник — MDN: Promise.

Як працює async/await і як обробляти в ньому помилки?

Функція з async завжди повертає проміс, а await призупиняє її до завершення проміса, не блокуючи потік: решта коду продовжує виконуватися. Помилку відхиленого проміса ловлять звичайним try/catch.

const load = (name, ms) => new Promise((resolve) => setTimeout(() => resolve(name), ms));

async function sequential() {
  const start = Date.now();
  const user = await load('user', 100);
  const posts = await load('posts', 100);
  return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}

async function parallel() {
  const start = Date.now();
  const [user, posts] = await Promise.all([load('user', 100), load('posts', 100)]);
  return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}

async function withError() {
  try {
    await Promise.reject(new Error('network error'));
  } catch (e) {
    return 'caught: ' + e.message;
  }
}

async function main() {
  console.log(await sequential()); // [ 'user', 'posts', 200 ]
  console.log(await parallel());   // [ 'user', 'posts', 100 ]
  console.log(await withError());  // caught: network error
}

main();

Третє число — час виконання в мілісекундах, округлений до сотень.

Типова помилка: ставити await поспіль для незалежних запитів. Вони виконуються по черзі й забирають удвічі більше часу; незалежні операції запускають разом через Promise.all. Довідник — MDN: async function.

FE

Ті самі теми — з тренером і code review

Курс Front-End у Prog Academy складається з модулів AI Start, «Верстка HTML/CSS» і JavaScript та триває 4,5 місяця. У програмі — Flexbox і Grid, прототипи, події та DOM, Promise і Fetch, React та/або Vue.js, збірка проєктів і дипломний проєкт для портфоліо з code review від викладача.

Програма курсу Front-End →

Питання з DOM і браузера

Що таке спливання і перехоплення подій?

Подія в DOM проходить три фази: перехоплення (від window вниз до елемента), ціль (сам елемент) і спливання (від елемента назад угору по предках). Типово обробники, додані через addEventListener, спрацьовують на фазі спливання; щоб упіймати подію на перехопленні, третім аргументом передають { capture: true }.

Що зазвичай уточнюють:

  • event.target — елемент, на якому подія сталася; event.currentTarget — елемент, на якому висить обробник, що виконується;
  • event.stopPropagation() зупиняє подальше поширення, а event.preventDefault() скасовує типову дію браузера — перехід за посиланням або надсилання форми. Це різні речі;
  • спливають не всі події: focus і blur не спливають, для них є спливні аналоги focusin та focusout.

Наочне пояснення — у MDN: Event bubbling.

Що таке делегування подій?

Делегування — це один обробник на спільному предку замість окремого обробника на кожному дочірньому елементі. Працює завдяки спливанню: клік по кнопці всередині списку дійде до самого списку, а потрібний елемент знаходять через event.target.

const list = document.querySelector('#todo-list');

list.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-action="remove"]');
  if (!button || !list.contains(button)) return;

  button.closest('li').remove();
});

Переваг дві: менше обробників у пам'яті й автоматична робота з елементами, які додали до списку пізніше. Метод closest() потрібен, бо клік може припасти на іконку всередині кнопки, а не на саму кнопку.

Чим відрізняються localStorage, sessionStorage і cookie?

Усі три зберігають рядки в браузері, але живуть і передаються по-різному. localStorage зберігає дані безстроково, доки їх не видалять. sessionStorage живе до закриття вкладки, і кожна вкладка має власне. Cookie мають термін життя і, на відміну від сховищ, автоматично надсилаються на сервер із кожним запитом.

ВластивістьlocalStoragesessionStorageCookie
Термін життяДоки не видалятьДо закриття вкладкиЗадається через Expires або Max-Age; без них — до кінця сесії браузера
Надсилання на серверНіНіАвтоматично з кожним запитом
Доступ із JavaScriptТакТакТак, якщо не виставлено прапорець HttpOnly
ОбсягКілька мегабайтів на джерело, залежить від браузераКілька мегабайтів на джерело, залежить від браузераБлизько 4 КБ на одну cookie

Що хочуть почути: усе, що лежить у localStorage, може прочитати будь-який скрипт на сторінці, тому за XSS-вразливості токен звідти вкрадуть. Cookie з прапорцями HttpOnly, Secure і SameSite з JavaScript недоступна. Опис API — MDN: Web Storage API.

Що таке CORS і чому браузер блокує запит?

За правилом одного джерела (same-origin policy) скрипт сторінки не може читати відповіді з іншого джерела, а джерело — це поєднання протоколу, домену і порту. CORS — механізм, яким сервер дозволяє такі запити: він додає у відповідь заголовок Access-Control-Allow-Origin з адресою сторінки, і браузер віддає відповідь скрипту.

Для «непростих» запитів — наприклад, із методом PUT чи DELETE, з тілом у форматі JSON або з нестандартними заголовками — браузер спершу надсилає попередній запит OPTIONS (preflight) і лише після дозвільної відповіді робить основний.

Що хочуть почути: CORS — це обмеження браузера, а не сервера: той самий запит із curl або з іншого сервера пройде. Тому помилку CORS виправляють налаштуванням сервера або проксі, а не кодом на фронтенді. Деталі — у MDN: Cross-Origin Resource Sharing.

Що відбувається після того, як користувач увів адресу й натиснув Enter?

Браузер знаходить IP-адресу сервера через DNS, встановлює з'єднання (TCP і TLS для HTTPS), надсилає HTTP-запит і отримує HTML. Далі починається відмальовування:

  1. з HTML будується дерево DOM, з CSS — дерево стилів CSSOM;
  2. з них збирається дерево рендерингу — лише видимі елементи з обчисленими стилями;
  3. layout — обчислюються розміри та положення кожного елемента;
  4. paint — елементи відмальовуються в шари;
  5. composite — шари збираються в підсумкову картинку на екрані.

CSS блокує відмальовування: доки стилі не завантажено, сторінку не показують. Звичайний <script> блокує розбір HTML, тому скрипти підключають із defer (виконуються після розбору документа, по порядку) або async (виконуються одразу після завантаження, у довільному порядку); скрипти з type="module" відкладені типово. Докладніше — web.dev: Critical rendering path.

Питання з TypeScript і React

Чим type відрізняється від interface у TypeScript?

Обидва способи описують форму об'єкта, і здебільшого вони взаємозамінні. Різниця в можливостях: interface розширюється через extends і допускає злиття оголошень з однаковим іменем, а type уміє те, чого інтерфейс не може, — об'єднання, перетини, псевдоніми примітивів і кортежів.

interface User {
  id: number;
  name: string;
  email?: string;
}

interface Admin extends User {
  permissions: string[];
}

type Status = 'idle' | 'loading' | 'error';
type WithTimestamps<T> = T & { createdAt: Date };

const admin: Admin = { id: 1, name: 'Olena', permissions: ['users:edit'] };
const status: Status = 'loading';
const stored: WithTimestamps<User> = { id: 2, name: 'Ira', createdAt: new Date(0) };

console.log(admin.permissions.length, status, stored.createdAt.getTime()); // 1 loading 0

Що хочуть почути: головне — одноманітність у проєкті. Рекомендація з TypeScript Handbook — використовувати interface, доки не знадобляться можливості type. І варто додати, що типи існують лише на етапі компіляції: у підсумковому JavaScript їх немає, тому дані із сервера потрібно перевіряти окремо.

Чим any відрізняється від unknown і навіщо потрібен never?

any вимикає перевірку типів: зі значенням можна робити що завгодно, і помилка проявиться лише під час виконання. unknown теж приймає будь-яке значення, але не дає нічого з ним зробити, доки ви не звузите тип перевіркою. never — тип, який не має значень: його повертають функції, що завжди кидають помилку, і ним перевіряють, що у switch розібрано всі варіанти.

function parse(json: string): unknown {
  return JSON.parse(json);
}

const data = parse('{"name":"Olena"}');

// console.log(data.name); // error TS18046: 'data' is of type 'unknown'.

if (typeof data === 'object' && data !== null && 'name' in data) {
  console.log(data.name); // Olena
}

function assertNever(value: never): never {
  throw new Error('Unexpected value: ' + String(value));
}

type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
    default:
      return assertNever(shape);
  }
}

console.log(area({ kind: 'square', side: 3 })); // 9

Якщо до Shape додати третій варіант і забути про нього у switch, компілятор вкаже на рядок з assertNever. Звуження типів описано в TypeScript Handbook: Narrowing.

Що таке дженерики і навіщо вони потрібні?

Дженерик — це параметр типу: функція або тип описуються один раз, а конкретний тип підставляється під час використання. Так код залишається універсальним і водночас типобезпечним, без any.

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([1, 2, 3]);   // number | undefined
const s = first(['a', 'b']);  // string | undefined

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

const users = [
  { id: 1, name: 'Olena' },
  { id: 2, name: 'Ira' },
];

console.log(n, s, pluck(users, 'name')); // 1 a [ 'Olena', 'Ira' ]
// pluck(users, 'age'); // error TS2345: Argument of type '"age"' is not assignable to parameter of type '"name" | "id"'.

Запис K extends keyof T — обмеження: другим аргументом можна передати лише ім'я наявної властивості. Від джуна зазвичай достатньо вміння читати такі сигнатури; писати складні — рівень Middle. Розділ довідника — TypeScript Handbook: Generics.

Чим props відрізняються від state у React?

Props — це дані, які компонент отримує від батьківського компонента; змінювати їх усередині компонента не можна. State — це власні дані компонента, які зберігаються між рендерами; їх змінюють лише функцією-сетером, і це спричиняє новий рендер.

import { useState } from 'react';

function Counter({ step = 1 }) {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount((current) => current + step)}>
      Clicked: {count}
    </button>
  );
}

Тут step — props, count — state. Два уточнення, які варто назвати самому. Стан не змінюють на місці (items.push(...)), а замінюють новим значенням, інакше React не побачить зміни. І значення стану всередині одного рендеру не змінюється одразу після виклику сетера, тому, коли нове значення залежить від попереднього, у сетер передають функцію. Пояснення — у react.dev: State: A Component's Memory.

Навіщо потрібні ключі (key) у списках?

Ключ дає React змогу зрозуміти, який елемент списку якому відповідає між рендерами: що додали, що видалили, що переставили. Ключ має бути стабільним та унікальним серед сусідів — зазвичай це ідентифікатор із даних.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Типова помилка: використовувати індекс масиву як ключ. Якщо елементи вставляються, видаляються або сортуються, індекси зсуваються, і стан компонента — наприклад, текст у полі введення — опиняється в іншого елемента. Ключ із Math.random() ще гірший: елементи створюються заново на кожному рендері. Правила — у react.dev: Rendering Lists.

Як працює useEffect і навіщо потрібна функція очищення?

useEffect синхронізує компонент із чимось зовнішнім щодо React: підпискою, таймером, мережевим з'єднанням, стороннім віджетом. Ефект запускається після того, як React оновив екран. Масив залежностей визначає, коли запускати його знову: без масиву — після кожного рендеру, з порожнім масивом — один раз після появи компонента, з переліком значень — коли хоча б одне з них змінилося.

import { useEffect, useState } from 'react';

function OnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const update = () => setOnline(navigator.onLine);

    window.addEventListener('online', update);
    window.addEventListener('offline', update);

    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);

  return <p>{online ? 'Online' : 'Offline'}</p>;
}

Функція, яку повертає ефект, — очищення: React викликає її перед повторним запуском ефекту і під час видалення компонента. Без неї залишаються «завислі» підписки й таймери. У режимі розробки зі Strict Mode React один раз додатково запускає ефект і очищення, щоб такі помилки були помітні.

Що хочуть почути: ефект не потрібен, щоб обчислити щось із props або state, — це рахують просто під час рендеру. Довідник — react.dev: useEffect.

Коли компонент React рендериться заново?

Причин три: змінився стан самого компонента, заново відрендерився його батьківський компонент або змінилося значення контексту, який компонент читає. Важливо: під час рендеру батьківського компонента дочірні рендеряться типово, навіть якщо їхні props не змінилися.

Рендер — це виклик функції компонента, а не оновлення сторінки. Після нього React порівнює результат із попереднім і вносить у DOM лише відмінності, тому «зайвий» рендер не завжди проблема. Якщо він став дорогим, є інструменти: memo пропускає рендер компонента за тих самих props, useMemo запам'ятовує результат обчислення, useCallback — саму функцію. Проєкти з React Compiler отримують таку мемоізацію автоматично на етапі збірки.

Що хочуть почути: спершу виміряти в React DevTools Profiler, потім оптимізувати. Процес описано в react.dev: Render and Commit.

Які правила мають хуки і чому?

Правил два: хуки викликають лише на верхньому рівні компонента або власного хука — не в умовах, циклах і вкладених функціях, — і лише з React-функцій, а не зі звичайних. Причина в тому, що React зіставляє стан із хуком за порядком викликів: якщо на одному рендері хук викликано, а на іншому пропущено, значення зсунуться.

Виняток у сучасному React — функція use: її можна викликати всередині умов і циклів. Власний хук — це звичайна функція з іменем на use, яка викликає інші хуки; так виносять повторювану логіку на кшталт підписки з прикладу вище. Дотримання правил перевіряє лінтер — плагін eslint-plugin-react-hooks. Джерело — react.dev: Rules of Hooks.

Питання з інструментів і продуктивності

Навіщо потрібен збирач і що він робить?

Збирач перетворює вихідний код проєкту на файли, які швидко завантажуються в браузері. Він проходить графом імпортів, перетворює TypeScript і JSX на JavaScript, об'єднує модулі, видаляє невикористаний код (tree shaking), мініфікує результат і додає до імен файлів хеш вмісту, щоб браузер міг довго їх кешувати.

Ще два завдання: розділення коду (code splitting) — сторінка завантажує лише те, що потрібно їй зараз, а решта довантажується на вимогу через динамічний import(), — і сервер розробки з миттєвим оновленням модулів без перезавантаження сторінки. Найвідоміші збирачі — Vite і webpack. Мотивацію добре описано в документації Vite.

Що таке Core Web Vitals?

Core Web Vitals — це три метрики Google, які описують, як користувач відчуває швидкість сторінки:

МетрикаЩо вимірюєДобре значення
LCP (Largest Contentful Paint)Коли відмальовано найбільший елемент у видимій області — швидкість завантаженняДо 2,5 секунди
INP (Interaction to Next Paint)Скільки минає від дії користувача до наступного відмальовування — чуйністьДо 200 мілісекунд
CLS (Cumulative Layout Shift)Наскільки елементи зсуваються під час завантаження — візуальна стабільністьДо 0,1

Пороги оцінюються за 75-м перцентилем завантажень сторінки. Що допомагає: стиснені зображення в сучасних форматах і пріоритет для головного зображення (LCP), короткі обробники та розбиття довгих завдань (INP), атрибути width і height у зображень та зарезервоване місце під банери (CLS). Визначення і пороги — на web.dev: Web Vitals.

Чим git merge відрізняється від git rebase?

git merge з'єднує дві гілки і, якщо історії розійшлися, створює окремий коміт злиття: історія зберігається такою, якою була. git rebase переносить ваші коміти поверх іншої гілки, створюючи їх заново з новими хешами: історія виходить лінійною, але переписаною.

Що хочуть почути: правило з книжки Pro Git — не робити rebase комітів, які вже надіслано до спільного репозиторію і на яких хтось міг побудувати свою роботу. Власну локальну гілку перед pull request перебазувати можна, спільну — ні. Пояснення з рисунками — у Pro Git: Rebasing, а базові команди розібрано у статті Git для початківців.

Практичні задачі з розв'язками

На практичній частині джуну зазвичай дають невеликі задачі на функції, масиви та асинхронність — такі, що розв'язуються за 10–15 хвилин. Нижче чотири типові задачі; усі розв'язки запущено в Node.js v22.14.0.

Задача 1. Напишіть функцію debounce

Потрібно повернути функцію, яка викликає вихідну лише після того, як виклики припинилися на заданий час. Так роблять пошук під час введення: запит іде, коли користувач перестав друкувати.

function debounce(fn, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), delay);
  };
}

const search = debounce((query) => console.log('search:', query), 100);

search('r');
search('re');
search('react'); // only this call reaches the function
search: react

Що варто проговорити: timerId живе в замиканні, кожен новий виклик скасовує попередній таймер, а fn.apply(this, args) зберігає контекст та аргументи.

Задача 2. Розгорніть вкладений масив без методу flat

function flatten(items) {
  const result = [];
  for (const item of items) {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

console.log(flatten([1, [2, [3, [4]], 5]]));       // [ 1, 2, 3, 4, 5 ]
console.log([1, [2, [3, [4]], 5]].flat(Infinity)); // [ 1, 2, 3, 4, 5 ]

Розв'язок рекурсивний: якщо елемент — масив, функція викликає сама себе. Після ручного розв'язку назвіть вбудований спосіб — flat(Infinity): інтерв'юеру важливо, що ви знаєте обидва.

Задача 3. Згрупуйте масив об'єктів за полем

function groupBy(items, getKey) {
  return items.reduce((groups, item) => {
    const key = getKey(item);
    (groups[key] ??= []).push(item);
    return groups;
  }, {});
}

const users = [
  { name: 'Olena', role: 'dev' },
  { name: 'Ira', role: 'qa' },
  { name: 'Taras', role: 'dev' },
];

console.log(groupBy(users, (user) => user.role));
{
  dev: [ { name: 'Olena', role: 'dev' }, { name: 'Taras', role: 'dev' } ],
  qa: [ { name: 'Ira', role: 'qa' } ]
}

Оператор ??= створює масив для ключа, лише якщо його ще немає. У сучасних середовищах є вбудований Object.groupBy() з ES2024 — згадайте його після власного розв'язку.

Задача 4. Що виведе цей код і в якому порядку?

async function first() {
  console.log('A');
  await second();
  console.log('B');
}

async function second() {
  console.log('C');
}

console.log('D');
setTimeout(() => console.log('E'), 0);
first();
Promise.resolve().then(() => console.log('F'));
console.log('G');
D
A
C
G
B
F
E

Пояснення за кроками. D — синхронний код. Виклик first() виконується синхронно до першого await, тому одразу друкуються A і C. Решта first після await іде в чергу мікрозавдань, слідом туди ж потрапляє обробник then. Синхронний код закінчується рядком G, далі по порядку виконуються мікрозавдання — B і F, і лише потім макрозавдання таймера — E.

Типові помилки кандидатів

Більшість відмов на рівні Junior пов'язана не з прогалиною в одній темі, а з тим, як кандидат відповідає і міркує. Це спостереження автора, а не статистика:

  • Завчені визначення без прикладу. «Замикання — це функція та її лексичне оточення» нічого не доводить, доки ви не покажете лічильник або debounce.
  • Фреймворк замість мови. Знання хуків React не рятує, якщо незрозуміло, як працює this чи цикл подій.
  • Мовчання під час live coding. Інтерв'юер оцінює міркування; тишу він оцінити не може.
  • Розв'язок без уточнень. Що на вході? Чи можуть бути порожні значення? Поставити два питання перед початком — ознака досвіду, а не слабкості.
  • Вигадана відповідь замість «не знаю». Наступне уточнювальне питання це покаже.
  • Проєкт, який кандидат не може пояснити. Особливо якщо код писав AI-асистент, а ви його не розбирали.
  • Ігнорування HTML і доступності. «Я все роблю на div» — швидкий спосіб втратити бали вже на перших питаннях.
  • Відсутність питань до інтерв'юера. Це сприймається як відсутність інтересу до роботи.

Які питання поставити інтерв'юеру?

Співбесіда — двостороння розмова: вам теж потрібно зрозуміти, як тут працюють і чи будете ви зростати. Оберіть три-чотири питання зі списку:

  • Над яким продуктом або проєктом я працюватиму і який у нього стек?
  • Як влаштована команда: скільки фронтенд-розробників, хто буде моїм наставником?
  • Як проходить code review і скільки часу зазвичай триває?
  • Якими будуть мої завдання в перший місяць?
  • Який вигляд має процес від завдання до релізу: гілки, тести, деплой?
  • Як у команді ставляться до використання AI-асистентів у роботі?
  • За якими критеріями оцінюють результат випробувального терміну?
  • Яким буде наступний етап відбору і коли чекати на відповідь?

План підготовки на два тижні

План розрахований на тих, хто вже вивчив основи і має хоча б один власний проєкт: по півтори-дві години на день на повторення і тренування відповідей уголос. Якщо якась тема для вас нова, двох тижнів на неї не вистачить — спершу вивчіть її за планом зі статті Як стати Front-End розробником.

ДніТемаЩо зробити
1–2HTML і доступністьЗверстати форму з підписами та перевіркою, пройти її з клавіатури, відповісти вголос на питання блоку
3–4CSSЗібрати макет на Grid і Flexbox з однією контрольною точкою, пояснити вагу трьох своїх селекторів
5–7Основи JavaScriptЗапустити кожен приклад із блоку, змінити його і передбачити вивід до запуску
8–9АсинхронністьНамалювати схему циклу подій, розв'язати п'ять задач «що виведе код»
10DOM і браузерНаписати список із делегуванням подій, розповісти шлях від URL до відмальовування
11TypeScriptТипізувати функції зі свого проєкту, замінити кожен any на точний тип або unknown
12ReactПояснити вголос кожен useEffect у своєму проєкті: навіщо він, які залежності, чи потрібне очищення
13Практика та інструментиРозв'язати задачі з розділу вище з таймером; повторити Git: гілки, конфлікт, rebase
14Пробна співбесідаПопросити знайомого поставити питання або записати відповіді на диктофон; підготувати розповідь про себе і проєкт

Для щоденної практики з автоматичною перевіркою завдань підійдуть безплатний курс із JavaScript і безплатний курс із Git. Якщо ви ще вагаєтеся щодо напряму, подивіться порівняння Frontend чи Backend: що обрати.

Часті запитання

Скільки часу потрібно на підготовку до співбесіди на Junior Front-End?

Якщо основи вже вивчено і є власні проєкти, на повторення теорії та тренування відповідей уголос зазвичай вистачає двох тижнів по півтори-дві години на день. Це орієнтир автора, а не норма: якщо тема зі списку питань для вас нова, її спершу треба вивчити й закріпити практикою, а на це йдуть тижні, а не вечори.

Чи буде на співбесіді live coding?

Найімовірніше, так, у тому чи іншому вигляді: написати невелику функцію, передбачити вивід коду або виправити верстку, коментуючи свої дії. Формат залежить від компанії, тому запитайте про нього рекрутера заздалегідь. Оцінюють не швидкість набору, а хід міркувань: проговорюйте, що робите і чому.

Чи потрібна англійська на співбесіді Front-End розробника?

Читати англійською потрібно в будь-якому разі: нею написані документація, повідомлення про помилки та назви в коді. Розмовну англійську перевіряють, якщо команда або замовник спілкуються нею, і потрібний рівень зазвичай зазначено у вакансії. Частину співбесіди можуть провести англійською, тому підготуйте розповідь про себе і про свій проєкт двома мовами.

Чи можна користуватися AI-асистентами на співбесіді та в тестовому завданні?

На співбесіді — лише якщо інтерв'юер прямо дозволив; якщо правила не названо, запитайте. У тестовому завданні асистенти зазвичай допустимі, але за кожен рядок відповідаєте ви: на розборі попросять пояснити рішення і змінити його на ходу. Код, який ви не можете пояснити, шкодить більше, ніж його відсутність.

Що робити, якщо не знаєш відповіді на питання?

Сказати про це прямо і показати, як ви міркуєте: що знаєте із суміжної теми, як перевірили б припущення, де подивилися б у документації. Вигадувати відповідь гірше, ніж зізнатися: наступне уточнювальне питання це покаже. Після співбесіди запишіть питання і розберіть його — воно трапиться знову.

Чи питають алгоритми на співбесіді Junior Front-End?

Складних алгоритмів від джуна у фронтенді вимагають рідко, але базові задачі на масиви, рядки та об'єкти трапляються часто: згрупувати, відфільтрувати, розгорнути вкладений масив, прибрати дублікати. Потрібно впевнено володіти методами масивів і розуміти, скільки разів ваш код проходить по даних.

Як виконувати тестове завдання?

Спершу зробіть те, що написано в умові, і лише потім покращення. Додайте README з інструкцією запуску і переліком ухвалених рішень, перевірте верстку на мобільній ширині та керування з клавіатури, робіть осмислені коміти. Якщо термін або умова незрозумілі, поставте питання до початку роботи — це нормальна робоча практика.

Чи потрібно знати React, щоб пройти співбесіду на Junior?

Дивіться на вакансію: якщо в ній зазначено React, питання про компоненти, стан і хуки будуть обов'язково. Але фреймворк не замінює основ: кандидата, який пише на React і не може пояснити замикання чи цикл подій, відсіюють на блоці з JavaScript.

Що розповідати про себе, якщо комерційного досвіду немає?

Розкажіть про проєкти: яку задачу розв'язували, які технології обрали і чому, що було найважчим і що ви переробили б зараз. Двох-трьох хвилин достатньо. Не видавайте навчальні проєкти за комерційні — це з'ясовується за кілька уточнювальних питань.

Джерела

Курс Front-End у Prog Academy →

Фото автора: Максим Сластен

Про автора

Front-End розробник у DataScope Systems, тренер Prog Academy

Front-End розробник у DataScope Systems, у фронтенд-розробці з 2014 року. Викладає понад 9 років, автор усіх курсів Prog Academy з Front-End; у блозі пише про фронтенд, JavaScript і TypeScript.

Контакт

Записатися на консультацію

Вкажіть актуальний номер, ми зателефонуємо в будь яку країну :)