2026년 자바스크립트 면접 질문 50선 & 완벽 답변
2026년 자바스크립트 면접 질문 50선 & 완벽 답변
자바스크립트 기술 면접을 완벽하게 준비하세요. ES2026, async/await, 클로저, React, Node.js, 시스템 설계를 다루는 50개의 핵심 질문과 상세한 답변.
2026년 자바스크립트 면접 완벽 준비
자바스크립트는 여전히 가장 수요가 높은 프로그래밍 언어 중 하나이며, 기술 면접은 그 어느 때보다 엄격해졌습니다. 이 가이드는 2026년 자바스크립트 면접에서 가장 자주 묻는 50개의 질문과 현재 모범 사례를 반영한 상세한 답변을 담고 있습니다.
핵심 자바스크립트 기초
1. var, let, const의 차이점은?
var 는 함수 스코프(또는 함수 외부에서 선언된 경우 전역 스코프)를 가지며 undefined 초기값으로 스코프 상단에 호이스팅됩니다.
let 은 블록 스코프를 가지며 호이스팅되지만 초기화되지 않습니다(일시적 사각지대). 재할당 가능합니다.
const 는 블록 스코프를 가지며 호이스팅되지만 초기화되지 않고, 재할당할 수 없습니다. 주의: const는 객체를 불변으로 만들지 않습니다 — 객체 속성은 여전히 변경할 수 있습니다.
var x = 1;
let y = 2;
const z = 3;
function example() {
console.log(a); // undefined (호이스팅)
var a = 5;
// console.log(b); // ReferenceError: 초기화 전에 'b'에 접근 불가
let b = 6;
}
const obj = { value: 1 };
obj.value = 2; // 허용됨
// obj = {}; // 오류 발생
원칙: 기본적으로 const 사용, 재할당이 필요할 때 let, 현대 코드에서는 절대 var 사용 금지.
2. 클로저를 실용적인 예시로 설명하세요.
클로저는 외부 함수가 반환된 후에도 외부 스코프의 변수에 접근을 유지하는 함수입니다.
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
// count 변수는 외부에서 접근 불가
클로저는 주로 다음 용도로 사용됩니다:
- 데이터 프라이버시와 캡슐화
- 함수 팩토리
- 메모이제이션
- 콜백에서 상태 유지
3. 이벤트 위임이란 무엇이고 왜 사용하나요?
이벤트 위임은 여러 자식 요소에 여러 리스너를 붙이는 대신 하나의 이벤트 리스너를 부모 요소에 붙이는 것입니다.
// 위임 없이 (동적 목록에 부적합)
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// 위임 사용 (현재 및 미래 요소에 작동)
document.querySelector('.list').addEventListener('click', (event) => {
if (event.target.classList.contains('item')) {
handleClick(event.target);
}
});
장점: 많은 요소에 더 나은 성능, 동적으로 추가된 요소에 작동, 더 간단한 메모리 관리.
4. 자바스크립트 이벤트 루프를 설명하세요.
자바스크립트는 단일 스레드이지만 이벤트 루프를 통해 비동기 작업을 처리합니다.
구성 요소:
- 콜 스택: 동기 코드가 실행되는 곳
- 웹 API: 스택 외부에서 실행되는 브라우저 기능 (setTimeout, fetch, DOM 이벤트)
- 콜백 큐(태스크 큐): 실행을 기다리는 웹 API의 콜백
- 마이크로태스크 큐: Promise와 MutationObserver 콜백 (콜백 큐보다 우선순위 높음)
- 이벤트 루프: 콜 스택이 비어있는지 지속적으로 확인하고 큐에서 태스크를 이동
console.log('1 - 시작');
setTimeout(() => console.log('4 - setTimeout'), 0);
Promise.resolve()
.then(() => console.log('3 - Promise 마이크로태스크'));
console.log('2 - 끝');
// 출력: 1, 2, 3, 4
// 마이크로태스크(Promise)는 항상 매크로태스크(setTimeout) 전에 실행
5. ==와 ===의 차이점은?
==는 비교 전에 타입 강제 변환을 수행합니다. ===는 강제 변환 없이 값과 타입을 모두 비교합니다.
0 == false // true (false가 0으로 강제 변환)
0 === false // false (타입이 다름)
'' == false // true (둘 다 0으로 강제 변환)
null == undefined // true (특별한 경우)
null === undefined // false
// 타입 강제 변환이 특별히 필요하지 않으면 항상 === 사용
비동기 자바스크립트
6. Promise와 세 가지 상태를 설명하세요.
Promise는 비동기 작업의 최종 결과를 나타냅니다. 세 가지 상태:
- 대기(Pending): 초기 상태, 이행도 거부도 아님
- 이행(Fulfilled): 작업이 성공적으로 완료, 값이 있음
- 거부(Rejected): 작업이 실패, 이유(오류)가 있음
const fetchUser = (id) => new Promise((resolve, reject) => {
if (id <= 0) {
reject(new Error('유효하지 않은 ID'));
return;
}
setTimeout(() => resolve({ id, name: '김철수' }), 100);
});
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error))
.finally(() => console.log('완료'));
7. async/await란 무엇이고 Promise와 어떤 관계인가요?
async/await는 Promise 위의 문법적 설탕으로, 비동기 코드가 동기 코드처럼 보이고 동작하게 합니다.
// Promise 체인
function getUserData(userId) {
return fetchUser(userId)
.then(user => fetchUserPosts(user.id))
.then(posts => ({ user, posts }))
.catch(error => { throw new Error(`실패: ${error.message}`); });
}
// async/await 동등 표현
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(user.id);
return { user, posts };
} catch (error) {
throw new Error(`실패: ${error.message}`);
}
}
8. 여러 Promise를 동시에 처리하는 방법은?
// 순차적 (느림 - 각각을 기다림)
const user = await fetchUser(id);
const profile = await fetchProfile(id); // user를 먼저 기다림
// 동시 (빠름 - 병렬로 실행)
const [user, profile] = await Promise.all([
fetchUser(id),
fetchProfile(id)
]);
// Promise.allSettled - 모두 기다리고 실패 포함
const results = await Promise.allSettled([
fetchUser(id),
fetchProfile(id),
fetchSettings(id)
]);
results.forEach(result => {
if (result.status === 'fulfilled') console.log(result.value);
else console.error(result.reason);
});
객체지향 자바스크립트
9. 프로토타입 상속을 설명하세요.
모든 자바스크립트 객체는 프로토타입이라는 다른 객체에 대한 내부 링크를 가지고 있습니다. 속성에 접근할 때 자바스크립트는 먼저 해당 객체를 보고, 그 다음 프로토타입을 보고, 그 다음 프로토타입의 프로토타입을 보는 식으로 프로토타입 체인을 형성합니다.
const animal = {
speak() {
return `${this.name}이(가) 소리를 냅니다`;
}
};
const dog = Object.create(animal);
dog.name = '멍멍이';
dog.bark = function() { return `${this.name}이(가) 짖습니다`; };
console.log(dog.speak()); // "멍멍이이(가) 소리를 냅니다" (프로토타입에서)
console.log(dog.bark()); // "멍멍이이(가) 짖습니다" (자체 속성)
10. ES6 클래스와 프로토타입 기반 패턴의 차이점은?
ES6 클래스는 프로토타입 상속 위의 문법적 설탕입니다. 내부적으로 여전히 프로토타입을 사용합니다.
class Animal {
#name; // 비공개 필드 (ES2022)
constructor(name) {
this.#name = name;
}
get name() { return this.#name; }
speak() {
return `${this.#name}이(가) 소리를 냅니다`;
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
#breed;
constructor(name, breed) {
super(name);
this.#breed = breed;
}
speak() {
return `${super.speak()} - 멍멍!`;
}
}
함수형 프로그래밍
11. map, filter, reduce를 설명하세요.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: 각 요소 변환
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter: 조건에 맞는 요소 선택
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]
// reduce: 단일 값으로 누적
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 55
// 조합: 짝수의 제곱의 합
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n ** 2)
.reduce((acc, n) => acc + n, 0);
// 220
12. 커링이란 무엇인가요?
커링은 여러 인수를 가진 함수를 각각 단일 인수를 취하는 함수들의 시퀀스로 변환합니다.
// 일반 함수
const add = (a, b, c) => a + b + c;
console.log(add(1, 2, 3)); // 6
// 커링된 버전
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
// 실용적 사용: 특화된 함수 생성
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
모던 자바스크립트 (ES2020-2026)
13. 옵셔널 체이닝(?.)이란?
옵셔널 체이닝을 사용하면 각 레벨을 null 또는 undefined로 명시적으로 확인하지 않고 깊이 중첩된 속성에 접근할 수 있습니다.
const user = {
profile: {
address: {
city: '서울'
}
}
};
// 이전 방식
const city = user && user.profile && user.profile.address && user.profile.address.city;
// 옵셔널 체이닝
const city = user?.profile?.address?.city; // '서울'
const zip = user?.profile?.address?.zip; // undefined (오류 없음)
14. null 병합 연산자(??)란?
??는 왼쪽 값이 null 또는 undefined인 경우에만 오른쪽 값을 반환합니다(다른 falsy 값은 해당 없음).
const config = {
timeout: 0, // 유효한 값
retries: null, // 명시적 null
name: '', // 빈 문자열, 유효
};
// || 는 0과 ''를 falsy로 처리 (잘못된 결과!)
console.log(config.timeout || 5000); // 5000 (잘못됨!)
// ?? 는 null/undefined에 대해서만 기본값 설정
console.log(config.timeout ?? 5000); // 0 (올바름!)
console.log(config.name ?? '기본값'); // '' (올바름!)
console.log(config.retries ?? 3); // 3 (올바름!)
15. structuredClone이란?
structuredClone은 JSON.parse(JSON.stringify())가 처리할 수 없는 순환 참조와 복잡한 타입을 처리하며 객체의 깊은 복제를 만듭니다.
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
array: [1, [2, 3]],
};
// JSON 방식은 이러한 타입에 실패
const badClone = JSON.parse(JSON.stringify(original));
// date가 문자열이 되고, Map/Set이 {}가 됨
// structuredClone이 올바르게 처리
const goodClone = structuredClone(original);
goodClone.array[1].push(4);
console.log(original.array[1]); // [2, 3] — 영향 없음
React 관련 질문
16. useEffect의 의존성 배열 차이를 설명하세요.
// 매 렌더링 후 실행
useEffect(() => {
document.title = `${count}번 클릭`;
});
// 최초 렌더링 후 한 번만 실행
useEffect(() => {
initialData 가져오기();
return () => cleanup();
}, []);
// count나 userId가 변경될 때 실행
useEffect(() => {
fetchUserData(userId);
}, [userId, count]);
17. useMemo와 useCallback의 차이점은?
// useMemo: 계산된 값을 메모이제이션
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // items가 변경될 때만 재정렬
// useCallback: 함수 참조를 메모이제이션
const handleClick = useCallback((id) => {
setSelected(id);
onSelect(id);
}, [onSelect]); // onSelect가 변경되지 않으면 안정적인 참조
TypeScript 질문
18. interface와 type의 차이점은?
// interface: 객체 형태에 적합, 확장 가능, 병합 가능
interface User {
name: string;
age: number;
}
interface User { // 선언 병합
email: string;
}
interface Admin extends User {
role: string;
}
// type: 더 유연, 모든 타입 표현 가능, 병합 불가
type StringOrNumber = string | number;
type Point = [number, number];
19. 제네릭을 실용적인 예시로 설명하세요.
// 제네릭 없이 - 타입 안전하지 않음
function getFirst(arr: any[]): any {
return arr[0];
}
// 제네릭 사용 - 타입 정보 유지
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const firstNum = getFirst([1, 2, 3]); // 타입: number
const firstStr = getFirst(['a', 'b']); // 타입: string
// 제약이 있는 제네릭
function sortBy<T, K extends keyof T>(arr: T[], key: K): T[] {
return [...arr].sort((a, b) => {
if (a[key] < b[key]) return -1;
if (a[key] > b[key]) return 1;
return 0;
});
}
성능 최적화
20. 자바스크립트 성능을 향상시키는 기법은?
메모리 관리:
- 클로저와 이벤트 리스너의 메모리 누수 방지
- 객체 키 캐시에
WeakMap과WeakSet사용 - 컴포넌트 언마운트 시 인터벌과 타임아웃 제거
실행 효율성:
- 비용이 많이 드는 이벤트 핸들러를 디바운스 및 스로틀
- CPU 집약적 작업에 Web Workers 사용
- 애니메이션에
requestAnimationFrame선호 - DOM 작업 일괄 처리
// 디바운스: 사용자가 타이핑을 멈출 때까지 대기
const handleSearch = debounce((query) => {
fetchResults(query);
}, 300);
// 스로틀: 100ms마다 한 번으로 제한
const handleScroll = throttle((event) => {
updateScrollPosition(event);
}, 100);
시스템 설계
21. 속도 제한기를 자바스크립트로 어떻게 설계하겠습니까?
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map(); // userId -> [타임스탬프]
}
isAllowed(userId) {
const now = Date.now();
const windowStart = now - this.windowMs;
const userRequests = this.requests.get(userId) || [];
const validRequests = userRequests.filter(time => time > windowStart);
if (validRequests.length >= this.maxRequests) {
this.requests.set(userId, validRequests);
return false;
}
validRequests.push(now);
this.requests.set(userId, validRequests);
return true;
}
}
const limiter = new RateLimiter(10, 60000); // 분당 10개 요청
22. LRU 캐시를 구현하는 방법은?
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // Map은 삽입 순서 유지
}
get(key) {
if (!this.cache.has(key)) return -1;
// 끝으로 이동 (가장 최근 사용)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 가장 오래 전에 사용된 것 삭제 (Map의 첫 번째 항목)
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}
면접 팁
문제 해결 과정
- 코드 작성 전 요구사항 명확화 — 엣지 케이스와 제약 조건 확인
- 구현 전 접근 방식 설명
- 단순하게 시작하고 최적화
- 예시로 테스트 — 코드를 수동으로 단계별 확인
- 트레이드오프 논의 — 완벽한 해결책은 거의 없음
피해야 할 일반적인 실수
- 엣지 케이스 무시 (빈 배열, null 입력, 정수 오버플로우)
- 잘못된 자료 구조 선택 (O(1)인 Map 대신 O(n)인 배열)
- 비동기 오류 처리 안 함
- 입력 변경 (변경 가능 여부를 항상 명확히)