JavaScript

자바스크립트 완전 정복

실행 컨텍스트부터 메모리 최적화까지 — 핵심 개념 완전 정리
5
20
토픽
40+
퀴즈
BASIC

기초 문법

📦

변수 선언 & 자료형 & Truthy/Falsy

var/let/const · 7가지 원시 타입 · 참/거짓 판별
var: 함수 스코프, 재선언 가능, 호이스팅 → 사용 지양
let: 블록 스코프, 재할당 가능 / const: 블록 스코프, 재할당 불가 (참조값 내부는 변경 가능)
Falsy 값: false 0 "" null undefined NaN — 나머지는 모두 truthy (빈 배열 [], 빈 객체 {} 포함!)
Truthy / Falsy 판별표
Boolean 변환설명
false, 0, "", null, undefined, NaNfalse6가지 falsy
[], {}, "0", -1, Infinitytrue나머지 모두 truthy
예시 코드
// var vs let vs const
var x = 1;    var x = 2;  // ✅ 재선언 가능 (주의!)
let y = 1;    // y = 2;   // ✅ 재할당 가능
const z = 1;  // z = 2;   // ❌ TypeError

// const 참조 타입은 내부 변경 가능
const arr = [1, 2];
arr.push(3);    // ✅ [1, 2, 3]
// arr = [];    // ❌ 재할당 불가

// 7가지 원시 타입
typeof "hello"    // "string"
typeof 42         // "number"
typeof true       // "boolean"
typeof undefined  // "undefined"
typeof null       // "object"  ⚠️ JS 역사적 버그
typeof Symbol()   // "symbol"
typeof 42n        // "bigint"

// 참조 타입
typeof []         // "object"
typeof {}         // "object"
typeof function(){} // "function"

// Truthy/Falsy 활용
const name = "";
const display = name || "손님";  // "손님" (빈 문자열은 falsy)

const count = 0;
const val = count ?? "없음";  // 0 (?? 는 null/undefined만 대체)
⚠️ typeof null === "object" 는 JS 설계 버그입니다. null 체크는 반드시 === null로 직접 비교하세요.
문제
Q1. 다음 중 falsy 값이 아닌 것은?
[] (빈 배열)은 truthy입니다. 배열과 객체는 비어 있어도 truthy입니다. 배열이 비었는지 확인하려면 arr.length === 0을 사용하세요.
Q2. const obj = { a: 1 }; obj.b = 2; 실행 결과는?
const는 변수 바인딩(참조)을 고정하지, 객체 내부를 동결하지 않습니다. 프로퍼티 추가/수정은 가능합니다. 완전한 불변 객체가 필요하면 Object.freeze()를 사용하세요.

연산자 (Operators)

== vs === · 논리 연산자 · 옵셔널 체이닝 · 널 병합
==(동등 연산자)는 타입을 변환한 뒤 비교하므로 예상치 못한 결과를 낳습니다. 항상 ===(일치 연산자)를 사용하세요.
??(널 병합)는 null/undefined일 때만 대체값을 쓰고, ||는 falsy(0, "" 포함)일 때도 대체합니다.
예시 코드
// == vs === (항상 === 사용 권장)
0 == false    // true  ⚠️ 타입 변환
0 === false   // false ✅
"" == false   // true  ⚠️
null == undefined  // true  (특수 케이스)
null === undefined // false

// 논리 연산자 단락 평가
const user = null;
user && user.name          // null  (user 거짓이면 멈춤)
user?.name                 // undefined (옵셔널 체이닝, 에러 없음)

// || vs ??
const count = 0;
count || "없음"   // "없음" ⚠️ 0도 falsy 취급
count ?? "없음"   // 0      ✅ null/undefined만 대체

// 옵셔널 체이닝 ?. (ES2020)
const data = { user: null };
data.user?.address?.city   // undefined (TypeError 없음)
data.user?.getName?.()     // undefined (메서드 호출도 가능)

// 논리 할당 연산자 (ES2021)
let a = null;
a ??= "기본값"   // a = "기본값" (null이므로 할당)
let b = "기존";
b ??= "기본값"   // b = "기존"  (null 아니므로 유지)

// 삼항 연산자
const age = 20;
const isAdult = age >= 18 ? "성인" : "미성년자";  // "성인"
문제
Q1. 0 ?? "기본값"의 결과는?
??(널 병합)는 null이나 undefined일 때만 오른쪽 값을 반환합니다. 0은 falsy지만 null/undefined가 아니므로 0이 그대로 반환됩니다.
Q2. null?.name의 결과는?
✅ 옵셔널 체이닝 ?.은 왼쪽 값이 null 또는 undefined이면 에러 없이 undefined를 반환합니다.
🔁

조건문 & 반복문

if/switch · for/while · for...of/in · break/continue
for...of는 배열·문자열 등 이터러블의 을 순회합니다.
for...in은 객체의 열거 가능한 키를 순회합니다 (배열에는 사용 지양).
forEach는 배열 전용이며 break를 쓸 수 없습니다.
예시 코드
// if / else if / else
const score = 75;
if (score >= 90)      console.log("A");
else if (score >= 80) console.log("B");
else if (score >= 70) console.log("C");  // ← 출력
else                  console.log("F");

