시작하기
Hello World & 주석
JavaScript는 브라우저와 Node.js에서 실행됩니다. console.log()는 주요 디버그 출력입니다. JSDoc 주석(/** */)은 IDE에 타입 정보와 문서를 제공합니다. 더 안전한 파싱을 위해 'use strict'나 ES 모듈을 사용하세요. 주석은 런타임에 무시됩니다.
// Single-line comment
/* Multi-line
comment */
// Console output
console.log("Hello, World!");
console.warn("Warning message");
console.error("Error message");
console.table([{a: 1}, {a: 2}]);
// JSDoc comments (for documentation)
/**
* Adds two numbers
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} The sum
*/
function add(a, b) { return a + b; }엄격 모드 & 모듈
'use strict'는 더 엄격한 파싱을 활성화하여 조용한 에러를 잡습니다. ES 모듈(import/export)이 현대 표준입니다; CommonJS(require/module.exports)는 Node.js 전통입니다. 전역 스코프를 오염시키지 않으려면 항상 모듈을 사용하세요. 브라우저는 <script type='module'>을 지원합니다.
"use strict"; // enables strict mode (catches common mistakes)
x = 10; // ReferenceError: x is not defined (strict mode)
// ES Modules (modern)
// import { greet } from './utils.js';
// export function greet(name) { return `Hello, ${name}`; }
// CommonJS (Node.js traditional)
// const fs = require('fs');
// module.exports = { greet: (name) => `Hello, ${name}` };
// Running JS
// Browser: include in <script> or via dev server
// Node.js: node script.js
// Deno: deno run script.ts변수: let, const, var
항상 const를 선호하고; 재할당이 필요할 때만 let을 사용하고; var는 완전히 피하세요. const는 재할당을 방지하지만 변이는 방지하지 않습니다 — 객체/배열 내용은 여전히 변경할 수 있습니다. let/const는 블록 스코프이고; var는 함수 스코프이고 호이스팅됩니다 (버그 유발). TDZ는 선언 전 변수 사용을 방지합니다.
// const - cannot be reassigned (use by default)
const PI = 3.14159;
const user = { name: "Alice" };
user.name = "Bob"; // OK - object contents can change
// user = {}; // TypeError - can't reassign const
// let - block-scoped, can be reassigned
let count = 0;
count = 1;
if (true) {
let local = 5; // only accessible in this block
}
// var - function-scoped, hoisted (AVOID in modern code)
var old = "legacy"; // hoisted, can cause bugs
// Temporal Dead Zone (TDZ)
// console.log(x); // ReferenceError
// let x = 5;데이터 타입 & typeof
JavaScript에는 7개의 원시 타입(string, number, bigint, boolean, undefined, null, symbol)과 참조 타입(객체, 배열, 함수)이 있습니다. 원시값은 불변이고 값으로 복사됩니다; 객체는 가변이고 참조로 전달됩니다. typeof null은 역사적 버그로 'object'를 반환합니다 — null 검사에는 === null을 사용하세요.
// Primitive types (immutable, passed by value)
const str = "hello"; // string
const num = 42; // number (no separate int/float)
const big = 9007199254740993n; // bigint
const bool = true; // boolean
const undef = undefined; // undefined (no value)
const empty = null; // null (intentional empty)
const sym = Symbol("id"); // symbol (unique identifier)
// Reference types (mutable, passed by reference)
const arr = [1, 2, 3]; // object (array)
const obj = { a: 1 }; // object
const fn = function() {}; // object (function)
// typeof operator
console.log(typeof "hi"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (historical bug!)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"타입 변환 & 강제 변환
JavaScript의 타입 강제 변환은 악명 높게 혼란스럽습니다. 예상치 못한 강제 변환을 피하기 위해 항상 ==(느슨한) 대신 ===(엄격 동등)을 사용하세요. + 연산자는 피연산자 중 하나가 문자열이면 연결하고; 다른 연산자는 숫자로 강제 변환합니다. 거짓 값: false, 0, '', null, undefined, NaN. NaN 검사에는 Number.isNaN()을 사용하세요 (NaN !== NaN).
// Explicit conversion
String(42); // "42"
(42).toString(); // "42"
Number("42"); // 42
Number("3.14"); // 3.14
Number(""); // 0
Number("abc"); // NaN
parseInt("42px"); // 42
parseFloat("3.14abc"); // 3.14
Boolean(0); // false
Boolean(""); // false
Boolean("x"); // true
// Implicit coercion (often confusing)
console.log("5" + 3); // "53" (string concatenation)
console.log("5" - 3); // 2 (numeric subtraction)
console.log("5" * "2"); // 10
console.log(1 + "2" + 3); // "123"
console.log(true + 1); // 2
// Falsy values: false, 0, "", null, undefined, NaN
// Everything else is truthy
if ("0") console.log("truthy"); // runs! non-empty string
// Strict vs loose equality
console.log(1 == "1"); // true (loose, coerces)
console.log(1 === "1"); // false (strict, no coercion)
console.log(null == undefined); // true
console.log(null === undefined); // false문자열
문자열 메서드
문자열은 불변입니다 — 메서드는 새 문자열을 반환합니다. slice()는 음수 인덱스(끝에서부터)를 지원하고; substring()은 지원하지 않습니다. replace()는 첫 번째 매치만 치환합니다; 모두 치환하려면 replaceAll()(ES2021)을 사용하세요. at()(ES2022)은 음수 인덱스를 허용합니다. split() + join()은 문자열의 문자를 치환하는 관용적인 방법입니다.
const s = "Hello, World";
// Length & access
console.log(s.length); // 12
console.log(s[0]); // "H"
console.log(s.charAt(0)); // "H"
console.log(s.at(-1)); // "d" (ES2022, negative index)
// Case
console.log(s.toUpperCase()); // "HELLO, WORLD"
console.log(s.toLowerCase()); // "hello, world"
// Search
console.log(s.indexOf("World")); // 7 (-1 if not found)
console.log(s.includes("World")); // true
console.log(s.startsWith("Hello")); // true
console.log(s.endsWith("World")); // true
// Extract
console.log(s.slice(0, 5)); // "Hello"
console.log(s.slice(-5)); // "World"
console.log(s.substring(0, 5)); // "Hello" (no negative)
console.log(s.split(", ")); // ["Hello", "World"]
// Modify
console.log(s.replace("o", "0")); // "Hell0, World"
console.log(s.replaceAll("o", "0")); // "Hell0, W0rld"
console.log(s.trim()); // remove whitespace
console.log(s.padStart(15, "*")); // "***Hello, World"
console.log("a,b,c".split(",").join("-")); // "a-b-c"템플릿 리터럴
템플릿 리터럴(백틱)은 ${}로 문자열 보간, 여러 줄 문자열, 태그된 템플릿을 허용합니다. 문자열 연결보다 훨씬 더 읽기 쉽습니다. 태그된 템플릿은 함수로 템플릿 리터럴을 처리하게 합니다 — styled-components, graphql-tag 등에서 사용됩니다.
const name = "Alice";
const age = 30;
// Template literals (backticks)
const greeting = `Hello, ${name}! You are ${age} years old.`;
console.log(greeting);
// Multi-line strings
const html = `
<div>
<h1>${name}</h1>
<p>Age: ${age}</p>
</div>
`;
// Expressions inside ${}
console.log(`Next year: ${age + 1}`);
console.log(`Upper: ${name.toUpperCase()}`);
console.log(`${name.length} chars`);
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) =>
acc + str + (values[i] ? `<b>${values[i]}</b>` : ''), '');
}
const result = highlight`Name: ${name}, Age: ${age}`;
// "Name: <b>Alice</b>, Age: <b>30</b>"문자열 검색 & 정규식
JavaScript 문자열은 match(), matchAll(), replace(), search(), split()을 통해 정규식을 지원합니다. 명명된 캡처 그룹(?<name>...)(ES2018)은 정규식을 더 읽기 쉽게 만듭니다. matchAll()은 이터레이터를 반환합니다 (전역 정규식에 match()보다 효율적). 추출 없이 패턴이 매칭되는지 검사하려면 .test()를 사용하세요.
const text = "The quick brown fox jumps over the lazy dog";
// Search methods
console.log(text.search(/brown/)); // 10 (index of match)
console.log(text.match(/\w+/g)); // ["The","quick","brown",...]
console.log(text.matchAll(/\w+/g)); // iterator of matches
// Replace with regex
console.log(text.replace(/o/g, "0")); // replace all 'o'
console.log(text.replace(/(\w+)/g, "\$1!")); // capture group
// Capture groups
const date = "2024-01-15";
const [, year, month, day] = date.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(year, month, day); // 2024 01 15
// Named capture groups (ES2018)
const m = date.match(/(?<year>\d{4})-(?<month>\d{2})/);
console.log(m.groups.year); // 2024
console.log(m.groups.month); // 01
// Test if pattern matches
const emailRe = /^[^@]+@[^@]+\.[^@]+$/;
console.log(emailRe.test("[email protected]")); // true문자열 반복 & 전개
문자열은 for...of로 반복 가능합니다. 전개 연산자 [...]는 문자열을 문자로 분할합니다 — 올바른 이모지 처리(서로게이트 쌍)에 필수적입니다. 문자열 비교는 UTF-16 코드 단위별로 사전식이므로 로케일 인식 정렬에는 localeCompare()를 사용하세요. 이모지와 일부 문자는 2코드 단위 길이입니다.
const s = "Hello";
// Iterate characters
for (const char of s) {
console.log(char); // H, e, l, l, o
}
// Spread into array
const chars = [...s]; // ["H", "e", "l", "l", "o"]
console.log(chars);
// Spread with map/filter
const upper = [...s].map(c => c.toUpperCase()).join("");
console.log(upper); // HELLO
// String comparison
console.log("a" < "b"); // true (lexicographic)
console.log("apple" < "banana"); // true
console.log("Z" < "a"); // true (uppercase < lowercase in ASCII)
// Locale-aware comparison
console.log("ö".localeCompare("o", "de")); // locale-specific
// Repeat
console.log("ab".repeat(3)); // "ababab"
// Code points (handles emoji correctly)
const emoji = "😀";
console.log(emoji.length); // 2 (surrogate pair!)
console.log([...emoji].length); // 1 (correct)
console.log("😀".codePointAt(0)); // 128512숫자 & 수학
숫자 & 연산자
JavaScript는 단일 숫자 타입(64비트 float)을 가집니다 — 별도의 int/float가 없습니다. BigInt(n 접미사)는 2^53을 넘는 정수를 처리합니다. 부동소수점 산술에는 정밀도 문제가 있습니다 (0.1 + 0.2 !== 0.3) — 비교에는 Number.EPSILON을 사용하세요. **는 거듭제곱 연산자입니다 (ES2016).
// JavaScript has one number type (IEEE 754 double)
const int = 42;
const float = 3.14;
const exp = 1e6; // 1000000
const hex = 0xff; // 255
const bin = 0b1010; // 10
const oct = 0o755; // 493
const big = 9007199254740993n; // BigInt (arbitrary precision)
// Arithmetic
console.log(10 / 3); // 3.333...
console.log(10 % 3); // 1 (remainder)
console.log(2 ** 10); // 1024 (exponent)
console.log(Math.floor(10 / 3)); // 3
console.log(Math.trunc(-3.7)); // -3 (toward zero)
// Increment/decrement
let x = 5;
console.log(x++); // 5 (post-increment, returns old)
console.log(++x); // 7 (pre-increment, returns new)
// Floating point issues
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false!
// Fix: use Number.EPSILON or round
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // trueMath 객체
Math 객체는 상수와 함수를 제공합니다. 모든 삼각 함수는 라디안을 사용합니다. Math.random()은 [0, 1)을 반환합니다 — 정수 범위는 곱하고 버림하세요. 암호학적 무작위성에는 crypto.getRandomValues()를 사용하세요. Math.max/min은 배열을 직접 받지 않습니다 — ...로 전개하세요.
// Constants
console.log(Math.PI); // 3.141592653589793
console.log(Math.E); // 2.718281828459045
console.log(Math.SQRT2); // 1.4142135623730951
// Rounding
console.log(Math.round(3.7)); // 4 (nearest)
console.log(Math.floor(3.7)); // 3 (down)
console.log(Math.ceil(3.2)); // 4 (up)
console.log(Math.trunc(-3.7)); // -3 (toward zero)
console.log(Math.sign(-5)); // -1 (sign: -1, 0, or 1)
// Power & roots
console.log(Math.pow(2, 10)); // 1024
console.log(Math.sqrt(144)); // 12
console.log(Math.cbrt(27)); // 3
console.log(Math.abs(-5)); // 5
// Min/Max
console.log(Math.max(1, 5, 3)); // 5
console.log(Math.min(1, 5, 3)); // 1
console.log(Math.max(...[1, 5, 3])); // 5 (spread array)
// Trigonometry (radians)
console.log(Math.sin(Math.PI / 2)); // 1
console.log(Math.cos(0)); // 1
console.log(Math.PI / 180 * 90); // radians from degrees
// Random
console.log(Math.random()); // 0 to <1
console.log(Math.floor(Math.random() * 100)); // 0-99 integerNumber 메서드 & 파싱
parseInt()에는 항상 기수(진법)를 지정하세요 — 이전 브라우저는 선행 0을 8진수로 해석합니다. Number.isNaN()은 신뢰할 수 있습니다; 전역 isNaN()은 강제 변환합니다 (isNaN('abc')가 true). toFixed()는 숫자가 아닌 문자열을 반환합니다. MAX_SAFE_INTEGER를 넘는 숫자는 정밀도를 잃습니다 — BigInt를 사용하세요.
// Number methods
const num = 1234.5678;
console.log(num.toFixed(2)); // "1234.57" (string)
console.log(num.toPrecision(3)); // "1.23e+3"
console.log(num.toString(2)); // binary string
console.log((255).toString(16)); // "ff" (hex)
// Number object methods
console.log(Number.isInteger(42)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isNaN(NaN)); // true (reliable)
console.log(Number.isNaN("NaN")); // false (global isNaN would be true)
console.log(Number.parseInt("42px")); // 42
console.log(Number.parseFloat("3.14")); // 3.14
// Parsing strings
console.log(parseInt("42", 10)); // 42 (always specify radix!)
console.log(parseInt("0xff", 16)); // 255
console.log(parseFloat("3.14abc")); // 3.14
// Safe integers
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.isSafeInteger(2 ** 53)); // false
// NaN checks
const result = Number("abc"); // NaN
console.log(Number.isNaN(result)); // true
console.log(result === NaN); // false! NaN !== NaN데이터 구조
배열
배열은 동적이고 순서가 있으며 혼합 타입을 가질 수 있습니다. push/pop은 O(1); shift/unshift/splice는 O(n)입니다. find()/findIndex()는 술어 함수를 받습니다. forEach()는 아무것도 반환하지 않습니다; 변환하려면 map()을 사용하세요. 배열은 객체입니다 — typeof []는 'object'입니다. 검사하려면 Array.isArray()를 사용하세요.
// Creating arrays
const nums = [1, 2, 3, 4, 5];
const mixed = [1, "hello", true, null];
const empty = new Array(5); // [empty x 5]
// Access & modify
console.log(nums[0]); // 1
console.log(nums.length); // 5
nums[0] = 0; // modify
console.log(nums.at(-1)); // 5 (negative index, ES2022)
// Add/remove
nums.push(6); // add to end, returns new length
nums.pop(); // remove from end, returns element
nums.unshift(0); // add to start
nums.shift(); // remove from start
nums.splice(1, 2); // remove 2 elements at index 1
nums.splice(1, 0, "a"); // insert at index 1
// Search
console.log(nums.indexOf(3)); // index or -1
console.log(nums.includes(3)); // true/false
console.log(nums.find(n => n > 3)); // first match
console.log(nums.findIndex(n => n > 3)); // index of first match
// Iterate
nums.forEach((val, idx) => console.log(idx, val));
for (const [idx, val] of nums.entries()) {
console.log(idx, val);
}배열 메서드 (map, filter, reduce)
map/filter/reduce는 함수형 배열 프로그래밍의 삼위일체입니다. map은 변환하고, filter는 선택하고, reduce는 집계합니다. 체인 가능하고 원본을 변이하지 않습니다 (reverse/sort 제외). sort()는 기본적으로 문자열로 변환합니다 — 숫자에는 항상 비교자를 제공하세요. flat()은 중첩 배열을 평면화합니다.
const nums = [1, 2, 3, 4, 5];
// map - transform each element (returns new array)
const doubled = nums.map(n => n * 2); // [2, 4, 6, 8, 10]
const withIndex = nums.map((n, i) => `${i}:${n}`);
// filter - keep elements that pass test (returns new array)
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
// reduce - accumulate to single value
const sum = nums.reduce((acc, n) => acc + n, 0); // 15
const product = nums.reduce((acc, n) => acc * n, 1); // 120
const max = nums.reduce((a, b) => Math.max(a, b));
// Chaining
const result = nums
.filter(n => n % 2 === 0) // [2, 4]
.map(n => n * 10) // [20, 40]
.reduce((sum, n) => sum + n, 0); // 60
// Other useful methods
console.log(nums.slice(1, 3)); // [2, 3] (copy portion)
console.log(nums.concat([6, 7])); // [1,2,3,4,5,6,7]
console.log([...nums, 6, 7]); // spread (modern)
console.log(nums.reverse()); // reverse in-place
console.log(nums.sort((a, b) => a - b)); // numeric sort
console.log([3, 1, 2].sort()); // [1, 2, 3] (string sort!)
console.log(nums.flat()); // flatten one level
console.log([1, [2, [3]]].flat(Infinity)); // [1, 2, 3]객체
객체는 키-값 컬렉션입니다 (키는 문자열이나 심볼). 정적 키에는 점 표기법, 동적/특수 키에는 대괄호 표기법. 계산된 속성 이름 {[expr]: val}은 ES6입니다. Object.keys/values/entries는 배열을 추출하고; Object.fromEntries는 entries를 역전합니다. for...in은 키를 반복합니다 (상속된 것 포함).
// Object literal
const user = {
name: "Alice",
age: 30,
"is-admin": false, // keys with special chars need quotes
greet() { // method shorthand
return `Hello, I'm ${this.name}`;
}
};
// Access
console.log(user.name); // dot notation
console.log(user["is-admin"]); // bracket notation (for dynamic/special keys)
// Computed property names (ES6)
const key = "dynamic";
const obj = { [key]: "value", [`id_${1}`]: 100 };
// Add/modify/delete
user.email = "[email protected]"; // add
user.age = 31; // modify
delete user["is-admin"]; // delete
// Check property existence
console.log("name" in user); // true
console.log(user.hasOwnProperty("name")); // true
// Iterate
for (const key in user) {
console.log(key, user[key]);
}
// Object methods
console.log(Object.keys(user)); // ["name", "age", ...]
console.log(Object.values(user)); // ["Alice", 31, ...]
console.log(Object.entries(user)); // [["name","Alice"], ...]
console.log(Object.fromEntries([["a", 1]])); // {a: 1}구조 분해 & 전개
구조 분해는 객체/배열에서 값을 간결하게 추출합니다. 이름 변경(key: newName), 기본값(= value), 나머지(...rest)을 지원합니다. 전개(...)는 반복 가능 객체/객체를 확장합니다 — 병합과 얕은 복사에 훌륭합니다. 객체 전개는 중복 키를 덮어씁니다 (마지막이 승리). 함수 매개변수에서 구조 분해는 선택적 구성에 강력합니다.
// Object destructuring
const user = { name: "Alice", age: 30, email: "[email protected]" };
const { name, age } = user; // extract by key
const { name: fullName, email = "N/A" } = user; // rename + default
const { ...rest } = user; // rest pattern
// name -> undefined, fullName -> "Alice", rest -> {age, email}
// Array destructuring
const [a, b, c] = [1, 2, 3];
const [first, , third] = [1, 2, 3]; // skip elements
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
// Swap variables
let x = 1, y = 2;
[x, y] = [y, x]; // x=2, y=1
// Nested destructuring
const { data: { users } } = response;
const [[a, b], [c, d]] = [[1, 2], [3, 4]];
// Function parameters
function greet({ name, greeting = "Hello" }) {
return `${greeting}, ${name}`;
}
greet({ name: "Alice" }); // "Hello, Alice"
// Spread operator
const arr1 = [1, 2], arr2 = [3, 4];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
const obj1 = { a: 1 }, obj2 = { b: 2 };
const combined = { ...obj1, ...obj2 }; // {a:1, b:2}
const clone = { ...user }; // shallow copyMap, Set, WeakMap
Map은 모든 키 타입(문자열뿐 아닌 객체)을 허용하고 삽입 순서를 유지합니다 — 일반 객체와 다릅니다. Set은 고유 값을 저장합니다 — 중복 제거에 완벽합니다. WeakMap/WeakSet 키는 약하게 참조되어 (가비지 수집 가능) 메모리 누수를 방지합니다. 비문자열 키나 빈번한 추가/삭제가 필요할 때 Map을 사용하세요.
// Map - key-value pairs, any key type, maintains insertion order
const map = new Map();
map.set("name", "Alice");
map.set(42, "number key");
map.set({ obj: true }, "object key");
console.log(map.get("name")); // "Alice"
console.log(map.has("name")); // true
console.log(map.size); // 3
map.delete(42);
map.clear();
// Iterate Map
for (const [key, value] of map) {
console.log(key, value);
}
// Set - unique values
const set = new Set([1, 2, 3, 2, 1]);
console.log(set.size); // 3 (duplicates removed)
set.add(4);
set.has(2); // true
set.delete(1);
// Set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersection = new Set([...a].filter(x => b.has(x))); // {2,3}
const difference = new Set([...a].filter(x => !b.has(x))); // {1}
// WeakMap/WeakSet - keys must be objects, GC-friendly
const weakMap = new WeakMap();
weakMap.set({}, "value"); // key can be garbage collected제어 흐름
If / Else & 삼항
복잡한 로직에는 if/else를, 간단한 값 선택에는 삼항을 사용하세요. &&와 ||는 단락 평가합니다 (기본값/조건문에 유용). ??(널 병합)는 모든 거짓 값을 검사하는 ||와 달리 null/undefined만 검사합니다. ?.(옵셔널 체이닝)은 에러 없이 중첩 속성에 안전하게 접근합니다.
const score = 85;
// if / else if / else
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C");
}
// Ternary operator (expression)
const grade = score >= 60 ? "pass" : "fail";
// Nested ternary (avoid - hard to read)
const status = score >= 90 ? "excellent"
: score >= 80 ? "good"
: score >= 60 ? "pass"
: "fail";
// Short-circuit evaluation
const name = user?.name || "Anonymous"; // default value
const value = condition && doSomething(); // execute if true
// Nullish coalescing (??) - only null/undefined, not falsy
const count = 0 ?? 10; // 0 (not 10, because 0 is not null/undefined)
const name2 = null ?? "default"; // "default"
// Optional chaining (?.)
const city = user?.address?.city; // undefined if any link is null
const length = user?.name?.length; // undefined if user or name is nullSwitch 문
switch는 엄격 동등(===)으로 비교하므로 타입이 중요합니다. break를 잊지 마세요 — 없으면 다음 case로 떨어집니다. 케이스를 쌓아 그룹화하세요 (case 6: case 7:). Switch는 이산 값에 긴 if/else 체인보다 깔끔합니다. 현대 코드는 때때로 객체 조회 테이블을 선호합니다.
const day = 3;
// Traditional switch (use break!)
switch (day) {
case 1:
console.log("Monday");
break; // without break, falls through!
case 2:
console.log("Tuesday");
break;
case 6:
case 7: // multiple cases share code
console.log("Weekend");
break;
default:
console.log("Weekday");
}
// Switch with return (no break needed)
function getColor(type) {
switch (type) {
case "success": return "green";
case "error": return "red";
case "warning": return "yellow";
default: return "gray";
}
}
// Strict equality (===)
switch (1) {
case "1": console.log("string"); // NOT matched
case 1: console.log("number"); // matched
}루프
배열/문자열에는 for...of(값), 객체에는 for...in(키)을 사용하세요 — 배열에 for...in은 절대 사용하지 마세요 (인덱스를 문자열로 반복하고 프로토타입 포함). while은 실행 전 검사하고; do...while은 최소 한 번 실행합니다. break는 종료, continue는 건너뜁니다. for...of로 인덱스+값을 얻으려면 .entries()를 사용하세요.
// for loop
for (let i = 0; i < 5; i++) {
console.log(i);
}
// for...of (iterable values - arrays, strings, maps)
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
// for...in (object keys - AVOID for arrays!)
const user = { name: "Alice", age: 30 };
for (const key in user) {
console.log(key, user[key]);
}
// while
let count = 0;
while (count < 3) {
console.log(count);
count++;
}
// do...while (runs at least once)
let i = 0;
do {
console.log(i);
i++;
} while (i < 3);
// break & continue
for (let i = 0; i < 10; i++) {
if (i === 5) break; // exit loop
if (i % 2 === 0) continue; // skip iteration
console.log(i);
}
// Iterate with index
for (const [index, value] of fruits.entries()) {
console.log(index, value);
}이터레이터 & 제너레이터
이터레이터는 {value, done}을 반환하는 next()를 구현합니다. 제너레이터(function*)는 yield로 이터레이터 생성을 단순화합니다 — 실행을 일시 중단하고 next()에 재개합니다. 제너레이터는 지연 평가(요구 시 계산)되고 무한할 수 있습니다. 사용자 정의 반복 가능 객체, 시퀀스, async 흐름에 사용하세요.
// Iterable protocol (Symbol.iterator)
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
}
};
}
};
for (const num of range) console.log(num); // 1, 2, 3, 4, 5
console.log([...range]); // [1, 2, 3, 4, 5]
// Generator function (function*)
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const gen = idGenerator();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next().value); // 3
// Generator with yield*
function* nested() {
yield 1;
yield* [2, 3, 4]; // delegate to another iterable
yield 5;
}함수
함수 선언 & 표현식
함수 선언은 호이스팅됩니다 (정의 전에 호출 가능); 표현식은 그렇지 않습니다. 화살표 함수는 간결하고 자체 'this'가 없습니다 (둘러싸는 스코프에서 상속). IIFE는 private 스코프를 만듭니다 (모듈로 덜 필요). 함수는 일급입니다 — 인수로 전달, 반환, 변수에 저장하세요.
// Function declaration (hoisted - can be called before definition)
greet("Alice"); // works (hoisted)
function greet(name) {
return `Hello, ${name}!`;
}
// Function expression (not hoisted)
const greet2 = function(name) {
return `Hi, ${name}!`;
};
// Named function expression (for recursion/stack traces)
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
// Arrow function (ES6) - concise, no own this
const add = (a, b) => a + b;
const square = x => x * x;
const greet3 = () => "Hello!";
const log = x => { console.log(x); }; // block body needs return
// IIFE (Immediately Invoked Function Expression)
const result = (function() {
const private = "secret";
return private.toUpperCase();
})();
// Functions are first-class (can be passed/returned)
function apply(fn, value) {
return fn(value);
}
console.log(apply(square, 5)); // 25화살표 함수 & this
화살표 함수는 자체 'this', 'arguments', 'super', 'new.target'이 없습니다 — 둘러싸는 스코프에서 상속합니다. 이로 인해 콜백(특히 클래스 메서드에서)에 완벽합니다. 하지만 생성자나 자체 'this'가 필요한 메서드로는 사용할 수 없습니다. 객체 메서드에는 일반 함수를 사용하세요.
// Arrow function variations
const add = (a, b) => a + b; // implicit return
const greet = name => `Hi ${name}`; // single param, no parens
const log = () => console.log("hi"); // no params
const obj = (x, y) => ({ x, y }); // return object needs parens
const multi = (a, b) => { // block body
const sum = a + b;
return sum * 2;
};
// Arrow functions don't have their own 'this'
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // 'this' refers to Timer instance
console.log(this.seconds);
}, 1000);
}
// Regular function has its own 'this'
function Counter() {
this.count = 0;
// Regular function: 'this' is undefined (strict) or global
document.addEventListener("click", function() {
// this.count++; // ERROR: this is not Counter
});
// Arrow function: 'this' is Counter
document.addEventListener("click", () => {
this.count++; // works!
});
}
// Arrow functions can't be constructors
// const obj = new arrowFunc(); // TypeError클로저
클로저는 스코프가 종료된 후에도 정의 스코프의 변수를 '기억'하는 함수입니다. 데이터 프라이버시(모듈 패턴), 메모이제이션, 커링, 부분 적용을 가능하게 합니다. JavaScript의 모든 함수는 클로저입니다. 내부 함수는 외부 변수의 복사본이 아닌 참조를 유지합니다.
// A closure is a function that remembers its outer variables
function makeCounter() {
let count = 0; // private variable
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is private - can't access directly
// Module pattern (pre-ES6)
const calculator = (function() {
const result = 0; // private
return {
add(x) { return result + x; },
multiply(x) { return result * x; }
};
})();
// Practical: memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (!(key in cache)) {
cache[key] = fn.apply(this, args);
}
return cache[key];
};
}
const slowFib = memoize(n =>
n < 2 ? n : slowFib(n - 1) + slowFib(n - 2)
);
// Currying with closures
const multiply = a => b => a * b;
const double = multiply(2);
console.log(double(5)); // 10인수 & Rest/전개
기본 매개변수는 대체 값을 제공합니다. Rest 매개변수(...name)는 추가 인수를 실제 배열로 수집합니다 — 레거시 'arguments' 객체보다 선호하세요. 전개(...)는 배열을 개별 인수로 확장합니다. 매개변수에서 구조 분해는 명명된 선택적 구성 객체를 가능하게 합니다 — 일반적인 API 패턴입니다.
// Default parameters
function greet(name = "Guest", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
greet(); // "Hello, Guest!"
greet("Alice"); // "Hello, Alice!"
greet("Bob", "Hi"); // "Hi, Bob!"
// Rest parameters (...args collects into array)
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
function log(tag, ...args) {
console.log(tag, args);
}
// Spread (opposite of rest)
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // 3 (spread array as args)
console.log(sum(...nums)); // 6
// arguments object (old way, avoid in modern code)
function old() {
console.log(arguments); // array-like, not a real array
const args = Array.from(arguments); // convert
}
// Destructuring parameters
function process({ name, age = 0 } = {}) {
console.log(name, age);
}
process({ name: "Alice" }); // Alice 0
process(); // undefined 0 (default empty object)OOP & 클래스
클래스 & 생성자
ES6 클래스는 프로토타입 위의 syntactic sugar입니다. Private 필드(#name)는 ES2022이고 진정으로 private입니다 (_name 규칙과 다름). getter/setter는 계산된 프로퍼티를 허용합니다. 정적 멤버는 인스턴스가 아닌 클래스에 속합니다. 클래스 필드(name = value)는 생성자 없이 인스턴스 프로퍼티를 초기화합니다.
class Person {
// Fields (class fields proposal - ES2022)
species = "human"; // instance field
// Private fields (ES2022)
#ssn = "secret"; // truly private
// Static field/method
static count = 0;
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
Person.count++;
}
// Instance method
greet() {
return `Hello, I'm ${this.name}`;
}
// Getter
get info() {
return `${this.name}, ${this.age}`;
}
// Setter
set age(value) {
if (value < 0) throw new Error("Invalid age");
this._age = value;
}
get age() {
return this._age;
}
// Static method
static create(name) {
return new Person(name, 0);
}
}
const alice = new Person("Alice", 30);
console.log(alice.greet()); // Hello, I'm Alice
console.log(alice.info); // Alice, 30
console.log(Person.count); // 1
// alice.#ssn; // SyntaxError - private!상속 & 다형성
extends는 상속을 만들고; super()는 부모 생성자를 호출합니다 ('this' 사용 전 필수). 메서드를 재정의하려면 재정의하세요. JavaScript는 단일 상속이지만, 믹스인(클래스 팩토리)은 구성을 제공합니다. instanceof는 프로토타입 체인을 검사합니다. 다형성은 메서드 재정의를 통해 작동합니다.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
// Static method can be inherited
static create(type, name) {
return new type(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // must call super() first
this.breed = breed;
}
speak() {
return `${this.name} barks`; // override
}
fetch() {
return `${this.name} fetches`;
}
}
class Cat extends Animal {
speak() {
return `${this.name} meows`;
}
}
// Polymorphism
const animals = [new Dog("Rex", "Lab"), new Cat("Whiskers")];
animals.forEach(a => console.log(a.speak()));
// Rex barks
// Whiskers meows
// instanceof checks
const dog = new Dog("Buddy", "Poodle");
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog.constructor.name); // "Dog"
// Mixins (multiple inheritance alternative)
const Walker = (Base) => class extends Base {
walk() { return `${this.name} walks`; }
};
class Robot extends Walker(Animal) {}프로토타입
JavaScript는 프로토타입 상속을 사용합니다 — 객체는 프로토타입 체인을 통해 다른 객체에서 상속합니다. __proto__는 더 이상 사용되지 않습니다; Object.getPrototypeOf/setPrototypeOf를 사용하세요. 클래스는 이 시스템 위의 syntactic sugar입니다. 내장 프로토타입(Array.prototype) 수정은 위험합니다 — 코드를 손상시킬 수 있습니다. 깊은 상속보다 구성을 선호하세요.
// Every object has a prototype (chain of inheritance)
const obj = {};
console.log(obj.__proto__); // Object.prototype
console.log(Object.getPrototypeOf(obj)); // preferred
// Constructor function (pre-class syntax)
function OldPerson(name) {
this.name = name;
}
OldPerson.prototype.greet = function() {
return `Hi, ${this.name}`;
};
const p = new OldPerson("Alice");
console.log(p.greet()); // Hi, Alice
// Prototype chain
// p -> OldPerson.prototype -> Object.prototype -> null
// Adding to prototype (affects all instances)
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3
// Object.create (prototypal inheritance)
const animal = { type: "unknown" };
const dog = Object.create(animal);
dog.type = "dog";
console.log(dog.type); // "dog" (own property)
// Check own vs inherited
console.log(dog.hasOwnProperty("type")); // true
console.log("type" in dog); // true (includes inherited)
// Get prototype chain
let proto = Object.getPrototypeOf(dog);
while (proto) {
console.log(proto);
proto = Object.getPrototypeOf(proto);
}에러 처리
Try / Catch / Finally
try/catch/finally는 예외를 처리합니다. catch는 에러 객체를 바인딩합니다 (.message와 .stack 포함). instanceof로 특정 에러 타입을 다르게 처리하세요. 예상 에러 처리 후 알 수 없는 에러를 항상 다시 던지세요. 응용 프로그램별 에러 처리를 위해 Error를 확장하여 사용자 정의 에러를 만드세요.
try {
const result = JSON.parse('{"invalid json"');
} catch (error) {
console.error("Parse error:", error.message);
console.error("Stack:", error.stack);
} finally {
console.log("Always runs");
}
// Catch specific error types
try {
const data = JSON.parse(input);
if (!data.name) throw new TypeError("name is required");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("JSON syntax error");
} else if (error instanceof TypeError) {
console.log("Type error:", error.message);
} else {
throw error; // re-throw unknown errors
}
}
// Error properties
const err = new Error("Something went wrong");
err.code = "CUSTOM_ERROR";
err.statusCode = 500;
throw err;
// Built-in error types
// Error, TypeError, RangeError, ReferenceError
// SyntaxError, URIError, EvalError사용자 정의 에러
추가 컨텍스트(필드, 코드)가 있는 사용자 정의 에러 타입을 만들려면 Error를 확장하세요. 항상 클래스 이름과 일치하도록 this.name을 설정하세요. instanceof로 특정 에러 타입을 잡으세요. 에러 체이닝(ES2022 { cause })은 디버깅을 위해 원래 에러를 보존합니다. 좋은 에러 계층은 에러 처리를 정밀하게 만듭니다.
// Custom error class
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query;
}
}
// Usage
function validateUser(user) {
if (!user.email) {
throw new ValidationError("email", "Email is required");
}
if (!user.email.includes("@")) {
throw new ValidationError("email", "Invalid email format");
}
}
// Handling custom errors
try {
validateUser({ name: "Alice" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed: ${error.field} - ${error.message}`);
} else if (error instanceof DatabaseError) {
console.log(`DB error in query: ${error.query}`);
} else {
console.log("Unexpected error:", error);
}
}
// Error chaining (ES2022)
try {
throw new Error("Original cause");
} catch (cause) {
throw new Error("Failed to process", { cause });
}Async & Promises
콜백
콜백은 나중에 호출되도록 전달되는 함수입니다. 에러 우선 규칙(err, data)은 Node.js의 표준입니다. 중첩된 콜백은 '콜백 지옥'을 만듭니다 — 깊이 중첩되어 읽기 어려운 코드. Promises와 async/await가 이를 해결합니다. setTimeout/setInterval은 일반적인 콜백 기반 API입니다.
// Callback pattern (old way)
function fetchData(url, callback) {
setTimeout(() => {
callback(null, { data: "result" });
}, 1000);
}
// Callback with error-first convention (Node.js style)
fetchData("/api", (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});
// Callback hell (pyramid of doom)
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
// deeply nested...
});
});
});
// Event listeners are callbacks
button.addEventListener("click", (event) => {
console.log("Clicked!", event.target);
});
// setTimeout / setInterval
setTimeout(() => console.log("After 1s"), 1000);
const id = setInterval(() => console.log("tick"), 1000);
clearInterval(id); // stopPromises
Promises는 세 가지 상태의 미래 값을 나타냅니다: 대기, 이행, 거부. .then()은 성공을, .catch()는 에러를, .finally()는 항상 처리합니다. Promise.all()은 모두 대기(빠른 실패); allSettled()는 모두 대기(실패 없음); race()는 첫 번째 확정 반환; any()는 첫 번째 성공 반환.
// Creating a promise
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Data received");
} else {
reject(new Error("Failed"));
}
});
// Consuming a promise
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log("Done"));
// Chaining
fetch("/api/users")
.then(response => response.json())
.then(users => users.filter(u => u.active))
.then(active => console.log(active))
.catch(err => console.error("Error:", err));
// Promise.all - wait for all (fails if any fails)
Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]).then(([users, posts]) => {
console.log(users, posts);
});
// Promise.allSettled - wait for all (never fails)
Promise.allSettled([p1, p2]).then(results => {
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
});
// Promise.race - first to settle wins
Promise.race([p1, p2]).then(first => console.log(first));
// Promise.any - first to succeed
Promise.any([p1, p2]).then(first => console.log(first));Async / Await
async/await는 promises 위의 syntactic sugar입니다 — async 코드를 동기식으로 보이게 합니다. 'await'는 promise가 확정될 때까지 함수를 일시 중단합니다. 에러 처리를 위해 항상 await를 try/catch로 감싸세요. 병렬 작업에는 Promise.all()을 사용하세요 (루프에서 순차 await보다 빠름). 최상위 await는 ES 모듈에서 작동합니다.
// async function always returns a Promise
async function fetchData() {
// await pauses until the promise resolves
const response = await fetch("/api/users");
const data = await response.json();
return data;
}
// Error handling with try/catch
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (error) {
console.error("Failed:", error);
return null;
}
}
// Sequential vs parallel
async function sequential() {
const a = await fetch("/api/a"); // waits for a
const b = await fetch("/api/b"); // then waits for b
return [a, b];
}
async function parallel() {
const [a, b] = await Promise.all([ // both at once
fetch("/api/a"),
fetch("/api/b")
]);
return [a, b];
}
// Top-level await (ES2022, in modules)
// const data = await fetch("/api").then(r => r.json());
// Iterating with async
async function processUrls(urls) {
for (const url of urls) {
const data = await fetch(url);
console.log(data);
}
}
// Promise.all with map (parallel)
async function fetchAll(urls) {
return Promise.all(urls.map(url => fetch(url)));
}DOM 조작
요소 선택 & 수정
querySelector/querySelectorAll(CSS 선택자)는 요소를 선택하는 현대적인 방법입니다. textContent는 innerHTML보다 안전합니다 (XSS 방지). classList는 클래스에 add/remove/toggle/contains를 제공합니다. dataset은 data-* 속성에 접근합니다. innerHTML 설정 전에 항상 사용자 입력을 살균하세요.
// Selecting elements
const el = document.querySelector("#app"); // first match
const all = document.querySelectorAll(".item"); // all matches (NodeList)
const byId = document.getElementById("app");
const byClass = document.getElementsByClassName("item"); // HTMLCollection
// Modifying content
el.textContent = "Hello"; // text only (safe from XSS)
el.innerHTML = "<b>Bold</b>"; // HTML (XSS risk with user input!)
el.innerText = "Visible text"; // respects CSS visibility
// Attributes
el.setAttribute("data-id", "123");
const id = el.getAttribute("data-id");
el.removeAttribute("disabled");
el.dataset.id; // access data-* attributes
el.id = "new-id";
el.className = "active highlighted";
el.classList.add("active");
el.classList.remove("old");
el.classList.toggle("hidden");
el.classList.contains("active");
// Styles
el.style.color = "red";
el.style.backgroundColor = "blue"; // camelCase
el.style.cssText = "color: red; font-size: 16px;";
// Creating elements
const div = document.createElement("div");
div.textContent = "New element";
div.classList.add("box");
document.body.appendChild(div);
document.body.prepend(div); // add to beginning
document.body.insertBefore(div, referenceEl);이벤트
addEventListener는 on<event> 프로퍼티보다 선호됩니다 (여러 리스너 허용). 이벤트 위임(부모에서 듣기)은 동적으로 추가된 요소에 효율적입니다. e.target은 클릭된 것이고; e.currentTarget은 리스너가 있는 요소입니다. preventDefault()는 기본 동작을 중지하고; stopPropagation()은 버블링을 중지합니다.
// Add event listener
button.addEventListener("click", (event) => {
console.log("Clicked!", event.target);
console.log("Current target:", event.currentTarget);
});
// Common events
// click, dblclick, mousedown, mouseup, mousemove
// keydown, keyup, keypress
// submit, change, input, focus, blur
// load, DOMContentLoaded, resize, scroll
// Event object properties
input.addEventListener("keydown", (e) => {
console.log(e.key); // "Enter", "a", etc.
console.log(e.code); // "KeyA", "Enter"
console.log(e.ctrlKey); // true if Ctrl held
e.preventDefault(); // stop default behavior
e.stopPropagation(); // stop bubbling
});
// Event delegation (efficient for many elements)
document.addEventListener("click", (e) => {
if (e.target.matches(".delete-btn")) {
const id = e.target.dataset.id;
deleteItem(id);
}
});
// Custom events
const customEvent = new CustomEvent("userLogin", {
detail: { userId: 123 }
});
element.dispatchEvent(customEvent);
element.addEventListener("userLogin", (e) => {
console.log("User logged in:", e.detail.userId);
});
// Remove listener (must be same function reference)
const handler = () => console.log("click");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);모듈 & JSON
ES 모듈
ES 모듈(import/export)은 현대 표준이며 브라우저와 Node.js에서 지원됩니다. 기본 내보내기(모듈당 하나) vs 명명된 내보내기(여러 개). 동적 import()는 지연 로딩을 가능하게 합니다. 모듈은 항상 엄격 모드이고 자체 스코프를 가집니다. HTML script 태그에 type='module'을 사용하세요.
// math.js - exporting
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { /* ... */ }
// Default export (one per module)
export default function greet(name) {
return `Hello, ${name}`;
}
// main.js - importing
import greet from "./math.js"; // default
import { add, PI } from "./math.js"; // named
import * as math from "./math.js"; // namespace
import { add as plus } from "./math.js"; // rename
import greet, { add } from "./math.js"; // default + named
// Dynamic import (returns Promise)
const module = await import("./math.js");
console.log(module.add(2, 3));
// Conditional/dynamic loading
if (featureEnabled) {
const { default: Feature } = await import("./feature.js");
new Feature();
}
// Re-export
export { add } from "./math.js";
export * from "./utils.js";
// In HTML
// <script type="module" src="app.js"></script>
// Modules are deferred and in strict mode by defaultJSON
JSON.stringify()는 JS 값을 JSON 문자열로 변환하고; JSON.parse()는 역전합니다. 변환 중 필터링이나 변환을 위해 replacers/revivers를 사용하세요. JSON은 함수, undefined, 날짜(문자열이 됨) 또는 순환 참조를 지원하지 않습니다. fetch().json()은 JSON 응답을 자동으로 파싱합니다. 신뢰할 수 없는 입력의 JSON.parse는 항상 try/catch로 감싸세요.
// JSON is the standard data interchange format
// JSON supports: string, number, boolean, null, array, object
// JavaScript object to JSON string
const user = { name: "Alice", age: 30, active: true };
const jsonStr = JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'
// Pretty print
const pretty = JSON.stringify(user, null, 2);
// {
// "name": "Alice",
// "age": 30,
// "active": true
// }
// JSON string to JavaScript object
const parsed = JSON.parse('{"name":"Bob","age":25}');
console.log(parsed.name); // "Bob"
// Replacer function (filter/transform during stringify)
const filtered = JSON.stringify(user, (key, value) => {
if (key === "age") return undefined; // exclude
return value;
});
// Reviver function (transform during parse)
const data = JSON.parse(jsonStr, (key, value) => {
if (key === "date") return new Date(value);
return value;
});
// Fetch JSON from API
async function getUsers() {
const res = await fetch("/api/users");
return res.json(); // parses JSON automatically
}
// JSON limitations
// - No functions, undefined, or dates
// - No circular references (throws error)
// - Keys must be strings (with quotes)날짜 & 시간
JavaScript Date는 악명 높게 어렵습니다 — 월은 0부터(1월 = 0), 일은 1부터. Date 객체는 가변입니다. toISOString()은 UTC를; toLocaleString()은 현지 시간을 제공합니다. 진지한 날짜 작업에는 date-fns나 dayjs 같은 라이브러리를 사용하세요. Intl.DateTimeFormat은 로케일 인식 형식을 제공합니다.
// Create dates
const now = new Date();
const specific = new Date("2024-01-15T10:30:00");
const fromMs = new Date(1705315200000);
const fromParts = new Date(2024, 0, 15, 10, 30); // month is 0-indexed!
// Get components
console.log(now.getFullYear()); // 2024
console.log(now.getMonth()); // 0-11 (January = 0!)
console.log(now.getDate()); // 1-31
console.log(now.getDay()); // 0-6 (Sunday = 0)
console.log(now.getHours()); // 0-23
console.log(now.getTime()); // milliseconds since epoch
// Set components
now.setFullYear(2025);
now.setMonth(11); // December
// Formatting
console.log(now.toISOString()); // "2024-01-15T10:30:00.000Z"
console.log(now.toLocaleDateString()); // "1/15/2024" (locale)
console.log(now.toLocaleString()); // "1/15/2024, 10:30:00 AM"
// Timestamps
const start = Date.now(); // milliseconds since epoch
// ... do work ...
console.log(`Took ${Date.now() - start}ms`);
// Intl for formatting (modern)
const formatter = new Intl.DateTimeFormat("zh-CN", {
year: "numeric", month: "long", day: "numeric"
});
console.log(formatter.format(now)); // "2024年1月15日"ES6+ 기능
let, const & 블록 스코핑
기본적으로 const를, 재할당이 필요할 때 let을, var는 완전히 피하세요. const는 재할당을 방지하지만 객체/배열은 여전히 가변입니다. let과 const는 블록 스코프이고 선언 전 TDZ에 있습니다 (undefined로 호이스팅되는 var와 다름). 이는 많은 미묘한 버그를 방지합니다.
// var is function-scoped (hoisted, leaks out of blocks)
// let and const are block-scoped (stay inside {})
{
var x = 1; // accessible outside the block
let y = 2; // block-scoped
const z = 3; // block-scoped, cannot reassign
}
console.log(x); // 1
// console.log(y); // ReferenceError
// const prevents reassignment, NOT mutation
const arr = [1, 2, 3];
arr.push(4); // OK — mutating the array
// arr = [5]; // TypeError — reassigning const
// Temporal Dead Zone: let/const can't be used before declaration
// console.log(a); // ReferenceError (not undefined like var)
let a = 10;화살표 함수 & this
화살표 함수는 간결하고 둘러싸는 스코프에서 'this'를 상속합니다 — 콜백과 외부 'this'가 필요한 메서드에 완벽합니다. 하지만 생성자로 사용할 수 없고 'arguments' 객체가 없습니다. 'this'가 객체를 참조해야 하는 객체 메서드에 화살표 함수를 사용하지 마세요 (대신 일반 메서드 사용).
// Arrow functions: concise syntax, lexically bound 'this'
const add = (a, b) => a + b;
const square = x => x * x; // single param, no parens
const greet = name => `Hello, ${name}`; // template literal
const noop = () => {}; // no params, empty body
// Returning an object literal needs parens
const makeUser = (name, age) => ({ name, age });
// Arrow functions DON'T have their own 'this' — they inherit it
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // 'this' is the Counter instance (lexical)
console.log(this.count);
}, 1000);
}
// Regular function would lose 'this' (it'd be window/undefined)구조 분해 할당
구조 분해는 객체/배열에서 변수로 값을 한 줄로 추출합니다 — 수동 속성 접근보다 깨끗합니다. 객체 구조 분해는 { key }, 배열 구조 분해는 [index]를 사용합니다. 이름 변경(key: alias), 기본값(key = default), 나머지(...rest), 중첩 패턴을 지원합니다. React props와 함수 매개변수에서 많이 사용됩니다.
// Object destructuring
const user = { name: "Alice", age: 30, city: "NYC" };
const { name, age } = user;
console.log(name, age); // Alice 30
// Rename and default values
const { name: fullName, country = "USA" } = user;
console.log(fullName, country); // Alice USA
// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first, second, rest); // 1 2 [3,4,5]
// Swap variables (no temp needed!)
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Destructuring in function parameters
function greet({ name, greeting = "Hello" }) {
console.log(`${greeting}, ${name}`);
}
greet({ name: "Bob" }); // Hello, Bob
// Nested destructuring
const { data: { results } } = response;전개 & Rest 연산자
... 연산자는 확장 시(배열/객체/호출에서) '전개', 수집 시(매개변수/구조 분해에서) 'rest'입니다. 전개는 얕은 복사를 만들고 객체를 병합합니다 (나중 키가 이전 키를 덮어씀). Rest 매개변수는 이전 'arguments' 객체를 대체하고 실제 Array입니다. 둘 다 필수적인 현대 JS 관용구입니다.
// Spread (...) expands iterables into individual elements
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5] (copy + append)
const merged = [...arr1, ...arr2];
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 } (shallow copy)
const updated = { ...obj1, b: 99 }; // override b: { a: 1, b: 99 }
// Rest (...) collects multiple elements into an array
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
// Spread in function calls
Math.max(...[1, 5, 3]); // 5 (same as Math.max(1, 5, 3))템플릿 리터럴 & 태그된 템플릿
템플릿 리터럴(백틱)은 여러 줄 문자열과 ${} 보간을 지원합니다 — 문자열 연결보다 훨씬 깔끔합니다. 태그된 템플릿은 함수가 리터럴 부분과 보간된 값을 처리하게 하여 사용자 정의 형식 지정, 살균(예: HTML 이스케이프) 또는 i18n을 가능하게 합니다. styled-components와 graphql-tag에서 인기 있습니다.
// Template literals: backticks, multi-line, interpolation
const name = "Alice";
const msg = `Hello ${name},
this spans
multiple lines`;
// Expressions inside ${}
const price = 19.99;
const tax = 0.08;
console.log(`Total: ${(price * (1 + tax)).toFixed(2)}`); // Total: 21.59
// Nested template literals
const items = ["apple", "banana"];
const html = `
<ul>
${items.map(i => `<li>${i}</li>`).join("")}
</ul>
`;
// Tagged templates: function processes the literal
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) =>
acc + str + (values[i] ? `**${values[i]}**` : ""), "");
}
const result = highlight`Name: ${name}, Age: ${30}`;
// "Name: **Alice**, Age: **30**"옵셔널 체이닝 & 널 병합
옵셔널 체이닝(?.)은 체인의 일부가 null/undefined인 경우 undefined로 단락 평가합니다 — 장황한 수동 검사를 제거합니다. 널 병합(??)은 0, '', false도 덮어쓰는 ||와 달리 null/undefined에만 기본값을 제공합니다. 함께 가장 일반적인 '누락된 데이터' 패턴을 안전하게 처리합니다. ES2020부터 사용 가능.
// Optional chaining (?.): safely access nested properties
const user = { profile: { name: "Alice" } };
console.log(user?.profile?.name); // "Alice"
console.log(user?.settings?.theme); // undefined (no error!)
console.log(user?.profile?.address?.city); // undefined
// Without ?., you'd write:
// user && user.profile && user.profile.name
// Optional method calls
const result = obj?.method?.();
const firstChar = str?.[0];
// Nullish coalescing (??): default only for null/undefined
const theme = user.settings?.theme ?? "light"; // "light"
const count = 0 ?? 10; // 0 (NOT 10 — ?? keeps falsy but valid values)
const name = "" ?? "Anonymous"; // "" (empty string is kept!)
// vs || which treats all falsy values (0, "", false) as missing
const count2 = 0 || 10; // 10 (probably not what you want!)이벤트 & 이벤트 처리
addEventListener 기본
addEventListener는 이벤트를 바인딩하는 현대적인 방법입니다 — 여러 핸들러를 허용하고, 이벤트 위임을 지원하며, once, passive, capture 같은 옵션을 제공합니다. 나중에 제거해야 하는 경우 핸들러에 대한 참조를 항상 유지하세요 (익명 함수는 제거할 수 없음). 이벤트 객체는 target, currentTarget, preventDefault(), stopPropagation()을 전달합니다.
// Modern event binding (preferred over onclick=)
const button = document.querySelector("#myBtn");
button.addEventListener("click", function(event) {
console.log("Clicked!", event.target);
event.preventDefault(); // stop default action (e.g., form submit)
});
// Multiple listeners can be attached
button.addEventListener("click", handler1);
button.addEventListener("click", handler2);
// Remove a specific listener (must be same reference)
button.removeEventListener("click", handler1);
// Common event types
// "click", "dblclick", "mousedown", "mouseup", "mousemove"
// "keydown", "keyup", "keypress"
// "submit", "change", "input", "focus", "blur"
// "load", "DOMContentLoaded", "resize", "scroll"
// Once option: auto-remove after first trigger
button.addEventListener("click", handler, { once: true });이벤트 위임
이벤트 위임은 이벤트 버블링을 통해 모든 자식의 이벤트를 처리하는 부모에 하나의 리스너를 연결합니다. event.target.matches(selector)로 필터링하세요. 이는 각 자식에 바인딩하는 것보다 훨씬 효율적이며 동적으로 추가된 요소를 자동으로 처리합니다. 단점: 부모는 항상 존재하는 공통 조상이어야 합니다.
// Delegate events to a parent instead of each child
// Efficient for dynamically added elements
const list = document.querySelector("#item-list");
list.addEventListener("click", (event) => {
// event.target is the actual element clicked
if (event.target.matches("li.item")) {
console.log("Clicked:", event.target.textContent);
event.target.classList.toggle("selected");
}
});
// Now dynamically added items automatically work
list.insertAdjacentHTML("beforeend", "<li class='item'>New Item</li>");
// Benefits:
// 1. One listener instead of many (memory efficient)
// 2. Works for elements added after binding
// 3. No need to re-bind when DOM changes이벤트 전파 (버블링 & 캡처링)
이벤트는 세 단계로 전파됩니다: 캡처링(위에서 아래로), 타겟, 버블링(아래에서 위로, 기본값). 대부분의 핸들러는 버블링 단계에서 실행됩니다. stopPropagation()은 이벤트가 부모 요소에 도달하는 것을 방지하고; stopImmediatePropagation()은 같은 요소의 다른 핸들러도 중지합니다. 자식 핸들러 전에 실행되어야 하는 핸들러에는 캡처링(세 번째 인수 true)을 사용하세요.
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
// Events flow in three phases:
// 1. Capturing: top -> target (window -> document -> ... -> target)
// 2. Target: at the target element
// 3. Bubbling: target -> top (default phase most handlers run in)
// Bubbling (default): child fires first, then parents
btn.addEventListener("click", () => console.log("button"));
inner.addEventListener("click", () => console.log("inner"));
outer.addEventListener("click", () => console.log("outer"));
// Click button logs: button -> inner -> outer
// stopPropagation: prevent bubbling to parents
btn.addEventListener("click", (e) => {
e.stopPropagation();
console.log("only button");
});
// Capturing phase (third arg = true)
outer.addEventListener("click", handler, true); // runs during capture
// stopImmediatePropagation: stop other handlers on SAME element too사용자 정의 이벤트
CustomEvent를 사용하면 'detail' 속성에 페이로드 데이터가 있는 응용 프로그램별 이벤트를 만들 수 있습니다. dispatchEvent와 결합하여 컴포넌트를 분리하는 pub/sub 패턴을 가능하게 합니다 — 모듈이 직접 참조 없이 통신합니다. 충돌을 피하기 위해 'namespace:action' 같은 명명 규칙을 사용하세요. 이것이 많은 사용자 정의 요소 프레임워크의 기초입니다.
// Create and dispatch custom events
const event = new CustomEvent("userLoggedIn", {
detail: { userId: 42, name: "Alice" },
bubbles: true, // allow bubbling
});
document.dispatchEvent(event);
// Listen for the custom event
document.addEventListener("userLoggedIn", (e) => {
console.log("User logged in:", e.detail.userId, e.detail.name);
});
// Practical: decoupled communication between components
class Cart {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
window.dispatchEvent(new CustomEvent("cart:updated", {
detail: { count: this.items.length }
}));
}
}
// Any module can listen without knowing about Cart internals
window.addEventListener("cart:updated", (e) => {
updateBadge(e.detail.count);
});키보드 & 폼 이벤트
키보드 이벤트는 e.key(논리적 키 like 'a', 'Enter')와 e.code(물리적 키 like 'KeyA')를 제공합니다. 대부분의 로직에 e.key를 사용하세요. 폼 제출은 페이지 재로드를 막기 위해 항상 preventDefault()이 필요합니다. FormData + Object.fromEntries로 폼 데이터를 쉽게 수집하세요. 'input'은 연속 발생; 'change'는 필드가 포커스를 잃을 때 발생 — 검증 시점에 따라 선택하세요.
// Keyboard events
document.addEventListener("keydown", (e) => {
console.log(e.key, e.code); // e.key="a", e.code="KeyA"
if (e.key === "Escape") closeModal();
if (e.ctrlKey && e.key === "s") { e.preventDefault(); save(); }
});
// Form events
const form = document.querySelector("#myForm");
form.addEventListener("submit", (e) => {
e.preventDefault(); // stop page reload
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log(data); // { username: "...", email: "..." }
});
// Input validation on change/blur
const emailInput = document.querySelector("#email");
emailInput.addEventListener("blur", () => {
if (!emailInput.value.includes("@")) {
emailInput.setCustomValidity("Enter a valid email");
} else {
emailInput.setCustomValidity("");
}
});
// Change vs input:
// 'input' fires on every keystroke; 'change' fires on blurFetch API & AJAX
기본 fetch (GET)
fetch()는 XMLHttpRequest의 현대적 대체입니다 — promise 기반이고 더 깔끔합니다. 결정적으로, fetch는 네트워크 에러에서만 거부되고 HTTP 에러 상태(404, 500)에서는 거부되지 않습니다. 파싱 전에 항상 response.ok(상태 200-299)를 확인하세요. 읽기 쉬운 순차 코드를 위해 async/await를 사용하세요. response.json()은 body 스트림을 읽으므로 async입니다.
// fetch() returns a Promise; .json() returns another Promise
fetch("https://api.example.com/users")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json(); // or .text(), .blob(), .arrayBuffer()
})
.then(data => console.log(data))
.catch(error => console.error("Fetch failed:", error));
// fetch does NOT reject on HTTP errors (404, 500) — only on network
// failures. You MUST check response.ok manually.
// Same with async/await (preferred)
async function getUsers() {
try {
const res = await fetch("https://api.example.com/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(err);
}
}POST, PUT, DELETE with fetch
fetch 옵션 객체는 method, headers, body를 구성합니다. JSON의 경우 Content-Type: application/json을 설정하고 body를 JSON.stringify하세요. 파일 업로드의 경우 FormData를 사용하세요 (Content-Type을 수동으로 설정하지 마세요 — 브라우저가 multipart 경계를 추가). PUT은 리소스를 완전히 교체; PATCH는 부분 업데이트합니다.
// POST: create a resource
async function createUser(data) {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
}
createUser({ name: "Alice", email: "[email protected]" });
// PUT: update a resource (full replace)
await fetch("/api/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice Updated" }),
});
// DELETE
await fetch("/api/users/1", { method: "DELETE" });
// Sending form data (file uploads)
const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch("/upload", { method: "POST", body: formData });
// Don't set Content-Type for FormData — browser sets it with boundary요청 헤더 & 인증
헤더는 메타데이터와 인증 토큰을 전달합니다. Bearer 토큰(JWT)은 Authorization 헤더에 들어갑니다. 일부 헤더는 Host와 Cookie처럼 '금지'(브라우저 제어)입니다. CORS는 브라우저가 아닌 서버가 시행합니다 — 클라이언트 JS에서 우회할 수 없습니다; 서버가 Access-Control-Allow-Origin을 보내야 합니다. 비단순 요청에 대해 사전 OPTIONS 요청이 발생합니다.
// Custom headers (e.g., Bearer token auth)
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem("authToken");
const res = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"Accept": "application/json",
...options.headers, // allow overrides
},
});
return res;
}
// Common headers:
// Content-Type: application/json | application/x-www-form-urlencoded
// Authorization: Bearer <token> | Basic <base64>
// Accept: application/json
// X-Requested-With: XMLHttpRequest (anti-CSRF)
// CORS: browser blocks cross-origin requests unless the server
// returns Access-Control-Allow-Origin. fetch can't bypass CORS.AbortController (요청 취소)
AbortController는 fetch 요청을 취소합니다 — 타이핑 시 검색, 탐색 벗어남, 타임아웃에 필수적입니다. signal을 fetch에 전달하고; controller.abort() 호출은 AbortError를 트리거합니다. 이것이 없으면 오래된 요청이 UI를 순서 없이 업데이트할 수 있습니다. AbortController는 다른 async API와도 작동하며 현대 JS의 표준 취소 메커니즘입니다.
// AbortController lets you cancel in-flight fetch requests
const controller = new AbortController();
async function fetchWithTimeout(url, ms = 5000) {
const timeout = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} catch (err) {
if (err.name === "AbortError") {
console.log("Request was aborted (timeout or cancel)");
} else {
throw err;
}
} finally {
clearTimeout(timeout);
}
}
// Cancel manually (e.g., user navigated away)
// controller.abort();
// Practical: cancel on new search input
searchInput.addEventListener("input", (e) => {
controller.abort(); // cancel previous request
fetchResults(e.target.value);
});스트리밍 응답
response.body는 ReadableStream입니다 — 전체 응답을 메모리에 버퍼링하는 대신 도착하는 대로 청크로 데이터를 처리할 수 있습니다. 큰 파일, 스트리밍 로그, 실시간 데이터에 필수적입니다. 텍스트 스트림에는 TextDecoder를 사용하세요. reader.read() 루프는 done이 true가 될 때까지 계속됩니다. 스트리밍은 큰 페이로드에서 메모리 급증을 방지합니다.
// Read a large response in chunks (streaming)
async function streamJson(url) {
const res = await fetch(url);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete lines
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line
for (const line of lines) {
if (line.trim()) console.log(JSON.parse(line));
}
}
}
// Useful for: large files, NDJSON logs, real-time data feeds
// Processes data as it arrives instead of waiting for the full response웹 저장소 (LocalStorage & SessionStorage)
localStorage & sessionStorage 기본
localStorage는 무기한 지속되고; sessionStorage는 탭이 닫힐 때 지워집니다. 둘 다 문자열만 저장합니다 — 객체에는 JSON.stringify/parse를 사용하세요. 저장소는 동기식이고 메인 스레드를 차단하므로 큰 데이터 저장을 피하세요. 모든 현대 브라우저에서 사용 가능하지만 시크릿 모드에서는 비활성화될 수 있습니다. 용량은 출처당 ~5-10MB입니다.
// Both store strings only — JSON.stringify objects
// localStorage: persists until cleared (survives browser restart)
// sessionStorage: cleared when the tab closes
// localStorage API
localStorage.setItem("username", "Alice");
const name = localStorage.getItem("username"); // "Alice"
localStorage.removeItem("username");
localStorage.clear(); // remove ALL items
// sessionStorage API (same interface, tab-scoped)
sessionStorage.setItem("tempToken", "abc123");
// Storing objects (must serialize)
const prefs = { theme: "dark", lang: "en" };
localStorage.setItem("prefs", JSON.stringify(prefs));
const loaded = JSON.parse(localStorage.getItem("prefs"));
console.log(loaded.theme); // "dark"
// Check if storage exists (private mode may disable it)
if (typeof Storage !== "undefined") {
// localStorage/sessionStorage available
}TTL & JSON이 있는 저장소 헬퍼
웹 저장소에는 내장 만료가 없습니다 — 이 래퍼는 값과 함께 만료 타임스탬프를 저장하여 TTL(생존 시간)을 추가합니다. 이것은 API 응답이나 만료되어야 하는 세션 데이터를 캐싱하는 표준 패턴입니다. JSON.parse가 손상된 데이터에서 throw할 수 있고 할당량이 초과될 수 있으므로 프로덕션에서는 항상 저장소 접근을 try/catch로 감싸세요.
// A robust storage wrapper with expiration (TTL)
const store = {
set(key, value, ttlMs) {
const item = {
value: value,
expiry: ttlMs ? Date.now() + ttlMs : null,
};
localStorage.setItem(key, JSON.stringify(item));
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (item.expiry && Date.now() > item.expiry) {
localStorage.removeItem(key);
return null; // expired
}
return item.value;
},
remove(key) { localStorage.removeItem(key); },
};
// Usage: cache data for 5 minutes
store.set("userData", { name: "Alice" }, 5 * 60 * 1000);
console.log(store.get("userData")); // { name: "Alice" }
// After 5 min: returns null and cleans up저장소 이벤트 (크로스 탭 동기화)
storage 이벤트는 내장 크로스 탭 통신 채널입니다 — 한 탭이 localStorage를 수정하면 같은 출처의 다른 모든 탭이 이벤트를 받습니다 (시작 탭 제외). 이를 통해 WebSocket 없이 탭 간에 로그인/로그아웃, 장바구니 업데이트, 테마 변경 같은 상태를 동기화할 수 있습니다. 이벤트는 key, oldValue, newValue, url을 포함합니다.
// The 'storage' event fires in OTHER tabs when storage changes
// (not in the tab that made the change)
window.addEventListener("storage", (event) => {
console.log("Key changed:", event.key);
console.log("Old value:", event.oldValue);
console.log("New value:", event.newValue);
console.log("URL:", event.url);
if (event.key === "cart") {
updateCartDisplay(JSON.parse(event.newValue));
}
});
// Practical: sync logout across tabs
// Tab A: localStorage.setItem("logout", Date.now());
// Tab B: receives storage event -> redirects to login
// Practical: broadcast a message to all tabs
function broadcast(type, data) {
localStorage.setItem("broadcast", JSON.stringify({ type, data, t: Date.now() }));
}
window.addEventListener("storage", (e) => {
if (e.key === "broadcast") {
const msg = JSON.parse(e.newValue);
handleMessage(msg);
}
});IndexedDB (대용량 구조화 저장소)
IndexedDB는 브라우저의 강력한 NoSQL 데이터베이스입니다 — 비동기, 트랜잭셔널하며 localStorage보다 훨씬 더 많은 데이터(수백 MB)를 저장할 수 있습니다. 인덱스, 커서 및 트랜잭션을 지원합니다. 원시 API는 콜백 기반이고 장황합니다; 'idb' npm 패키지는 깔끔한 Promise 기반 래퍼를 제공합니다. 오프라인 우선 앱, 대규모 캐시 또는 복잡한 클라이언트 측 데이터에 IndexedDB를 사용하세요.
// IndexedDB: async, transactional NoSQL store for large data
// Capacity: hundreds of MB to GB (far more than localStorage's 5MB)
// Open a database
const request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
// Create an object store (like a table) with a key path
if (!db.objectStoreNames.contains("users")) {
db.createObjectStore("users", { keyPath: "id" });
}
};
request.onsuccess = (e) => {
const db = e.target.result;
// Add data in a transaction
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
store.add({ id: 1, name: "Alice", age: 30 });
store.add({ id: 2, name: "Bob", age: 25 });
// Query
store.get(1).onsuccess = (e) => {
console.log(e.target.result); // { id: 1, name: "Alice", age: 30 }
};
};
// For easier use, consider the 'idb' library (Promise-based wrapper)쿠키 vs 저장소 비교
쿠키는 모든 HTTP 요청과 함께 전송되어(대역폭 추가) 서버 측 인증(세션 ID, CSRF 토큰)의 표준입니다. localStorage/sessionStorage는 클라이언트 전용이며 훨씬 더 많은 데이터를 보유합니다. IndexedDB는 대규모 구조화 데이터용입니다. 다음에 따라 선택하세요: 서버가 필요한가? (쿠키) 데이터 양은? (저장소 vs IndexedDB) 기간은? (세션 vs 로컬). 보안에 민감한 쿠키에는 Secure, HttpOnly, SameSite를 설정하세요.
// COOKIES: sent with every HTTP request, ~4KB limit
document.cookie = "session=abc123; max-age=3600; path=/; Secure; SameSite=Strict";
console.log(document.cookie); // "session=abc123; theme=dark"
// Set cookie with attributes
document.cookie = "token=xyz; max-age=86400; Secure; HttpOnly; SameSite=Lax";
// Note: HttpOnly can't be set via JS (server-only for security)
// LOCAL STORAGE: ~5-10MB, NOT sent to server, synchronous
localStorage.setItem("theme", "dark");
// SESSION STORAGE: ~5MB, cleared on tab close
sessionStorage.setItem("draft", "work in progress");
// WHEN TO USE WHAT:
// Cookies -> auth tokens that the server needs to read, server-side sessions
// localStorage -> user preferences, cached data that persists
// sessionStorage -> temporary per-tab state (form drafts, wizard steps)
// IndexedDB -> large datasets, offline data, complex queries타이머(setTimeout, setInterval)
setTimeout & setInterval
setTimeout은 지연 후 콜백을 한 번 실행하고; setInterval은 반복적으로 실행합니다. 둘 다 clearTimeout/clearInterval로 취소할 ID를 반환합니다. 타이머는 정확하지 않습니다 — 이벤트 루프, 탭 가시성(백그라운드 탭에서 제한됨) 및 메인 스레드 가용성에 따른 최소 지연입니다. 중첩 타이머에서 4ms 미만의 지연은 4ms로 조정될 수 있습니다.
// setTimeout: run once after a delay (milliseconds)
const timeoutId = setTimeout(() => {
console.log("Runs after 2 seconds");
}, 2000);
// Cancel before it fires
clearTimeout(timeoutId);
// setInterval: run repeatedly every interval
const intervalId = setInterval(() => {
console.log("Runs every 1 second");
}, 1000);
// Stop the interval
clearInterval(intervalId);
// Pass arguments to the callback
setTimeout((greeting, name) => {
console.log(`${greeting}, ${name}`);
}, 1000, "Hello", "Alice");
// NOTE: timers don't guarantee exact timing — they run after the
// minimum delay, but only when the call stack is empty (event loop)재귀 setTimeout(setInterval보다 나음)
재귀 setTimeout은 반복 async 작업에 setInterval보다 선호됩니다 — 다음 시작 전에 이전 호출이 완료되도록 보장하여 겹치는 실행이 없기 때문입니다. 동적 간격(예: 오류 시 더 긴 백오프)도 허용합니다. setInterval은 핸들러가 간격보다 오래 걸리면 호출이 누적되어 성능 문제를 일으킬 수 있습니다.
// setInterval problems: doesn't wait for the previous call to finish
// If a call takes longer than the interval, they can stack up.
// Better: recursive setTimeout — guarantees the gap between completions
function poll() {
fetch("/api/status")
.then(r => r.json())
.then(data => {
console.log(data);
setTimeout(poll, 1000); // schedule next AFTER this finishes
})
.catch(err => {
console.error(err);
setTimeout(poll, 5000); // retry with backoff on error
});
}
poll(); // start the loop
// This pattern: always waits for the previous call to complete
// before scheduling the next, avoiding overlap/stacking.requestAnimationFrame(부드러운 애니메이션)
requestAnimationFrame(rAF)은 시각적 애니메이션을 수행하는 올바른 방법입니다 — 브라우저의 다시 그리기 주기(~60fps)와 동기화되고, 탭이 숨겨졌을 때 불필요한 프레임을 피하며, setInterval보다 부드러운 애니메이션을 생성합니다. 다른 새로고침 속도에서 애니메이션 속도를 일관되게 유지하기 위해 델타 타임 계산을 위해 타임스탬프 매개변수를 사용하세요. 완료 시 항상 cancelAnimationFrame으로 취소하세요.
// requestAnimationFrame: syncs with the display refresh (~60fps)
// More efficient than setInterval for visual animations
function animate() {
element.style.left = parseInt(element.style.left || 0) + 2 + "px";
if (parseInt(element.style.left) < 300) {
requestAnimationFrame(animate); // schedule next frame
}
}
requestAnimationFrame(animate);
// Cancel if needed
const rafId = requestAnimationFrame(animate);
cancelAnimationFrame(rafId);
// Timestamp for delta-time calculations
let lastTime = 0;
function loop(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
update(delta); // frame-rate independent movement
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// rAF pauses in background tabs (saves CPU/battery)디바운스 & 스로틀
디바운스는 실행 전 호출 일시 정지를 기다립니다(예: 사용자가 타이핑을 멈춘 후 300ms 후에만 검색). 스로틀은 간격당 한 번으로 실행을 제한합니다(예: 스크롤 위치를 최대 200ms마다 업데이트). 둘 다 고빈도 이벤트로 인한 성능 문제를 방지합니다. 디바운스 = '빠른 호출을 하나로 그룹화'; 스로틀 = '호출 속도 제한'.
// Debounce: delay execution until calls stop for N ms
// Use for: search input, window resize, button spam
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // reset the countdown
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => {
fetchResults(query);
}, 300);
input.addEventListener("input", (e) => search(e.target.value));
// Throttle: execute at most once per N ms
// Use for: scroll, mousemove, drag handlers
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const onScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
}, 200);
window.addEventListener("scroll", onScroll);Promise화된 setTimeout(delay)
setTimeout을 Promise로 감싸면 async/await를 위한 깔끔한 'delay' 함수가 생성됩니다 — 콜백 체인보다 훨씬 가독성이 좋습니다. 이 패턴은 지수 백오프가 있는 재시도 로직(재시도 간 1초, 2초, 4초 대기), 순차 애니메이션 및 속도 제한을 구동합니다. delay 헬퍼는 현대 async JS에서 가장 유용한 작은 유틸리티 중 하나입니다.
// A promise-based delay for use with async/await
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function countdown() {
console.log("3...");
await delay(1000);
console.log("2...");
await delay(1000);
console.log("1...");
await delay(1000);
console.log("Go!");
}
countdown();
// Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
throw new Error(`HTTP ${res.status}`);
} catch (err) {
if (i === retries - 1) throw err;
const wait = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Retry ${i + 1} in ${wait}ms`);
await delay(wait);
}
}
}Map, Set & WeakMap
Map(모든 키 타입의 키-값)
Map은 키가 모든 타입(객체, 함수, 숫자)이 될 수 있는 적절한 키-값 컬렉션입니다 — 키를 문자열로 강제 변환하는 객체와 다릅니다. Map은 삽입 순서를 보존하고, .size 속성이 있으며, 직접 반복 가능합니다. 비문자열 키, 빈번한 추가/삭제가 필요할 때 또는 컬렉션이 레코드/DTO가 아닐 때 Map을 사용하세요. 고정 모양 데이터에는 Object를 사용하세요.
// Map: like an Object but keys can be ANY type (not just strings)
const map = new Map();
// Keys can be objects, functions, primitives
const objKey = { id: 1 };
map.set("name", "Alice");
map.set(objKey, "data for this object");
map.set(42, "numeric key");
map.set(true, "boolean key");
console.log(map.get("name")); // "Alice"
console.log(map.get(objKey)); // "data for this object"
console.log(map.size); // 4
console.log(map.has(42)); // true
// Iteration preserves insertion order
for (const [key, value] of map) {
console.log(key, value);
}
// Map vs Object:
// - Map keys can be any type; Object keys are strings/symbols
// - Map has .size; Object needs Object.keys().length
// - Map is iterable by default; Object needs Object.entries()
// - Map performs better for frequent add/removeSet(고유 값)
Set은 고유 값을 저장합니다 — 중복 제거 및 멤버십 테스트에 완벽합니다. 배열을 Set으로 변환하고 다시([...new Set(arr)])하는 것이 중복 제거의 관용적인 방법입니다. Set.has()는 O(1)이고 Array.includes()는 O(n)이므로 자주 확인하는 큰 컬렉션에는 Set을 사용하세요. Set에는 map/filter가 없습니다 — 먼저 배열로 전개하세요.
// Set: collection of unique values (no duplicates)
const set = new Set([1, 2, 3, 2, 1]);
console.log(set); // Set(3) { 1, 2, 3 }
console.log(set.size); // 3
set.add(4);
set.add(4); // ignored (already exists)
console.log(set.has(3)); // true
set.delete(2);
// Iterate (preserves insertion order)
for (const item of set) {
console.log(item);
}
// Common use cases:
// 1. Remove duplicates from an array
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]
// 2. Check for membership (faster than Array.includes for large sets)
const validIds = new Set([101, 102, 103]);
if (validIds.has(userId)) { /* ... */ }
// 3. Set operations (union, intersection)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union = new Set([...a, ...b]); // {1,2,3,4}
const intersect = new Set([...a].filter(x => b.has(x))); // {2,3}WeakMap & WeakSet(메모리 안전 참조)
WeakMap과 WeakSet은 객체에 대한 약한 참조를 보유합니다 — 다른 참조가 없으면 항목이 자동으로 가비지 컬렉션됩니다. 이것은 DOM 요소나 기타 객체와 데이터를 연결할 때 메모리 누수를 방지합니다. 키는 객체여야 합니다. WeakMap/WeakSet은 반복 불가능하며 GC 중 언제든 항목이 사라질 수 있으므로 .size가 없습니다. 객체 수명과 연결된 캐싱/메타데이터에 사용하세요.
// WeakMap: keys must be objects; references are "weak"
// (doesn't prevent garbage collection of the key object)
const weakMap = new WeakMap();
let user = { name: "Alice" };
weakMap.set(user, "metadata");
console.log(weakMap.get(user)); // "metadata"
user = null; // remove the only strong reference
// Now the object can be GC'd, and WeakMap entry disappears automatically
// Practical: attach data to DOM elements without memory leaks
const elementData = new WeakMap();
function cacheData(element, data) {
elementData.set(element, data);
}
// When the DOM element is removed, its data is auto-cleaned
// WeakSet: collection of objects, weakly held
const processed = new WeakSet();
function processOnce(obj) {
if (processed.has(obj)) return; // already done
processed.add(obj);
doWork(obj);
}
// WeakMap/WeakSet are NOT iterable (no .size, no for...of)Map 이터레이션 & 변환
Map은 for...of(기본적으로 entries), .keys(), .values() 또는 .forEach()로 삽입 순서대로 반복 가능합니다. Object.entries()와 Object.fromEntries()를 사용하여 Map과 Object 간에 변환하세요. 이 양방향 변환은 API가 일반 객체를 기대하지만 내부적으로 Map의 기능을 원할 때 편리합니다. 주의: 변환에서 Object 키는 문자열 타입이 됩니다.
const map = new Map([
["name", "Alice"],
["age", 30],
["city", "NYC"],
]);
// Iterate entries (default)
for (const [key, value] of map) {
console.log(`${key} = ${value}`);
}
// Iterate keys only
for (const key of map.keys()) {
console.log(key);
}
// Iterate values only
for (const value of map.values()) {
console.log(value);
}
// forEach (callback style)
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// Convert to Array
const entries = [...map.entries()]; // [["name","Alice"], ["age",30], ...]
const keys = [...map.keys()]; // ["name", "age", "city"]
const values = [...map.values()]; // ["Alice", 30, "NYC"]
// Convert Object to Map and back
const obj = { a: 1, b: 2 };
const mapFromObj = new Map(Object.entries(obj));
const objFromMap = Object.fromEntries(map);Map, Set, Object & Array 중 선택
올바른 컬렉션을 선택하세요: 중복 및 인덱스 접근이 있는 순서 리스트는 배열; 고정 모양 레코드 및 JSON은 객체; 모든 키 타입의 동적 키-값 컬렉션은 Map; 고유성 및 빠른 조회는 Set. 잘못된 구조를 사용하면 장황한 코드와 성능 문제가 발생합니다 — 예: 루프에서 Array.includes() vs Set.has().
// Decision guide:
// Use ARRAY when:
// - You need ordered data with duplicates
// - You need index-based access (arr[5])
// - You'll map/filter/reduce frequently
const todoList = ["buy milk", "walk dog"];
// Use OBJECT when:
// - You have a fixed known shape (like a user record)
// - You need JSON serialization (JSON.stringify)
// - Keys are strings and known at write time
const user = { name: "Alice", age: 30 };
// Use MAP when:
// - Keys are not strings (objects, functions, numbers)
// - You frequently add/remove key-value pairs
// - You need to iterate in insertion order
// - The collection size changes dynamically
const handlers = new Map();
handlers.set(buttonElement, () => onClick());
// Use SET when:
// - You need unique values (no duplicates)
// - You need fast membership testing (.has is O(1))
// - Order doesn't matter much
const seen = new Set();
if (!seen.has(url)) { seen.add(url); visit(url); }제너레이터 & 이터레이터
이터레이터 & Symbol.iterator
이터레이터는 {value, done}을 반환하는 next() 메서드를 가집니다. 이터러블은 이터레이터를 반환하는 Symbol.iterator를 구현합니다. 내장 이터러블(배열, 문자열, Map, Set)은 for...of, 전개(...), 구조 분해 및 Array.from()과 작동합니다. Symbol.iterator를 구현하면 사용자 정의 객체가 이러한 모든 언어 기능과 원활하게 작동합니다.
// An iterator is an object with a next() method returning {value, done}
function makeIterator(arr) {
let i = 0;
return {
next() {
return i < arr.length
? { value: arr[i++], done: false }
: { value: undefined, done: true };
},
};
}
const it = makeIterator(["a", "b", "c"]);
console.log(it.next()); // { value: "a", done: false }
console.log(it.next()); // { value: "b", done: false }
console.log(it.next()); // { value: "c", done: false }
console.log(it.next()); // { value: undefined, done: true }
// An iterable implements Symbol.iterator (returns an iterator)
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
},
};
},
};
// Now range works with for...of, spread, etc.
console.log([...range]); // [1, 2, 3]제너레이터 함수(function*)
제너레이터 함수(function*)는 yield를 사용하여 실행을 일시 정지하고 재개합니다 — 값을 지연 평가로 한 번에 하나씩 생성합니다. 이는 무한 시퀀스, 지연 평가 및 메모리 효율적인 파이프라인을 가능하게 합니다. 제너레이터는 이터레이터이자 이터러블입니다. next()를 호출할 때마다 다음 yield(또는 return)까지 실행됩니다. async 제너레이터와 JS 코루틴 패턴의 기초입니다.
// Generators: pause execution with 'yield', resume with next()
function* idGenerator() {
let id = 1;
while (true) {
yield id++; // pause here, return value, resume on next()
}
}
const gen = idGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
// Infinite sequence — only computes values on demand
// Generators are iterables (work with for...of)
function* take(n, iterable) {
for (const item of iterable) {
if (n-- <= 0) return;
yield item;
}
}
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
const firstFive = [...take(5, naturalNumbers())];
console.log(firstFive); // [1, 2, 3, 4, 5]yield*(다른 제너레이터에 위임)
yield*는 모든 yield를 다른 이터러블이나 제너레이터에 위임합니다 — 중첩된 구조를 평면화하고 제너레이터를 깔끔하게 컴포지션합니다. Python의 'yield from'의 JS 동등물입니다. 위임된 제너레이터의 값은 외부 제너레이터의 일부인 것처럼 하나씩 yield됩니다. 이것이 제너레이터를 재귀적으로 컴포지션하는 표준 방법입니다.
// yield* delegates to another iterable/generator
function* inner() {
yield "a";
yield "b";
}
function* outer() {
yield 1;
yield* inner(); // delegates: yields "a", then "b"
yield* [10, 20]; // also works with arrays
yield 2;
}
console.log([...outer()]); // [1, "a", "b", 10, 20, 2]
// Practical: flatten nested structures
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // recursive delegation
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4]], 5];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5]next(value)를 통한 제너레이터 send()
제너레이터는 양방향 통신을 지원합니다: next(value)는 값을 전달하고(마지막 yield 표현식의 결과가 됨), throw(error)는 yield 지점에서 예외를 주입합니다. 이를 통해 코루틴, 상태 머신 및 초기 async/await 구현을 구동한 제너레이터 기반 async 패턴이 가능해집니다. 첫 번째 next() 호출은 값을 전달할 수 없습니다(받을 것이 아직 없음).
// next(value) passes a value INTO the generator (becomes yield's result)
function* conversation() {
const name = yield "What's your name?"; // receives via next()
const age = yield `Hello ${name}, how old are you?`;
return `${name} is ${age} years old`;
}
const talk = conversation();
console.log(talk.next().value); // "What's your name?"
console.log(talk.next("Alice").value); // "Hello Alice, how old are you?"
console.log(talk.next(30).value); // "Alice is 30 years old"
// throw(error) injects an exception at the yield point
function* safeGen() {
try {
yield "step 1";
} catch (e) {
console.log("Caught:", e.message);
yield "recovered";
}
}
const g = safeGen();
g.next(); // "step 1"
g.throw(new Error("oops")); // "Caught: oops" -> "recovered"Async 제너레이터
Async 제너레이터(async function*)는 제너레이터와 async/await를 결합합니다 — 각 yield는 await 후에 값을 생성할 수 있습니다. for await...of로 소비하세요. 이것은 페이지 매김 API, 스트리밍 데이터 또는 값을 비동기적으로 생성하는 모든 시나리오에 이상적입니다. 콜백이나 수동 promise 체이닝 없이 JS에서 스트리밍 데이터를 처리하는 현대적 방법입니다.
// async function* : yields promises, can use await
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) break; // no more pages
yield data.items;
page++;
}
}
// Consume with for await...of
(async () => {
for await (const items of fetchPages("/api/products")) {
console.log(`Got ${items.length} items`);
renderItems(items);
}
console.log("All pages loaded");
})();
// Practical: stream data as it arrives
async function* streamLines(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) yield line;
}
}Proxy & Reflect
Proxy 기본(작업 가로채기)
Proxy를 사용하면 객체에 대한 기본 작업을 가로채고 사용자 정의할 수 있습니다 — get, set, has, deleteProperty, ownKeys 등. 핸들러 객체는 '트랩'을 정의합니다(모든 속성에 대한 getter/setter 같은). 사용 사례: 검증, 로깅, 기본값, 접근 제어, 반응형 시스템(Vue 3은 반응성에 Proxy 사용). 엄격 모드에서 성공을 나타내려면 set에서 true를 반환하세요.
// Proxy wraps an object and intercepts fundamental operations
const target = { name: "Alice", age: 30 };
const handler = {
get(obj, prop) {
console.log(`Reading ${prop}`);
return prop in obj ? obj[prop] : "N/A"; // default value
},
set(obj, prop, value) {
console.log(`Setting ${prop} = ${value}`);
if (prop === "age" && value < 0) return false; // validation
obj[prop] = value;
return true; // indicate success
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // logs "Reading name", returns "Alice"
proxy.age = 25; // logs "Setting age = 25"
// proxy.age = -5; // logs "Setting age = -5", rejected
console.log(proxy.unknown); // logs "Reading unknown", returns "N/A"일반적인 Proxy 사용 사례
Proxy는 강력한 메타프로그래밍을 가능하게 합니다: 음수 인덱스, 지연 속성 계산, 읽기 전용 뷰, 검증, 로깅 및 반응형 데이터 바인딩. Vue 3의 반응성 시스템은 속성 접근을 추적하고 자동으로 다시 렌더링을 트리거하기 위해 Proxy를 사용합니다. 단점은 약간의 성능 오버헤드이므로 추상화 가치가 있는 곳에 Proxy를 사용하고 모든 객체에 사용하지 마세요.
// 1. Negative array indices (like Python)
function negativeArray(arr) {
return new Proxy(arr, {
get(target, prop) {
const idx = Number(prop);
if (Number.isInteger(idx)) {
return target[idx < 0 ? target.length + idx : idx];
}
return target[prop];
},
});
}
const arr = negativeArray(["a", "b", "c"]);
console.log(arr[-1]); // "c"
// 2. Auto-populating / lazy properties
const lazy = new Proxy({}, {
get(target, prop) {
if (!(prop in target)) {
target[prop] = expensiveCompute(prop); // compute on first access
}
return target[prop];
},
});
// 3. Read-only object
const readOnly = new Proxy(data, {
set() { throw new Error("This object is read-only"); },
deleteProperty() { throw new Error("Cannot delete"); },
});Reflect API
Reflect는 Proxy 트랩이 가로채는 것과 동일한 작업을 제공합니다 — 트랩 내에서 기본 동작으로 전달하기 위해 사용하세요. Reflect 메서드는 throw 대신 boolean(성공/실패)을 반환하여 조건부 로직에 더 안전합니다. Reflect.ownKeys는 열거 가능한 문자열 키만 반환하는 Object.keys와 달리 모든 키(문자열 및 심볼)를 반환합니다. 함께 Proxy와 Reflect는 JS의 메타프로그래밍 툴킷을 형성합니다.
// Reflect provides default behavior for proxy traps and
// functional equivalents of object operations
const obj = { x: 1, y: 2 };
// Reflect.get / Reflect.set (instead of obj[prop])
console.log(Reflect.get(obj, "x")); // 1
Reflect.set(obj, "z", 3); // obj.z = 3
console.log(Reflect.has(obj, "x")); // true (like "x" in obj)
Reflect.deleteProperty(obj, "y"); // delete obj.y
console.log(Reflect.ownKeys(obj)); // ["x", "z"]
// In a Proxy, use Reflect to forward to the default behavior
const proxy = new Proxy({}, {
get(target, prop, receiver) {
console.log(`get ${prop}`);
return Reflect.get(target, prop, receiver); // default behavior
},
set(target, prop, value, receiver) {
console.log(`set ${prop}`);
return Reflect.set(target, prop, value, receiver);
},
});
// Reflect.construct: call a constructor with an array of args
const instance = Reflect.construct(Array, [1, 2, 3]);Proxy가 있는 반응형 객체(Vue 스타일)
이것이 Vue 3 반응성의 핵심 아이디어입니다 — Proxy가 get(어느 이펙트가 속성에 의존하는지 추적)과 set(속성이 변경될 때 해당 이펙트를 트리거)을 가로챕니다. 이를 통해 선언적 UI 업데이트가 가능해집니다: 상태를 변형하면 프레임워크가 자동으로 다시 렌더링합니다. Proxy는 ES6에서 이것을 가능하게 했습니다; 이전 프레임워크(Vue 2)는 제한이 있는 Object.defineProperty를 사용했습니다.
// A minimal reactive system using Proxy
function reactive(target) {
const subscribers = new Set();
const handler = {
get(obj, prop) {
track(prop); // record who reads this property
const value = Reflect.get(obj, prop);
return typeof value === "object" && value !== null
? reactive(value) // deeply reactive
: value;
},
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
trigger(prop); // notify subscribers
return result;
},
};
let currentEffect = null;
function track(prop) {
if (currentEffect) subscribers.add(currentEffect);
}
function trigger(prop) {
subscribers.forEach(fn => fn());
}
return new Proxy(target, handler);
}
const state = reactive({ count: 0 });
// When state.count changes, effects re-run automaticallyWeb Workers
Web Worker 생성
Web Workers는 JavaScript를 별도의 백그라운드 스레드에서 실행하여 UI를 차단하지 않고 진정한 병렬성을 가능하게 합니다. 메인 스레드와 워커는 postMessage로 통신합니다(데이터는 공유가 아닌 복사/구조화 복제됨). 워커는 DOM이나 window 객체에 접근할 수 없습니다 — 격리되어 있습니다. 이미지 처리, 큰 파일 파싱 또는 복잡한 계산과 같은 CPU 집약적 작업에 워커를 사용하세요.
// main.js — workers run JS in a background thread
const worker = new Worker("worker.js");
// Send data to the worker
worker.postMessage({ command: "calculate", data: [1, 2, 3, 4, 5] });
// Receive results from the worker
worker.onmessage = (event) => {
console.log("Result from worker:", event.data);
};
worker.onerror = (error) => {
console.error("Worker error:", error.message);
};
// Terminate when done (frees resources)
// worker.terminate();
// --- worker.js (separate file) ---
// self.onmessage = (event) => {
// const { command, data } = event.data;
// const result = heavyComputation(data);
// self.postMessage(result);
// };인라인 워커(Blob URL)
인라인 워커는 Blob URL을 통해 코드 문자열에서 Worker를 만듭니다 — 별도의 파일이 필요 없습니다. 데모, 작은 유틸리티 또는 빌드 시스템이 별도의 워커 파일을 쉽게 처리하지 못할 때 편리합니다. 메모리 누수를 방지하려면 객체 URL을 취소하세요. 워커 코드는 문자열이므로 편집기 구문 강조 및 타입 검사를 잃습니다 — 프로덕션에서 주의해서 사용하세요.
// Create a worker from a string (no separate file needed)
const workerCode = `
self.onmessage = function(e) {
const result = e.data.map(x => x * x);
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage([1, 2, 3, 4]);
worker.onmessage = (e) => console.log(e.data); // [1, 4, 9, 16]
// Clean up the blob URL when done
// URL.revokeObjectURL(blob URL);
// Useful for: single-file demos, bundlers that inline workers,
// or when you can't serve a separate .js file전송 가능 객체(제로 카피)
일반적으로 postMessage는 데이터를 복사(구조화 복제)하여 큰 버퍼에 느립니다. 전송 목록(두 번째 인수)은 ArrayBuffer/MessagePort/ImageBitmap의 소유권을 복사 없이 워커로 이동합니다 — 크기에 관계없이 거의 즉각적입니다. 원래 버퍼는 분리됩니다(사용 불가). 큰 데이터셋, 이미지 처리 또는 오디오의 복사 오버헤드를 피하기 위해 이것을 사용하세요.
// Transferable objects move data to a worker WITHOUT copying
// (the original becomes unusable) — much faster for large ArrayBuffers
// Create a large buffer
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10MB
const view = new Float64Array(buffer);
view[0] = 3.14;
// Transfer the buffer (second arg = transfer list)
worker.postMessage({ data: buffer }, [buffer]);
// After transfer, 'buffer' is detached (length becomes 0)
console.log(buffer.byteLength); // 0 — ownership moved to worker
// Worker receives it normally:
// self.onmessage = (e) => {
// const buf = e.data.data; // full 10MB, no copy
// };
// Transferable types: ArrayBuffer, MessagePort, ImageBitmap
// Use for: large datasets, image data, audio buffersSharedArrayBuffer & Atomics
SharedArrayBuffer는 스레드 간에 진정한 공유 메모리를 허용하고(복사 없음), Atomics는 그 위에 스레드 안전 작업(add, load, store, compareExchange, wait/notify)을 제공합니다. 이를 통해 JS에서 고성능 병렬 알고리즘이 가능해집니다. Spectre 보안 우려로 인해 SharedArrayBuffer는 교차 출처 격리 HTTP 헤더가 필요합니다 — 없으면 최신 브라우저에서 비활성화됩니다. WASM 상호 운용 및 무거운 병렬 계산에 사용하세요.
// SharedArrayBuffer: memory shared between main thread and workers
// (both can read/write simultaneously) — true shared memory
// const sharedBuffer = new SharedArrayBuffer(1024);
// const view = new Int32Array(sharedBuffer);
// worker.postMessage({ buffer: sharedBuffer });
// Atomics: thread-safe operations on SharedArrayBuffer
// const sharedArray = new Int32Array(sharedBuffer);
// Atomics.add(sharedArray, 0, 1); // atomic increment
// Atomics.load(sharedArray, 0); // atomic read
// Atomics.store(sharedArray, 0, 42); // atomic write
// Atomics.compareExchange(sharedArray, 0, 42, 99); // CAS
// Atomics.wait / notify: block and wake threads
// Worker:
// Atomics.wait(sharedArray, 0, 0); // block until index 0 != 0
// Main:
// Atomics.store(sharedArray, 0, 1);
// Atomics.notify(sharedArray, 0); // wake waiting workers
// NOTE: Requires cross-origin isolation headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp워커 풀 패턴
워커 생성에는 오버헤드가 있으므로 워커 풀은 고정된 수의 워커를 재사용하고 작업을 큐에 넣습니다 — 다른 언어의 스레드 풀처럼. 이 패턴은 태스크당 워커 생성 비용을 피하면서 CPU 활용도를 최대화합니다(코어당 하나의 워커). 풀은 사용 가능한 워커에게 작업을 분배하고 모두 사용 중일 때 큐에 넣습니다. 독립적인 작업 청크를 많이 처리하는 데 필수적입니다.
// A worker pool reuses workers to avoid creation overhead
class WorkerPool {
constructor(workerUrl, size = 4) {
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
const worker = new Worker(workerUrl);
worker.busy = false;
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const worker = this.workers.find(w => !w.busy);
if (worker) {
this.execute(worker, task);
} else {
this.queue.push(task); // wait for a free worker
}
});
}
execute(worker, task) {
worker.busy = true;
worker.onmessage = (e) => {
worker.busy = false;
task.resolve(e.data);
this.next();
};
worker.postMessage(task.data);
}
next() {
const task = this.queue.shift();
const worker = this.workers.find(w => !w.busy);
if (task && worker) this.execute(worker, task);
}
}DOM 조작 심층
querySelector
querySelector는 첫 번째 일치를 반환하고, querySelectorAll은 정적 NodeList를 반환합니다. getElementsByClassName은 라이브 HTMLCollection을 반환합니다. NodeList는 forEach를 지원하고; HTMLCollection은 지원하지 않습니다.
const el = document.querySelector('.my-class');
const all = document.querySelectorAll('div.item');
all.forEach(el => console.log(el.textContent));
const live = document.getElementsByClassName('item'); // live
const static = document.querySelectorAll('.item'); // static생성 & 삽입
createElement는 새 요소를 만듭니다. appendChild는 마지막 자식으로 추가하고, prepend는 첫 번째로 추가합니다. textContent는 XSS를 방지하므로 innerHTML보다 안전합니다.
const div = document.createElement('div');
div.className = 'card';
div.textContent = 'Hello';
div.setAttribute('data-id', '42');
const parent = document.querySelector('#container');
parent.appendChild(div);
parent.prepend(div);이벤트 위임
이벤트 위임은 자식마다가 아닌 부모에 하나의 리스너를 연결합니다. closest는 선택자와 일치하는 가장 가까운 조상을 찾습니다. 동적으로 추가된 요소를 처리합니다.
document.querySelector('#list').addEventListener('click', (e) => {
const item = e.target.closest('.list-item');
if (!item) return;
console.log('Clicked:', item.dataset.id);
});classList API
classList는 CSS 클래스를 안전하게 조작하는 메서드를 제공합니다. toggle은 추가 시 true, 제거 시 false를 반환합니다. className 문자열 조작보다 깔끔합니다.
const el = document.querySelector('.box');
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('dark-mode');
el.classList.replace('old', 'new');
if (el.classList.contains('active')) { /* ... */ }Dataset 속성
data-* 속성은 사용자 정의 데이터를 저장합니다. dataset은 camelCase 접근을 제공합니다: data-user-id는 dataset.userId가 됩니다. 값은 항상 문자열입니다.
// HTML: <div data-user-id="42" data-role="admin"></div>
const el = document.querySelector('[data-user-id]');
console.log(el.dataset.userId); // "42"
console.log(el.dataset.role); // "admin"
el.dataset.userId = '99';Canvas API
기본 그리기
getContext("2d")는 2D 렌더링 컨텍스트를 반환합니다. fillRect는 채워진 사각형을 그리고, strokeRect는 윤곽선을 그립니다. 그리기 전에 fillStyle/strokeStyle을 설정하세요.
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.strokeRect(130, 10, 100, 50);경로 & 선
moveTo는 그리지 않고 커서를 이동합니다. lineTo는 선을 그립니다. closePath는 시작점으로 다시 연결합니다. arc는 원/호를 그립니다.
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(100, 150);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.arc(250, 35, 25, 0, Math.PI * 2);
ctx.fill();텍스트 & 그라데이션
createLinearGradient는 그라데이션을 만듭니다. addColorStop은 위치(0-1)에서 색상을 정의합니다. 텍스트 그리기 전에 font 및 textAlign을 설정하세요.
const grad = ctx.createLinearGradient(0, 0, 200, 0);
grad.addColorStop(0, 'red');
grad.addColorStop(1, 'blue');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 200, 100);
ctx.font = '24px Arial';
ctx.fillText('Hello Canvas', 100, 50);애니메이션 루프
requestAnimationFrame은 디스플레이 새로고침과 동기화합니다(~60fps). clearRect는 각 프레임 전에 지웁니다. cancelAnimationFrame으로 취소하세요.
let x = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'green';
ctx.fillRect(x, 50, 30, 30);
x += 2;
if (x > canvas.width) x = 0;
requestAnimationFrame(animate);
}
animate();이미지 조작
drawImage는 이미지를 렌더링합니다. getImageData는 픽셀 데이터를 RGBA 배열로 반환합니다. 필터를 위해 픽셀을 조작하세요. putImageData는 수정된 픽셀을 다시 씁니다.
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, 200, 150);
const data = ctx.getImageData(0, 0, 200, 150);
for (let i = 0; i < data.data.length; i += 4)
data.data[i] = 255 - data.data[i];
ctx.putImageData(data, 0, 0);
};
img.src = 'photo.jpg';WebSockets
기본 WebSocket
WebSocket은 단일 TCP 연결을 통한 전이중 통신을 제공합니다. onopen은 연결 시, onmessage는 데이터 도착 시 발생합니다. 항상 onerror를 처리하세요.
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => { ws.send('Hello Server'); };
ws.onmessage = (e) => { console.log('Received:', e.data); };
ws.onclose = () => console.log('Disconnected');
ws.onerror = (err) => console.error('Error:', err);JSON 송수신
WebSocket 데이터는 문자열 또는 바이너리로 전송됩니다. JSON.stringify/parse로 구조화된 데이터 교환이 가능합니다. type 필드는 메시지 라우팅을 가능하게 합니다.
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'message', text: 'Hello' }));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
switch (data.type) {
case 'message': console.log(data.text); break;
}
};재연결
WebSocket 연결은 끊어질 수 있습니다. 지수 백오프(2^retries)는 서버 과부하를 방지합니다. 지연을 30초로 제한하세요. 성공 시 재시도 횟수를 재설정하세요.
class ReconnectingWS {
constructor(url) { this.url = url; this.retries = 0; this.connect(); }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.retries = 0; };
this.ws.onclose = () => {
const delay = Math.min(1000 * 2 ** this.retries++, 30000);
setTimeout(() => this.connect(), delay);
};
}
}바이너리 데이터
WebSocket은 ArrayBuffer를 통해 바이너리 데 이터를 지원합니다. binaryType을 arraybuffer로 설정하세요. DataView는 타입화된 접근을 제공합니다. 숫자 데이터에는 바이너리가 더 효율적입니다.
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setInt32(0, 42);
ws.send(buffer);
ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
if (e.data instanceof ArrayBuffer) {
const v = new DataView(e.data);
console.log(v.getInt32(0));
}
};하트비트
하트비트는 오래된 연결을 감지합니다. 핑을 보내고 퐁을 기대하세요. 타임아웃 내에 퐁이 없으면 닫고 재연결하세요. readyState는 연결 상태를 확인합니다.
setInterval(() => {
if (ws.readyState === WebSocket.OPEN)
ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
setInterval(() => {
if (Date.now() - lastPong > 60000) ws.close();
}, 10000);Service Workers
등록
서비스 워커는 별도의 스레드에서 실행되며 네트워크 요청을 가로챕니다. 등록은 HTTPS 또는 localhost에서 이루어져야 합니다. SW 파일 위치가 스코프를 결정합니다.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Registered:', reg.scope))
.catch(err => console.error('Failed:', err));
}캐싱
캐시 우선 전략: 캐시에서 서비스, 네트워크로 폴백. install은 자산을 사전 캐시합니다. fetch는 요청을 가로챕니다. 기타 전략: 네트워크 우선, stale-while-revalidate.
const CACHE = 'app-v1';
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE)
.then(c => c.addAll(['/', '/index.html', '/style.css'])));
});
self.addEventListener('fetch', (e) => {
e.respondWith(caches.match(e.request)
.then(cached => cached || fetch(e.request)));
});백그라운드 동기화
백그라운드 동기화는 연결이 돌아올 때까지 작업을 지연시킵니다. sync 이벤트는 네트워크가 사용 가능할 때 발생합니다. 보류 중인 작업을 IndexedDB에 저장하세요.
navigator.serviceWorker.ready.then(reg =>
reg.sync.register('send-messages'));
self.addEventListener('sync', (e) => {
if (e.tag === 'send-messages')
e.waitUntil(sendPendingMessages());
});푸시 알림
푸시 알림은 앱이 닫혀 있어도 작동합니다. subscribe는 VAPID 키를 사용하여 푸시 서비스에 등록합니다. userVisibleOnly는 알림 표시를 요구합니다.
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
});
self.addEventListener('push', (e) => {
const data = e.data.json();
e.waitUntil(self.registration.showNotification(data.title, {
body: data.body
}));
});업데이트 & 활성화
skipWaiting는 새 SW를 즉시 활성화합니다. activate는 오래된 캐시를 정리합니다. clients.claim은 즉시 제어권을 가져옵니다. 업데이트를 트리거하려면 캐시 이름에 버전을 지정하세요.
self.addEventListener('install', (e) => {
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE)
.map(k => caches.delete(k)))));
self.clients.claim();
});IndexedDB
데이터베이스 열기
IndexedDB는 브라우저의 NoSQL 데이터베이스입니다. onupgradeneeded는 버전 변경 시 발생하며, 객체 저장소를 만드는 데 사용됩니다. keyPath는 기본 키를 정의합니다.
const req = indexedDB.open('MyDatabase', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('users'))
db.createObjectStore('users', { keyPath: 'id' });
};
req.onsuccess = (e) => { const db = e.target.result; };추가 & Put
트랜잭션은 작업을 원자적으로 그룹화합니다. add는 중복 키에서 실패하고, put은 덮어씁니다. readwrite는 수정을 허용합니다. oncomplete는 성공 시 발생합니다.
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: 'Alice' }); // Fails if key exists
store.put({ id: 1, name: 'Bob' }); // Overwrites
tx.oncomplete = () => console.log('Saved');데이터 쿼리
get은 키로 검색합니다. openCursor는 레코드를 반복합니다. cursor.continue는 다음으로 이동합니다. async/await 사용을 위해 Promise로 감싸세요.
const tx = db.transaction('users', 'readonly');
const req = tx.objectStore('users').get(1);
req.onsuccess = () => console.log(req.result);
const cursorReq = tx.objectStore('users').openCursor();
cursorReq.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) { console.log(cursor.value); cursor.continue(); }
};인덱스 쿼리
인덱스는 비키 필드에 대한 쿼리를 가능하게 합니다. IDBKeyRange는 범위를 만듭니다. 인덱스는 onupgradeneeded에서 생성되어야 합니다.
const tx = db.transaction('users', 'readonly');
const index = tx.objectStore('users').index('name');
index.get('Alice').onsuccess = (e) =>
console.log(e.target.result);
const range = IDBKeyRange.bound('A', 'M');
index.openCursor(range).onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) { cursor.continue(); }
};삭제 & 지우기
delete는 단일 레코드를 제거합니다. clear는 모든 레코드를 제거합니다. deleteDatabase는 전체 데이터베이스를 제거합니다. 모든 수정에는 readwrite 트랜잭션이 필요합니다.
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').delete(1); // Delete by key
tx.objectStore('users').clear(); // Clear all
indexedDB.deleteDatabase('MyDatabase'); // Delete DB
tx.oncomplete = () => console.log('Done');WebRTC
사용자 미디어 가져오기
getUserMedia는 카메라와 마이크 접근을 요청합니다. MediaStream을 반환합니다. srcObject는 스트림을 video 요소에 할당합니다. HTTPS 및 사용자 권한이 필요합니다.
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.querySelector('video').srcObject = stream;
});피어 연결
RTCPeerConnection은 P2P 연결을 설정합니다. addTrack은 미디어를 추가합니다. ontrack은 원격 스트림을 받습니다. ICE 후보는 시그널링을 통해 교환되는 네트워크 경로입니다.
const pc = new RTCPeerConnection(config);
stream.getTracks().forEach(t => pc.addTrack(t, stream));
pc.ontrack = (e) => { remoteVideo.srcObject = e.streams[0]; };
pc.onicecandidate = (e) => {
if (e.candidate) sendToPeer(e.candidate);
};Offer & Answer
SDP 협상: offer/answer 교환은 미디어 형식을 설명합니다. setLocalDescription는 로컬 SDP를 설정하고, setRemoteDescription는 원격 SDP를 설정합니다.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeer(offer);
// Callee:
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeer(answer);데이터 채널
데이터 채널은 낮은 지연으로 WebRTC를 통해 임의의 데이터 전송을 가능하게 합니다. createDataChannel은 offerer에 생성합니다. ondatachannel은 answerer에 수신합니다.
const channel = pc.createDataChannel('chat');
channel.onopen = () => console.log('Connected');
channel.onmessage = (e) => console.log('Received:', e.data);
channel.send('Hello peer!');
pc.ondatachannel = (e) => {
e.channel.onmessage = (ev) => console.log(ev.data);
};화면 공유
getDisplayMedia는 화면, 창 또는 탭을 캡처합니다. 브라우저가 선택기를 표시합니다. onended는 사용자가 공유를 중지할 때 발생합니다. 항상 정리를 처리하세요.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 }, audio: true
});
video.srcObject = stream;
stream.getVideoTracks()[0].onended = () =>
console.log('Stopped');성능 최적화
디바운스 & 스로틀
디바운스는 호출이 멈출 때까지 실행을 지연합니다(검색에 적합). 스로틀은 간격당 한 번으로 제한합니다(스크롤에 적합). 둘 다 과도한 호출을 방지합니다.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) { fn(...args); inThrottle = true;
setTimeout(() => inThrottle = false, limit); }
};
}Web Workers
Web Workers는 CPU 집약적 작업을 위해 JavaScript를 별도의 스레드에서 실행합니다. 데이터는 postMessage로 전달됩니다. 워커는 DOM에 접근할 수 없습니다.
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = (e) => console.log('Result:', e.data);
// worker.js
self.onmessage = (e) => {
self.postMessage(e.data.data.map(x => x ** 2));
};지연 로딩
IntersectionObserver는 요소가 뷰포트에 들어올 때 발생합니다. data-src는 실제 URL을 보유하고; src는 보일 때 설정됩니다. 이미지가 많은 페이지의 초기 로드를 줄입니다.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));DOM 업데이트 일괄 처리
DocumentFragment는 DOM 삽입을 단일 리플로우로 일괄 처리하여 하나씩 추가하는 것보다 훨씬 빠릅니다. 레이아웃 스래싱을 최소화하세요.
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment); // Single reflow메모리 관리
WeakMap은 키의 GC를 허용하여 누수를 방지합니다. 요소가 제거될 때 항상 이벤트 리스너를 제거하세요. GC를 위해 큰 객체를 null로 설정하세요.
const cache = new WeakMap();
cache.set(element, data);
// When element is GC'd, entry is removed
element.removeEventListener('click', handler);
bigArray = null; // Allow GC보안(XSS/CSRF)
XSS 방지
XSS는 사용자 입력을 통해 악성 스크립트를 주입합니다. 신뢰할 수 없는 데이터에 innerHTML을 절대 사용하지 마세요. textContent는 안전합니다. CSP 헤더는 스크립트 소스를 제한합니다.
// BAD: vulnerable to XSS
element.innerHTML = userInput;
// GOOD: safe alternatives
element.textContent = userInput;
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}CSRF 방지
CSRF는 사용자를 속여 원치 않는 작업을 수행하게 합니다. 토큰은 요청이 앱에서 왔음을 보장합니다. SameSite=Strict 쿠키는 크로스 사이트로 전송되지 않습니다. 상태 변경 작업에는 CSRF 토큰을 사용하세요.
const token = document.cookie.match(/csrf_token=([^;]+)/)?.[1];
fetch('/api/data', {
method: 'POST',
headers: { 'X-CSRF-Token': token }
});
// SameSite cookie: Set-Cookie: session=abc; SameSite=Strict콘텐츠 보안 정책
CSP는 로드할 수 있는 리소스를 제한합니다. default-src는 폴백입니다. script-src는 JavaScript를 제어합니다. 강제 전에 report-only 모드로 시작하세요.
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:">보안 쿠키
HttpOnly는 JavaScript의 쿠키 접근을 방지하여 XSS를 완화합니다. Secure는 HTTPS만 보장합니다. SameSite=Strict는 CSRF를 방지합니다. 세션 쿠키는 항상 둘 다 사용해야 합니다.
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
// HttpOnly: not accessible via JavaScript
// Secure: only sent over HTTPS
// SameSite: Strict | Lax | None
document.cookie // Cannot see HttpOnly cookies입력 검증
클라이언트 측 검증은 UX를 향상시키지만 보안은 아닙니다. 항상 서버에서 검증하세요. HTML 살균에는 DOMPurify를 사용하세요. 허용된 태그를 화이트리스트하세요.
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function sanitize(html) {
return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong'] });
}
// Never trust client input - always validate on server too디자인 패턴
싱글톤
싱글톤은 하나의 인스턴스만 존재하도록 보장합니다. 생성자는 기존 인스턴스를 반환합니다. 설정, 로깅 및 데이터베이스 연결에 유용합니다.
class Config {
constructor() {
if (Config.instance) return Config.instance;
this.settings = {};
Config.instance = this;
}
}
const c1 = new Config();
const c2 = new Config();
console.log(c1 === c2); // true옵저버/Pub-Sub
옵저버 패턴은 객체가 이벤트를 구독할 수 있게 합니다. on은 등록, emit은 트리거, off는 구독 취소입니다. 이벤트 기반 아키텍처의 기초입니다.
class EventEmitter {
constructor() { this.events = {}; }
on(event, cb) { (this.events[event] ||= []).push(cb); }
emit(event, data) { (this.events[event] || []).forEach(cb => cb(data)); }
off(event, cb) { this.events[event] = this.events[event]?.filter(c => c !== cb); }
}팩토리
팩토리는 인스턴스화 로직을 노출하지 않고 객체를 만듭니다. 호출자는 타입을 지정하고, 팩토리는 어느 클래스를 인스턴스화할지 결정합니다.
class Dog { speak() { return 'Woof'; } }
class Cat { speak() { return 'Meow'; } }
function AnimalFactory(type) {
switch (type) {
case 'dog': return new Dog();
case 'cat': return new Cat();
}
}모듈 패턴
모듈 패턴은 클로저를 사용하여 private 상태를 캡슐화합니다. IIFE는 private 스코프를 만듭니다. 반환된 메서드만 public입니다.
const counter = (() => {
let count = 0; // Private
return {
increment: () => ++count,
getCount: () => count
};
})();
counter.increment();
console.log(counter.getCount()); // 1전략
전략 패턴은 교환 가능한 알고리즘을 캡슐화합니다. 컨텍스트는 선택된 전략에 위임합니다. 큰 if/else 체인을 방지합니다.
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
function calculate(strategy, a, b) {
return strategies[strategy]?.(a, b);
}일반적인 함정
this 바인딩
this는 함수가 호출되는 방식에 따라 결정됩니다. 화살표 함수는 둘러싸는 스코프에서 this를 상속합니다. 분리된 메서드는 this를 잃습니다. bind를 사용하세요.
const obj = {
name: 'Alice',
greet: function() { console.log(this.name); },
arrow: () => console.log(this.name)
};
obj.greet(); // Alice
obj.arrow(); // undefined
const fn = obj.greet;
fn(); // undefined (lost binding)부동소수점
JavaScript는 IEEE 754 부동소수점을 사용합니다. 비교에는 Number.EPSILON을 사용하거나, 정수로 작업하기 위해 10의 거듭제곱을 곱하세요.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
const eps = Number.EPSILON;
console.log(Math.abs(0.1 + 0.2 - 0.3) < eps); // true== vs ===
==는 타입 강제 변환을 수행하여 놀라운 결과를 초래합니다. ===는 강제 변환 없이 타입과 값 모두를 검사합니다. 항상 ===와 !==를 사용하세요.
console.log(0 == false); // true
console.log('' == false); // true
console.log(null == undefined); // true
console.log(0 === false); // false
console.log('' === false); // false
// Always use ===루프의 클로저
var는 함수 스코프이므로 모든 클로저가 동일한 변수를 공유합니다. let은 블록 스코프이며 반복당 새 바인딩을 만듭니다. 항상 let/const를 사용하세요.
// BUG: all print 3
for (var i = 0; i < 3; i++)
setTimeout(() => console.log(i), 100);
// FIX: let
for (let i = 0; i < 3; i++)
setTimeout(() => console.log(i), 100);Async 에러 처리
처리되지 않은 Promise 거부는 Node.js를 충돌시킬 수 있습니다. 항상 await를 try/catch로 감싸거나, .catch()를 사용하세요. unhandledRejection 이벤트를 수신하세요.
// BUG: unhandled rejection
async function fetchData() {
const data = await fetch('/api');
return data.json();
}
// FIX: try/catch
async function fetchDataSafe() {
try { return await (await fetch('/api')).json(); }
catch (err) { console.error('Failed:', err); return null; }
}관련 JavaScript 스니펫
Copy-paste ready code for common tasks.
배열 Map Filter Reduce
배열에서 map, filter, reduce, find, some, every 체이닝.
배열 중복 제거
Set을 사용해 배열에서 중복 제거.
깊은 복제
객체를 깊이 복제하며 일반 데이터 타입 지원.
디바운스 함수
이벤트 발생 후 일정 시간 대기 후 실행; 대기 중 다시 발생하면 타이머 재설정.
스로틀 함수
시간 간격 내에 함수가 최대 한 번만 실행되도록 제한.
Promise.all 동시성 제어
동시성 제한이 있는 Promise 실행기.
async/await 오류 처리
비동기 함수를 감싸 예외를 일관되게 잡기.
Fetch 래퍼
타임아웃, 오류 처리 및 JSON 파싱이 있는 fetch 래핑.
localStorage 연산
만료 시간 및 JSON 지원으로 localStorage 래핑.
Cookie 연산
Cookie 읽기, 쓰기 및 삭제 작업 래핑.
URL 매개변수 파싱
URL 쿼리 문자열을 객체로 파싱.
날짜 형식화
날짜를 지정된 문자열로 형식화.
금액 형식화
숫자를 천 단위 구분자가 있는 금액 문자열로 형식화.
난수 생성
범위 내 난수 및 무작위 문자열 생성.
색상 변환
RGB와 HEX 색상 간 변환.
UUID 생성
UUID v4를 준수하는 고유 식별자 생성.
문자열 자르기
문자열을 자르고 말줄임표 추가.
배열 평탄화
다차원 배열을 1차원으로 평탄화.
객체 병합
여러 객체를 깊이 병합.
타입 확인
JavaScript 데이터 타입을 정확히 판별.
이벤트 위임
이벤트 버블링을 통해 이벤트 위임 구현.
DOM 조작
DOM 요소를 동적으로 생성하고 조작.
폼 검증
일반적인 폼 검증 규칙 모음.
파일 업로드
진행률 및 청킹 지원으로 파일 업로드 래핑.
이미지 지연 로딩
IntersectionObserver로 이미지 지연 로딩 구현.
클립보드에 복사
크로스 브라우저 클립보드 복사 방법.
전체화면 API
브라우저 전체화면 작업 래핑.
지리적 위치
사용자 지리적 위치 정보 가져오기.
Web Worker
시간이 많이 걸리는 작업을 실행할 Web Worker 생성.
Service Worker
오프라인 캐싱을 위해 Service Worker 등록.
IndexedDB 연산
IndexedDB CRUD 작업 래핑.
Canvas 그리기
기본 Canvas 그리기 예제.
Was this helpful?