false 0 "" null undefined NaN — 나머지는 모두 truthy (빈 배열 [], 빈 객체 {} 포함!)
| 값 | Boolean 변환 | 설명 |
|---|---|---|
false, 0, "", null, undefined, NaN | false | 6가지 falsy |
[], {}, "0", -1, Infinity | true | 나머지 모두 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만 대체)
=== null로 직접 비교하세요.arr.length === 0을 사용하세요.const obj = { a: 1 }; obj.b = 2; 실행 결과는?const는 변수 바인딩(참조)을 고정하지, 객체 내부를 동결하지 않습니다. 프로퍼티 추가/수정은 가능합니다. 완전한 불변 객체가 필요하면 Object.freeze()를 사용하세요.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 ? "성인" : "미성년자"; // "성인"
0 ?? "기본값"의 결과는???(널 병합)는 null이나 undefined일 때만 오른쪽 값을 반환합니다. 0은 falsy지만 null/undefined가 아니므로 0이 그대로 반환됩니다.null?.name의 결과는??.은 왼쪽 값이 null 또는 undefined이면 에러 없이 undefined를 반환합니다.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
}
for...in으로 배열을 순회하면 안 되는 이유는?for...in은 배열 인덱스를 숫자가 아닌 문자열로 반환하며, 상속된 enumerable 속성도 순회할 수 있습니다. 배열은 for...of나 forEach를 사용하세요.this·arguments가 없고, new로 생성자 호출 불가합니다.// 함수 선언식 (호이스팅 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) - 서울"
arguments 객체를 참조하면?arguments 객체를 가지지 않습니다. 외부 함수의 arguments를 참조하거나, 최상위에서는 ReferenceError가 납니다. 가변 인수 처리는 ...rest를 사용하세요.// 실행 컨텍스트가 만들어지는 순간
// 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); // 꼬리 호출
}
// 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은 블록 스코프)
}
let으로 선언한 변수를 선언문 이전에 접근하면?let과 const는 호이스팅되지만 초기화되지 않은 상태(TDZ)에 머뭅니다. 선언문에 도달하기 전까지 접근하면 ReferenceError가 발생합니다. 이것이 var보다 안전한 이유입니다.// 클로저로 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); // 빠름 (캐시 활용)
#private 필드가 등장하기 전까지 JS에서 private 상태를 구현하는 주요 방법이었습니다.this는 함수가 호출되는 방식에 따라 결정됩니다 (정의 위치 X).this가 없어 정의된 위치의 외부 this를 사용합니다 (렉시컬 this).
| 규칙 | 호출 방식 | 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 참조 불가 ❌
}
};
const fn = obj.method; fn();에서 fn 내부의 this는?fn()은 단독 호출이므로 기본 바인딩이 적용됩니다. 이런 경우 bind(obj)로 this를 고정하거나 화살표 함수를 사용하세요.null에 도달하면 undefined를 반환합니다.
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에서 탐색)
d1.toString()을 호출하면 JS 엔진이 탐색하는 순서는?toString은 d1에도, Dog.prototype에도 없으므로 Object.prototype에서 찾게 됩니다.sort, splice 제외).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 내림차순
[1,2,3].map(n => n * 2) 실행 후 원본 배열은?map은 원본을 변경하지 않고 새 배열을 반환합니다. 원본을 직접 변경하는 배열 메서드는 sort(), splice(), push(), pop(), reverse() 등입니다.| 항목 | Map | Object |
|---|---|---|
| 키 타입 | 모든 타입 (함수, 객체 등) | string / symbol만 |
| 삽입 순서 | 보장 ✅ | 불완전 |
| 크기 확인 | map.size | Object.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}
new Set([1, "1", true, 1])의 size는?1과 "1"은 다른 타입이므로 다른 값이고, true도 별개입니다. 중복은 숫자 1뿐이므로 size는 3 ({1, "1", true})입니다....는 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); // 모던 방법 ✅
const copy = { ...original };에서 original 내부의 중첩 객체를 수정하면?structuredClone()이나 JSON.parse(JSON.stringify())를 사용하세요.#field로 private 필드 선언, static으로 클래스 레벨 메서드, extends로 상속합니다.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 (상속 체인)
super() 없이 this를 사용하면?extends로 상속받은 자식 클래스의 constructor에서 this를 사용하려면 먼저 super()를 호출해야 합니다. 이를 어기면 ReferenceError: Must call super constructor in derived class before accessing 'this'가 발생합니다.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();
}
Promise.resolve().then(A)와 setTimeout(B, 0) 중 먼저 실행되는 것은?// 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)]);
Promise.all([p1, p2, p3])에서 p2가 reject되면?Promise.all은 하나라도 reject되면 즉시 전체를 reject합니다. 성공/실패 여부에 상관없이 모든 결과가 필요하다면 Promise.allSettled를 사용하세요.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)));
async function fn() { return 42; }의 반환값 타입은?async 함수는 항상 Promise를 반환합니다. return 42는 내부적으로 Promise.resolve(42)와 동일합니다. 따라서 반환값을 사용하려면 .then()이나 다른 async 함수 내에서 await해야 합니다.// 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; // 기본값 반환
}
}
finally 블록이 실행되는 조건은?finally는 try/catch 실행 결과와 무관하게 항상 실행됩니다. DB 연결 해제, 로딩 스피너 숨기기 등 반드시 정리해야 할 작업에 활용합니다.// ❌ 메모리 누수 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); }
WeakMap이 Map보다 메모리 측면에서 유리한 이유는?WeakMap의 키는 약한 참조(weak reference)를 유지합니다. 키 객체에 다른 참조가 없으면 GC가 해당 객체와 WeakMap 엔트리를 모두 해제합니다. DOM 노드나 외부 객체에 대한 메타데이터를 저장할 때 메모리 누수를 방지하는 데 이상적입니다.// ===== 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
import Foo from "..."). named export: 여러 개 가능, 반드시 같은 이름으로 가져와야 함 (import { add } from "..."). 별칭은 as로 가능합니다.// 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 }"