// switch (break 빠지면 fall-through 주의!)
const day = "월";
switch (day) {
  case "토": case "일": console.log("주말"); break;
  default: console.log("평일");
}

// for (인덱스 필요할 때)
for (let i = 0; i < 3; i++) { /* 0,1,2 */ }

// for...of (배열/문자열 값 순회)
const fruits = ["사과", "바나나", "포도"];
for (const f of fruits) console.log(f);

// 인덱스 + 값 동시에
for (const [i, f] of fruits.entries()) console.log(i, f);

// for...in (객체 키 순회)
const user = { name: "Kim", age: 30 };
for (const key in user) console.log(key, user[key]);

// while / do...while
let n = 0;
while (n < 3) n++;         // 최소 0회
do { n--; } while (n > 0); // 최소 1회

// break / continue
for (let i = 0; i < 5; i++) {
  if (i === 2) continue;  // 2 건너뜀
  if (i === 4) break;     // 4에서 종료
  console.log(i);         // 0, 1, 3
}
문제
Q1. for...in으로 배열을 순회하면 안 되는 이유는?
for...in은 배열 인덱스를 숫자가 아닌 문자열로 반환하며, 상속된 enumerable 속성도 순회할 수 있습니다. 배열은 for...offorEach를 사용하세요.
⚙️

함수 (Functions)

선언식 vs 표현식 · 화살표 함수 · 기본값/rest 매개변수
함수 선언식은 호이스팅되어 선언 전 호출 가능합니다.
화살표 함수는 자체 this·arguments가 없고, new로 생성자 호출 불가합니다.
순수 함수(Pure Function)는 같은 입력 → 같은 출력, 부수 효과 없음 — 예측 가능하고 테스트하기 쉽습니다.
예시 코드
// 함수 선언식 (호이스팅 O — 선언 전 호출 가능)
greet("철수");  // ✅ "안녕, 철수!"
function greet(name) { return `안녕, ${name}!`; }

// 함수 표현식 (호이스팅 X)
// add(1,2);   // ❌ ReferenceError
const add = function(a, b) { return a + b; };

// 화살표 함수 (간결 문법)
const mul  = (a, b) => a * b;        // 한 줄: return 생략
const dbl  = n => n * 2;             // 매개변수 1개: () 생략
const getObj = () => ({ x: 1 });     // 객체 반환: () 필요

// 기본값 매개변수
function hi(name = "손님", msg = "안녕") {
  return `${msg}, ${name}!`;
}
hi();               // "안녕, 손님!"
hi("민준", "Hello") // "Hello, 민준!"

// 나머지 매개변수 (rest) — 항상 마지막에
function sum(first, ...rest) {
  return rest.reduce((acc, n) => acc + n, first);
}
sum(1, 2, 3, 4);  // 10

// 구조분해 매개변수
function display({ name, age = 0, city = "서울" }) {
  return `${name}(${age}) - ${city}`;
}
display({ name: "이수진", age: 25 }); // "이수진(25) - 서울"
문제
Q1. 화살표 함수에서 arguments 객체를 참조하면?
✅ 화살표 함수는 자체 arguments 객체를 가지지 않습니다. 외부 함수의 arguments를 참조하거나, 최상위에서는 ReferenceError가 납니다. 가변 인수 처리는 ...rest를 사용하세요.
CORE

핵심 개념

🧠

실행 컨텍스트 & 콜 스택

Variable Environment · Scope Chain · 콜 스택 동작 원리
실행 컨텍스트는 코드가 실행되는 환경으로, 변수·함수·this 정보를 담습니다.
함수 호출마다 새 컨텍스트가 콜 스택 위에 쌓이고, 실행 완료 시 제거됩니다.
콜 스택이 꽉 차면 Maximum call stack size exceeded 에러가 발생합니다 (무한 재귀).
콜 스택 시각화 — inner() 실행 중
inner() ← 현재 실행
outer()
전역 컨텍스트 (Global EC)
↑ 위에 쌓임 (LIFO)
① 전역 컨텍스트 생성
② outer() 호출 → 스택에 push
③ inner() 호출 → 스택에 push
④ inner() 완료 → 스택에서 pop
⑤ outer() 완료 → 스택에서 pop
예시 코드
// 실행 컨텍스트가 만들어지는 순간
// 1) Variable Environment (변수 선언)
// 2) Lexical Environment (스코프 체인)
// 3) ThisBinding (this 값 결정)

function outer() {
  const outerVal = "외부";

  function inner() {
    const innerVal = "내부";
    // 이 시점 콜 스택: [전역 EC, outer EC, inner EC]
    console.log(outerVal); // 스코프 체인으로 접근 ✅
  }

  inner(); // inner EC 생성 → 스택 push
  // inner() 종료 후 inner EC 제거 → 스택 pop
}

outer(); // outer EC 생성 → 스택 push

// 재귀로 인한 스택 오버플로우
// function infinite() { return infinite(); }
// infinite(); // ❌ Maximum call stack size exceeded

// 꼬리 재귀 최적화 (일부 환경 지원)
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc); // 꼬리 호출
}
문제
Q1. 콜 스택(Call Stack)의 자료구조 특성은?
✅ 콜 스택은 LIFO 구조입니다. 가장 마지막에 호출된 함수가 가장 먼저 완료됩니다. 마치 접시를 쌓는 것처럼, 꺼낼 때는 위에서부터 꺼냅니다.
🔭

스코프 & 호이스팅 & TDZ

렉시컬 스코프 · 스코프 체인 · TDZ · var의 문제점
JS는 렉시컬(정적) 스코프: 함수가 정의된 위치에 따라 스코프가 결정됩니다.
호이스팅: var 선언과 함수 선언식은 스코프 최상단으로 끌어올려집니다.
TDZ(Temporal Dead Zone): let/const는 선언 전 접근 시 ReferenceError 발생합니다.
스코프 체인 — 안쪽에서 바깥쪽으로 변수를 탐색
전역 스코프
let globalVar = "전역";
outer() 스코프
let outerVar = "외부";
inner() 스코프 ← 현재 위치
// globalVar, outerVar 모두 접근 가능 ✅
// 체인: inner → outer → 전역 → (없으면 ReferenceError)
예시 코드
// var 호이스팅 (선언만 끌어올림, 초기화 X)
console.log(a); // undefined (에러 없음! ⚠️)
var a = 5;
// 실제 동작: var a; → console.log(a) → a = 5

// let/const — TDZ (선언 전 접근 = 에러)
// console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 10;

// 함수 선언식: 선언+초기화 모두 호이스팅
greet(); // ✅ "Hello!"
function greet() { console.log("Hello!"); }

// 함수 표현식: 변수만 호이스팅
// sayHi(); // ❌ Cannot read properties of undefined
var sayHi = function() { console.log("Hi!"); };

// var의 블록 스코프 문제
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
  // 결과: 3, 3, 3 ❌ (var는 함수 스코프)
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100);
  // 결과: 0, 1, 2 ✅ (let은 블록 스코프)
}
문제
Q1. let으로 선언한 변수를 선언문 이전에 접근하면?
letconst는 호이스팅되지만 초기화되지 않은 상태(TDZ)에 머뭅니다. 선언문에 도달하기 전까지 접근하면 ReferenceError가 발생합니다. 이것이 var보다 안전한 이유입니다.
🔒

클로저 (Closure)

렉시컬 환경 보존 · 캡슐화 · 함수 팩토리 · 메모이제이션
클로저는 함수 + 함수가 선언된 렉시컬 환경의 조합입니다.
외부 함수가 종료된 뒤에도, 내부 함수는 외부 함수의 변수를 기억합니다.
이를 활용해 외부에서 접근 불가한 private 상태를 만들 수 있습니다.
클로저 — 외부 함수 종료 후에도 변수 참조 유지
makeCounter() 스코프 (실행 종료 후에도 유지됨)
let count = 0; ← 클로저가 참조 중
반환된 increment 함수
count++ → 외부 count 변수에 접근 ✅
외부에서 count에 직접 접근 불가 🔒
예시 코드
// 클로저로 private 상태 관리
function makeCounter(initial = 0) {
  let count = initial; // 외부 직접 접근 불가

  return {
    increment() { return ++count; },
    decrement() { return --count; },
    reset()     { count = initial; },
    get()       { return count; }
  };
}

const c1 = makeCounter(10);
const c2 = makeCounter();
c1.increment(); // 11
c1.increment(); // 12
c2.increment(); // 1  (c1과 독립적)

// 함수 팩토리 — factor를 클로저로 기억
function multiplier(factor) {
  return n => n * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
double(5); // 10
triple(5); // 15

// 메모이제이션 (결과 캐싱)
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}
const expFib = memoize(n => n <= 1 ? n : expFib(n-1) + expFib(n-2));
expFib(40); // 빠름 (캐시 활용)
문제
Q1. 클로저를 이용한 모듈 패턴의 가장 큰 이점은?
✅ 클로저는 내부 변수를 외부에서 직접 접근하지 못하게 캡슐화할 수 있습니다. ES2022에서 클래스의 #private 필드가 등장하기 전까지 JS에서 private 상태를 구현하는 주요 방법이었습니다.
👆

this 바인딩

4가지 바인딩 규칙 · 화살표 함수 · call/apply/bind
this함수가 호출되는 방식에 따라 결정됩니다 (정의 위치 X).
화살표 함수는 자체 this가 없어 정의된 위치의 외부 this를 사용합니다 (렉시컬 this).
this 바인딩 4가지 규칙
규칙호출 방식this 값
암묵적 바인딩obj.method()obj
명시적 바인딩fn.call(obj) / apply / bind지정한 obj
new 바인딩new Fn()새로 생성된 인스턴스
기본 바인딩fn() (단독 호출)전역 객체 / strict에서 undefined
예시 코드
// 1. 암묵적 바인딩 — 호출 객체가 this
const obj = {
  name: "Kim",
  greet() { return `Hi, ${this.name}`; }
};
obj.greet(); // "Hi, Kim" (this = obj)

// 2. 명시적 바인딩 — call/apply/bind
function introduce(age) { return `${this.name}, ${age}세`; }
introduce.call({ name: "Lee" }, 30);    // "Lee, 30세"
introduce.apply({ name: "Park" }, [25]); // "Park, 25세"
const bound = introduce.bind({ name: "Choi" });
bound(28); // "Choi, 28세" (나중에 호출)

// 3. new 바인딩
function Person(name) {
  this.name = name; // this = 새 인스턴스
}
const p = new Person("민수"); // p.name = "민수"

// 4. 기본 바인딩 (엄격 모드에서 undefined)
function alone() { console.log(this); }
alone(); // 전역(window) or undefined (strict mode)

// 화살표 함수 — 렉시컬 this (정의 위치 기준)
const timer = {
  count: 0,
  start() {
    // 화살표 함수는 start()의 this를 그대로 사용
    setInterval(() => { this.count++; }, 1000); // ✅
    // 일반 함수: this가 전역이 되어 count 참조 불가 ❌
  }
};
문제
Q1. const fn = obj.method; fn();에서 fn 내부의 this는?
✅ 메서드를 변수에 할당하면 객체와의 연결이 끊어집니다. fn()은 단독 호출이므로 기본 바인딩이 적용됩니다. 이런 경우 bind(obj)로 this를 고정하거나 화살표 함수를 사용하세요.
🧬

프로토타입 체인 (Prototype Chain)

[[Prototype]] · 상속 체인 · hasOwnProperty · Object.create
모든 JS 객체는 [[Prototype]] 내부 슬롯을 통해 다른 객체를 참조합니다.
프로퍼티/메서드를 찾을 때 현재 객체 → prototype → prototype의 prototype... 순으로 올라가다가 null에 도달하면 undefined를 반환합니다.
프로토타입 체인 — dog 인스턴스의 메서드 탐색 경로
dog
{ name: "바둑이" }

[[Prototype]]
Dog.prototype
speak: fn

[[Prototype]]
Object.prototype
toString, hasOwnProperty...
null
예시 코드
function Dog(name) {
  this.name = name; // 인스턴스 프로퍼티
}
// 프로토타입에 메서드 추가 (모든 인스턴스가 공유)
Dog.prototype.speak = function() {
  return `${this.name}: 멍멍!`;
};

const d1 = new Dog("바둑이");
const d2 = new Dog("해피");
d1.speak(); // "바둑이: 멍멍!"  (Dog.prototype에서 탐색)
d2.speak(); // "해피: 멍멍!"

// 프로토타입 체인 확인
d1.__proto__ === Dog.prototype;         // true
Dog.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;    // true (체인 끝)

// hasOwnProperty: 자신 소유 프로퍼티만 확인
d1.hasOwnProperty("name");   // true (직접 소유)
d1.hasOwnProperty("speak");  // false (prototype에 있음)
"speak" in d1;               // true (체인 포함 탐색)

// Object.create: 특정 객체를 prototype으로 설정
const animal = { breathe() { return "호흡 중"; } };
const cat = Object.create(animal);
cat.name = "나비";
cat.breathe(); // "호흡 중" (animal에서 탐색)
문제
Q1. d1.toString()을 호출하면 JS 엔진이 탐색하는 순서는?
✅ 프로퍼티 탐색은 항상 현재 객체 → prototype 체인 위로 진행됩니다. toString은 d1에도, Dog.prototype에도 없으므로 Object.prototype에서 찾게 됩니다.
DATA

자료구조 & ES6+

📋

배열 고차 메서드 (Array Methods)

map · filter · reduce · find · sort · flat · flatMap
고차 메서드는 원본 배열을 변경하지 않고 새 값을 반환합니다 (sort, splice 제외).
map → 변환 / filter → 필터 / reduce → 누산 / find → 첫 일치 요소
예시 코드
const nums = [1, 2, 3, 4, 5];
const users = [
  { name: "Alice", age: 25, score: 88 },
  { name: "Bob",   age: 17, score: 72 },
  { name: "Carol", age: 30, score: 95 },
];

// map: 변환 → 새 배열 반환
const doubled = nums.map(n => n * 2);          // [2,4,6,8,10]
const names   = users.map(u => u.name);        // ["Alice","Bob","Carol"]

// filter: 조건 → 새 배열 반환
const evens  = nums.filter(n => n % 2 === 0);  // [2,4]
const adults = users.filter(u => u.age >= 18); // [Alice, Carol]

// reduce: 누산 → 단일 값
const sum  = nums.reduce((acc, n) => acc + n, 0); // 15
const byName = users.reduce((acc, u) => {
  acc[u.name] = u.score; return acc;
}, {}); // { Alice:88, Bob:72, Carol:95 }

// find / findIndex: 첫 번째 일치
nums.find(n => n > 3);      // 4 (요소)
nums.findIndex(n => n > 3); // 3 (인덱스)

// some / every
nums.some(n => n > 4);   // true (하나라도)
nums.every(n => n > 0);  // true (모두)

// flat / flatMap
[[1,2],[3,4]].flat();          // [1,2,3,4]
nums.flatMap(n => [n, n*2]);   // [1,2,2,4,3,6,4,8,5,10]

// sort (원본 변경! 주의)
[3,1,4,1,5].sort((a, b) => a - b);           // [1,1,3,4,5]
users.sort((a, b) => b.score - a.score);     // score 내림차순
문제
Q1. [1,2,3].map(n => n * 2) 실행 후 원본 배열은?
map은 원본을 변경하지 않고 새 배열을 반환합니다. 원본을 직접 변경하는 배열 메서드는 sort(), splice(), push(), pop(), reverse() 등입니다.
🗃️

Map & Set

Map vs Object · Set으로 중복 제거 · WeakMap/WeakSet
Map: 키-값 쌍, 모든 타입을 키로 사용 가능, 삽입 순서 보장, 순회 용이.
Set: 중복 없는 값의 집합, 빠른 O(1) has 연산.
WeakMap/WeakSet: 키가 객체만 가능, GC 대상 허용 (메모리 누수 방지에 유용).
Map vs Object 비교
항목MapObject
키 타입모든 타입 (함수, 객체 등)string / symbol만
삽입 순서보장 ✅불완전
크기 확인map.sizeObject.keys(o).length
순회for...of 직접 가능Object.entries() 필요
성능 (잦은 추가/삭제)우수 ✅상대적으로 불리
예시 코드
// Map
const map = new Map();
map.set("name", "Kim");
map.set(42, "숫자 키");
map.set({ id: 1 }, "객체 키");  // 객체도 키 가능!

map.get("name");    // "Kim"
map.has("name");    // true
map.size;           // 3
map.delete("name");

for (const [key, val] of map) console.log(key, val);

// 객체를 Map으로 변환
const obj = { a: 1, b: 2 };
const m2 = new Map(Object.entries(obj));
// Map 다시 객체로
const obj2 = Object.fromEntries(m2);

// Set — 중복 자동 제거
const set = new Set([1, 2, 2, 3, 3, 3]);
set.size;         // 3 (중복 제거됨)
set.has(2);       // true
set.add(4).add(5);
set.delete(1);

// 배열 중복 제거 (실전 패턴)
const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]

// 두 배열의 교집합
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const intersection = [...a].filter(x => b.has(x)); // [2, 3]
const union = new Set([...a, ...b]); // {1,2,3,4}
문제
Q1. new Set([1, "1", true, 1])의 size는?
✅ Set은 엄격한 동등성(===)으로 중복을 판별합니다. 1"1"은 다른 타입이므로 다른 값이고, true도 별개입니다. 중복은 숫자 1뿐이므로 size는 3 ({1, "1", true})입니다.

객체 & ES6+ 현대 문법

구조분해 · 스프레드 · 단축 프로퍼티 · Object 메서드
ES6+의 현대 문법은 코드를 간결하게 만들지만, 얕은 복사(shallow copy) 주의가 필요합니다.
스프레드 연산자 ...는 1단계 깊이만 복사합니다. 중첩 객체는 참조가 공유됩니다.
예시 코드
// 구조분해 — 배열
const [a, b, ...rest] = [1, 2, 3, 4, 5];
// a=1, b=2, rest=[3,4,5]
const [x, , z] = [10, 20, 30]; // 중간 건너뜀

// 구조분해 — 객체 (별칭, 기본값)
const { name: n, age = 20, city = "서울" } = { name: "Kim" };
// n="Kim", age=20, city="서울"

// 함수 매개변수 구조분해
const show = ({ name, score = 0 }) => `${name}: ${score}`;
show({ name: "Lee", score: 95 }); // "Lee: 95"

// 단축 프로퍼티 & 메서드
const x2 = 10, y = 20;
const point = { x2, y, move() { return `(${this.x2},${this.y})`; } };

// 스프레드 연산자
const arr1 = [1, 2, 3];
const arr2 = [0, ...arr1, 4]; // [0,1,2,3,4]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3, b: 99 }; // { a:1, b:99, c:3 } (덮어씀)

// Object 정적 메서드
const person = { name: "Park", age: 28 };
Object.keys(person);    // ["name", "age"]
Object.values(person);  // ["Park", 28]
Object.entries(person); // [["name","Park"],["age",28]]

// 깊은 복사
const deep1 = JSON.parse(JSON.stringify(person)); // 간단, Date/함수 손실
const deep2 = structuredClone(person);            // 모던 방법 ✅
문제
Q1. const copy = { ...original };에서 original 내부의 중첩 객체를 수정하면?
✅ 스프레드 연산자는 1단계 깊이만 복사합니다 (얕은 복사). 중첩 객체/배열은 여전히 같은 참조를 가리킵니다. 깊은 복사가 필요하면 structuredClone()이나 JSON.parse(JSON.stringify())를 사용하세요.
🏗️

클래스 (Class)

상속 · private 필드 · static · getter/setter
ES6 클래스는 프로토타입 기반 상속의 문법적 설탕입니다.
#field로 private 필드 선언, static으로 클래스 레벨 메서드, extends로 상속합니다.
자식 클래스의 constructor에서 super()this 사용 전에 반드시 호출해야 합니다.
예시 코드
class Animal {
  #name;       // private 필드 (외부 직접 접근 불가)
  #sound;

  constructor(name, sound) {
    this.#name  = name;
    this.#sound = sound;
  }

  // getter / setter
  get name() { return this.#name; }
  set name(v) {
    if (!v) throw new Error("이름 필수");
    this.#name = v;
  }

  speak() { return `${this.#name}: ${this.#sound}!`; }

  // 정적 메서드 — 인스턴스 없이 호출
  static compare(a, b) { return a.name.localeCompare(b.name); }
}

// 상속 (extends)
class Dog extends Animal {
  #breed;

  constructor(name, breed) {
    super(name, "멍멍"); // 반드시 this 사용 전에 호출
    this.#breed = breed;
  }

  // 메서드 오버라이딩
  speak() { return super.speak() + " 🐶"; }
  info()  { return `${this.name} (${this.#breed})`; }
}

const dog = new Dog("바둑이", "진돗개");
dog.speak();           // "바둑이: 멍멍! 🐶"
dog.info();            // "바둑이 (진돗개)"
dog instanceof Dog;    // true
dog instanceof Animal; // true (상속 체인)
문제
Q1. 자식 클래스 constructor에서 super() 없이 this를 사용하면?
extends로 상속받은 자식 클래스의 constructor에서 this를 사용하려면 먼저 super()를 호출해야 합니다. 이를 어기면 ReferenceError: Must call super constructor in derived class before accessing 'this'가 발생합니다.
ASYNC

비동기 처리

🔄

이벤트 루프 (Event Loop)

콜 스택 · Web APIs · 마이크로태스크 큐 · 매크로태스크 큐
JS는 단일 스레드이지만 이벤트 루프 덕분에 비동기 처리가 가능합니다.
마이크로태스크 큐(Promise.then, queueMicrotask)는 매크로태스크 큐(setTimeout, setInterval)보다 먼저 처리됩니다.
이벤트 루프 구조
콜 스택
console.log("A")
main()
Web APIs
setTimeout(cb, 0)
fetch(url)
태스크 큐
마이크로: then cb
매크로: setTimeout cb
이벤트 루프: 콜 스택이 비면 → 마이크로태스크 큐 전부 처리 → 매크로태스크 큐에서 하나 처리 → 반복
예시 코드 — 실행 순서 예측
console.log("1");  // 동기

setTimeout(() => console.log("2"), 0);  // 매크로태스크

Promise.resolve().then(() => console.log("3")); // 마이크로태스크

console.log("4");  // 동기

// 출력 순서: 1 → 4 → 3 → 2
// 1, 4: 동기 코드 (콜 스택에서 즉시 실행)
// 3: 마이크로태스크 큐 (Promise.then) — 콜 스택 비면 즉시
// 2: 매크로태스크 큐 (setTimeout) — 마이크로태스크 모두 처리 후

// 실전 예시: UI 업데이트와 비동기
async function fetchAndRender() {
  // 1. fetch는 Web API가 처리 (콜 스택 안 막음)
  const res = await fetch("/api/data");
  // 2. await 이후 코드는 마이크로태스크로 예약
  const data = await res.json();
  renderUI(data); // 3. 데이터 도착 후 렌더
}

// 무거운 작업을 setTimeout으로 분할 (UI 프리징 방지)
function heavyTask(items, callback) {
  let i = 0;
  function chunk() {
    const end = Math.min(i + 100, items.length);
    for (; i < end; i++) process(items[i]);
    if (i < items.length) setTimeout(chunk, 0); // 다음 이벤트 루프로
    else callback();
  }
  chunk();
}
문제
Q1. Promise.resolve().then(A)setTimeout(B, 0) 중 먼저 실행되는 것은?
✅ 이벤트 루프는 콜 스택이 비면 마이크로태스크 큐를 먼저 전부 소진한 뒤 매크로태스크를 처리합니다. Promise.then은 마이크로태스크, setTimeout은 매크로태스크이므로 A가 먼저 실행됩니다.
🤝

Promise

3가지 상태 · .then/.catch · Promise.all/allSettled/race
Promise는 비동기 작업의 최종 완료/실패를 나타내는 객체로, 3가지 상태를 가집니다.
한 번 settled(fulfilled/rejected)되면 상태를 바꿀 수 없습니다 (불변).
Promise 상태 흐름
Pending ⏳
초기 상태
→ resolve() →
Fulfilled ✅
.then() 실행
Pending
→ reject() →
Rejected ❌
.catch() 실행
예시 코드
// Promise 생성
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) reject(new Error("유효하지 않은 ID"));
    else setTimeout(() => resolve({ id, name: "User" + id }), 500);
  });
}

// .then / .catch / .finally
fetchUser(1)
  .then(user => { console.log(user); return fetchUser(2); }) // 체이닝
  .then(user2 => console.log(user2))
  .catch(err => console.error("에러:", err.message))
  .finally(() => console.log("항상 실행"));

// Promise.all — 모두 성공해야 / 하나라도 실패하면 전체 실패
const [u1, u2] = await Promise.all([fetchUser(1), fetchUser(2)]);

// Promise.allSettled — 성공/실패 상관없이 모두 기다림
const results = await Promise.allSettled([fetchUser(1), fetchUser(-1)]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.error(r.reason.message);
});

// Promise.race — 가장 빠른 것 반환
const fastest = await Promise.race([fetchUser(1), fetchUser(2)]);

// Promise.any — 첫 성공 (모두 실패하면 AggregateError)
const first = await Promise.any([fetchUser(-1), fetchUser(2)]);
문제
Q1. Promise.all([p1, p2, p3])에서 p2가 reject되면?
Promise.all은 하나라도 reject되면 즉시 전체를 reject합니다. 성공/실패 여부에 상관없이 모든 결과가 필요하다면 Promise.allSettled를 사용하세요.

async / await

동기식 코드처럼 작성 · 순차 vs 병렬 실행 · Fetch API
async 함수는 항상 Promise를 반환합니다.
await는 Promise가 settled될 때까지 해당 async 함수의 실행을 일시 정지합니다 (다른 코드는 계속 실행).
순차 await는 느리므로, 독립적인 작업은 Promise.all병렬 실행하세요.
예시 코드
// async 함수는 항상 Promise 반환
async function getNum() { return 42; }
getNum().then(console.log); // 42

// 순차 실행 (느림 — 각 요청이 완료 후 다음 시작)
async function sequential() {
  const u1 = await fetchUser(1); // 500ms 대기
  const u2 = await fetchUser(2); // 500ms 대기
  return [u1, u2];               // 총 1000ms
}

// 병렬 실행 (빠름 — 동시에 시작)
async function parallel() {
  const [u1, u2] = await Promise.all([
    fetchUser(1),
    fetchUser(2),
  ]);
  return [u1, u2]; // 총 ~500ms
}

// Fetch API 실전 패턴
async function getPost(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
  return await res.json();
}

// top-level await (ES2022, 모듈에서 사용 가능)
// const data = await getPost(1);

// 여러 async 작업을 순서대로 처리
const ids = [1, 2, 3];
const posts = await Promise.all(ids.map(id => getPost(id)));
문제
Q1. async function fn() { return 42; }의 반환값 타입은?
async 함수는 항상 Promise를 반환합니다. return 42는 내부적으로 Promise.resolve(42)와 동일합니다. 따라서 반환값을 사용하려면 .then()이나 다른 async 함수 내에서 await해야 합니다.
🚨

에러 처리 (Error Handling)

try/catch/finally · 커스텀 에러 · async 에러 처리
try/catch로 예상 가능한 에러를 처리하고, finally는 성공/실패 무관하게 항상 실행됩니다.
async/await에서 rejected Promise는 try/catch로 잡을 수 있습니다.
예시 코드
// try / catch / finally
function divide(a, b) {
  if (b === 0) throw new Error("0으로 나눌 수 없음");
  return a / b;
}

try {
  console.log(divide(10, 0));
} catch (err) {
  console.log(err.name);    // "Error"
  console.log(err.message); // "0으로 나눌 수 없음"
} finally {
  console.log("항상 실행"); // 성공/실패 무관
}

// 커스텀 에러 클래스
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateAge(age) {
  if (typeof age !== "number") throw new ValidationError("age", "숫자여야 합니다");
  if (age < 0 || age > 150) throw new ValidationError("age", "유효하지 않은 나이");
}

try {
  validateAge("스물");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`${e.field}: ${e.message}`);
  } else {
    throw e; // 예상치 못한 에러는 다시 던짐
  }
}

// async/await 에러 처리
async function loadData(id) {
  try {
    const res = await fetch(`/api/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("로딩 실패:", err.message);
    return null; // 기본값 반환
  }
}
문제
Q1. finally 블록이 실행되는 조건은?
finally는 try/catch 실행 결과와 무관하게 항상 실행됩니다. DB 연결 해제, 로딩 스피너 숨기기 등 반드시 정리해야 할 작업에 활용합니다.
ADVANCED

심화

🧮

메모리 & 성능

스택 vs 힙 · 가비지 컬렉션 · 메모리 누수 패턴 · WeakMap
원시 타입은 스택에, 참조 타입(객체/배열/함수)은 힙에 저장됩니다.
JS는 가비지 컬렉션(GC)으로 더 이상 참조되지 않는 객체를 자동 해제합니다.
하지만 클로저, 이벤트 리스너, 전역 변수 등으로 메모리 누수가 발생할 수 있습니다.
메모리 구조 — 스택 vs 힙
스택 (Stack) — 원시 타입
num = 42
str = "hello"
bool = true
ref → 0x001 (힙 주소)
힙 (Heap) — 참조 타입
0x001: { name: "Kim" }
0x002: [1, 2, 3]
0x003: function() {}
예시 코드 — 메모리 누수 패턴과 해결책
// ❌ 메모리 누수 1: 클로저가 DOM 참조 유지
function attachHandler() {
  const btn = document.querySelector("#btn");
  const heavyData = new Array(1000000).fill("data"); // 큰 데이터

  btn.addEventListener("click", () => {
    console.log(heavyData.length); // heavyData가 클로저에 잡힘
  });
}
// ✅ 해결: 이벤트 리스너 제거
const handler = () => console.log("클릭");
btn.addEventListener("click", handler);
btn.removeEventListener("click", handler); // 컴포넌트 언마운트 시

// ❌ 메모리 누수 2: 전역 변수에 대용량 데이터 저장
window.cache = {}; // 절대 GC 불가
// ✅ 해결: Map/WeakMap 사용, 스코프 제한

// WeakMap — 키가 객체만 가능, GC 허용
const cache = new WeakMap();
function process(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const result = heavyCompute(obj);
  cache.set(obj, result); // obj GC되면 캐시도 자동 해제
  return result;
}

// 성능 최적화 패턴
// 1. 문서 조각(DocumentFragment)으로 DOM 일괄 추가
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `항목 ${i}`;
  frag.appendChild(li);
}
document.querySelector("ul").appendChild(frag); // 리플로우 1회만

// 2. 객체 풀링 (재사용)
const pool = [];
function getObj() { return pool.pop() || {}; }
function release(obj) { Object.keys(obj).forEach(k => delete obj[k]); pool.push(obj); }
문제
Q1. WeakMapMap보다 메모리 측면에서 유리한 이유는?
WeakMap의 키는 약한 참조(weak reference)를 유지합니다. 키 객체에 다른 참조가 없으면 GC가 해당 객체와 WeakMap 엔트리를 모두 해제합니다. DOM 노드나 외부 객체에 대한 메타데이터를 저장할 때 메모리 누수를 방지하는 데 이상적입니다.
📦

모듈 시스템 (ES Modules)

named export · default export · 동적 import · 트리 쉐이킹
ES 모듈은 파일 단위 코드 캡슐화로 네임스페이스 충돌을 방지하고 코드 재사용을 높입니다.
동적 import()는 코드 스플리팅에 활용되어 초기 로딩 속도를 개선합니다.
예시 코드
// ===== math.js (내보내기) =====
export const PI = 3.14159;                        // named export
export function add(a, b) { return a + b; }       // named export
export default class Calculator {                  // default export
  multiply(a, b) { return a * b; }
}

// ===== main.js (가져오기) =====
import Calculator, { PI, add } from "./math.js";  // default + named
import { add as myAdd } from "./math.js";          // 별칭
import * as MathUtils from "./math.js";            // 전체 네임스페이스

MathUtils.add(1, 2);  // 3
MathUtils.PI;         // 3.14159

// 동적 import — 필요할 때 로드 (코드 스플리팅)
async function loadChart() {
  const { Chart } = await import("./chart.js"); // 필요시 로드
  return new Chart();
}

// 조건부 로드
const module = await import(isDev ? "./dev-tools.js" : "./prod.js");

// re-export (배럴 파일 패턴)
// index.js
export { add, PI } from "./math.js";
export { formatDate } from "./date.js";
export { fetchUser } from "./api.js";
// 사용처: import { add, fetchUser } from "./utils/index.js"

// import.meta (모듈 메타데이터)
console.log(import.meta.url);  // 현재 모듈 URL
문제
Q1. default export와 named export의 차이점은?
default export: 파일당 하나, import 시 원하는 이름으로 가져올 수 있음 (import Foo from "..."). named export: 여러 개 가능, 반드시 같은 이름으로 가져와야 함 (import { add } from "..."). 별칭은 as로 가능합니다.
🛠️

유용한 패턴

Debounce · Throttle · Observer · Currying · Pipe
Debounce: 마지막 이벤트 후 일정 시간 뒤 실행 (검색창 자동완성).
Throttle: 일정 시간 간격으로만 실행 (스크롤, 리사이즈 핸들러).
Currying: 인수를 하나씩 받는 함수 체인으로 변환하여 재사용성을 높입니다.
예시 코드
// Debounce — 마지막 호출 후 delay ms 뒤에 실행
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const onSearch = debounce(q => fetchResults(q), 300);
input.addEventListener("input", e => onSearch(e.target.value));

// Throttle — delay ms 간격으로 최대 1회 실행
function throttle(fn, delay) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      return fn.apply(this, args);
    }
  };
}
window.addEventListener("scroll", throttle(() => updateNav(), 100));

// Currying — 인수를 하나씩 받는 함수 변환
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);
double(5); // 10
triple(5); // 15

// Pipe — 함수를 순서대로 연결
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const process = pipe(
  x => x * 2,        // 10
  x => x + 1,        // 11
  x => `결과: ${x}`  // "결과: 11"
);
process(5); // "결과: 11"

// Observer 패턴 (이벤트 시스템)
class EventEmitter {
  #listeners = new Map();
  on(event, fn)  { (this.#listeners.get(event) || this.#listeners.set(event,[]).get(event)).push(fn); }
  off(event, fn) { const arr = this.#listeners.get(event); if(arr) this.#listeners.set(event, arr.filter(f=>f!==fn)); }
  emit(event, ...args) { (this.#listeners.get(event) || []).forEach(fn => fn(...args)); }
}
const bus = new EventEmitter();
bus.on("data", d => console.log("받음:", d));
bus.emit("data", { id: 1 }); // "받음: { id: 1 }"
문제
Q1. 검색창에서 사용자가 타이핑할 때마다 API 요청을 하지 않고, 입력이 멈추면 요청하는 패턴은?
Debounce는 연속 이벤트가 끝난 후 일정 시간이 지나야 실행합니다. 반면 Throttle은 일정 간격으로 계속 실행합니다. 검색 자동완성, 창 리사이즈 후 처리 등은 Debounce, 무한 스크롤, 실시간 위치 추적 등은 Throttle이 적합합니다.