JavaScript는 브라우저의 언어를 넘어 프론트엔드 설계의 핵심이 됨. 단순히 동작하는 코드를 넘어 패턴, 성능, 보안을 함께 고려해야 실무에서 유지보수 가능한 코드가 나옴.
1. 실무에서 자주 쓰는 패턴
1-1. 클로저 (Closure) — 상태를 캡슐화
클로저는 함수가 자신이 선언된 스코프의 변수를 기억하는 것. 외부에서 직접 접근할 수 없는 프라이빗 상태를 만들 때 활용.
// 카운터: 외부에서 count에 직접 접근 불가
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment() { count += 1; },
decrement() { count -= 1; },
reset() { count = initialValue; },
getCount() { return count; },
};
}
const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.getCount()); // 12
console.log(counter.count); // undefined — 외부 접근 불가
// 실무 활용: API 요청 제한기 (rate limiter)
function createRateLimiter(maxRequests, windowMs) {
const requests = [];
return function canRequest() {
const now = Date.now();
// 윈도우 밖 오래된 요청 제거
while (requests.length && requests[0] <= now - windowMs) {
requests.shift();
}
if (requests.length < maxRequests) {
requests.push(now);
return true;
}
return false;
};
}
const limiter = createRateLimiter(5, 1000); // 1초에 5번
if (limiter()) fetchData();
JavaScript
복사
1-2. 커링 (Currying) — 함수를 부분 적용
인수가 많은 함수를 단계적으로 호출 가능하게 만듦. 재사용 가능한 특화 함수를 만들 때 유용.
// 기본 커링
const multiply = (a) => (b) => a * b;
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 범용 커링 유틸리티
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...moreArgs) => curried(...args, ...moreArgs);
};
}
// 실무 활용: 필터 함수 조합
const filter = curry((predicate, arr) => arr.filter(predicate));
const map = curry((transform, arr) => arr.map(transform));
const isActive = filter(user => user.active);
const getNames = map(user => user.name);
const getActiveNames = (users) => getNames(isActive(users));
const users = [
{ name: '김철수', active: true },
{ name: '이영희', active: false },
{ name: '박민준', active: true },
];
console.log(getActiveNames(users)); // ['김철수', '박민준']
JavaScript
복사
1-3. 옵저버 패턴 (Observer) — 이벤트 기반 통신
컴포넌트 간 직접 의존 없이 이벤트로 통신. 전역 상태 변화 감지, 실시간 UI 업데이트에 활용.
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(callback);
// 구독 해제 함수 반환 (메모리 누수 방지)
return () => this.off(event, callback);
}
off(event, callback) {
this.#listeners.get(event)?.delete(callback);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(cb => cb(...args));
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
// 실무 활용: 장바구니 상태 관리
const cartEmitter = new EventEmitter();
// 헤더 컴포넌트에서 구독
const unsubscribe = cartEmitter.on('cart:updated', ({ count, total }) => {
document.getElementById('cart-count').textContent = count;
document.getElementById('cart-total').textContent = `${total.toLocaleString()}원`;
});
// 상품 페이지에서 발행
cartEmitter.emit('cart:updated', { count: 3, total: 45000 });
// 페이지 이탈 시 구독 해제
window.addEventListener('beforeunload', unsubscribe);
JavaScript
복사
1-4. 프록시 (Proxy) — 객체 동작 가로채기
객체의 get/set/delete를 가로채 유효성 검증, 로깅, 반응형 시스템 구현에 활용.
// 유효성 검증이 내장된 폼 상태
function createFormState(initialState, validators = {}) {
const errors = {};
return new Proxy({ ...initialState }, {
set(target, key, value) {
// 유효성 검증
if (validators[key]) {
const error = validators[key](value);
if (error) {
errors[key] = error;
console.warn(`[FormState] ${key}: ${error}`);
return true; // set은 막지 않고 에러만 기록
} else {
delete errors[key];
}
}
target[key] = value;
return true;
},
get(target, key) {
if (key === '$errors') return { ...errors };
if (key === '$isValid') return Object.keys(errors).length === 0;
return target[key];
},
});
}
const form = createFormState(
{ email: '', age: 0 },
{
email: (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v) ? null : '올바른 이메일 형식이 아닙니다',
age: (v) => (v >= 0 && v <= 150) ? null : '나이는 0~150 사이여야 합니다',
}
);
form.email = 'not-an-email'; // warn: 올바른 이메일 형식이 아닙니다
form.email = 'user@example.com';
form.age = 25;
console.log(form.$isValid); // true
console.log(form.$errors); // {}
JavaScript
복사
1-5. 메모이제이션 (Memoization) — 결과 캐싱
동일한 입력에 대한 반복 계산을 캐시해 성능 향상.
// 범용 메모이제이션
function memoize(fn, keyFn = (...args) => JSON.stringify(args)) {
const cache = new Map();
return function (...args) {
const key = keyFn(...args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 피보나치 수열 — 반복 계산 제거
const fib = memoize(function(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // 빠르게 계산됨
// TTL(만료 시간) 있는 캐시 — API 응답 캐싱
function memoizeWithTTL(fn, ttlMs = 60_000) {
const cache = new Map();
return async function (...args) {
const key = JSON.stringify(args);
const entry = cache.get(key);
if (entry && Date.now() - entry.timestamp < ttlMs) {
return entry.value; // 캐시 히트
}
const value = await fn(...args);
cache.set(key, { value, timestamp: Date.now() });
return value;
};
}
const fetchUser = memoizeWithTTL(
(id) => fetch(`/api/users/${id}`).then(r => r.json()),
5 * 60 * 1000 // 5분 캐시
);
JavaScript
복사
2. 비동기 처리 심화
2-1. 병렬 vs 순차 처리
// 순차 처리: 각 요청이 완료돼야 다음 시작 (느림)
async function fetchSequential(ids) {
const results = [];
for (const id of ids) {
const data = await fetch(`/api/items/${id}`).then(r => r.json());
results.push(data);
}
return results;
}
// 병렬 처리: 동시에 모두 요청 (빠름, 독립적인 경우)
async function fetchParallel(ids) {
return Promise.all(
ids.map(id => fetch(`/api/items/${id}`).then(r => r.json()))
);
}
// 부분 실패 허용: 하나가 실패해도 나머지 결과 수집
async function fetchWithFallback(ids) {
const results = await Promise.allSettled(
ids.map(id => fetch(`/api/items/${id}`).then(r => r.json()))
);
return results.map((r, i) => ({
id: ids[i],
data: r.status === 'fulfilled' ? r.value : null,
error: r.status === 'rejected' ? r.reason.message : null,
}));
}
// 동시 요청 수 제한: 서버 부하 방지
async function fetchWithConcurrency(ids, maxConcurrent = 3) {
const results = [];
for (let i = 0; i < ids.length; i += maxConcurrent) {
const chunk = ids.slice(i, i + maxConcurrent);
const batch = await Promise.all(
chunk.map(id => fetch(`/api/items/${id}`).then(r => r.json()))
);
results.push(...batch);
}
return results;
}
JavaScript
복사
2-2. 재시도 로직 (Retry with Exponential Backoff)
async function fetchWithRetry(url, options = {}, retries = 3) {
const { baseDelay = 300, maxDelay = 5000, ...fetchOptions } = options;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, fetchOptions);
// 5xx 서버 오류는 재시도, 4xx 클라이언트 오류는 즉시 throw
if (response.status >= 500 && attempt < retries) {
throw new Error(`Server error: ${response.status}`);
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
if (attempt === retries) throw err;
// 지수 백오프 + 지터(jitter)로 서버 집중 방지
const delay = Math.min(
baseDelay * 2 ** attempt + Math.random() * 100,
maxDelay
);
console.warn(`Attempt ${attempt + 1} failed. Retrying in ${delay.toFixed(0)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// AbortController로 타임아웃 처리
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} catch (err) {
if (err.name === 'AbortError') throw new Error(`Request timeout after ${timeoutMs}ms`);
throw err;
} finally {
clearTimeout(timeoutId);
}
}
JavaScript
복사
2-3. 비동기 큐 (Task Queue)
class AsyncQueue {
#queue = [];
#running = 0;
#maxConcurrent;
constructor(maxConcurrent = 1) {
this.#maxConcurrent = maxConcurrent;
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.#queue.push({ task, resolve, reject });
this.#process();
});
}
async #process() {
if (this.#running >= this.#maxConcurrent || !this.#queue.length) return;
this.#running++;
const { task, resolve, reject } = this.#queue.shift();
try { resolve(await task()); }
catch (err) { reject(err); }
finally {
this.#running--;
this.#process();
}
}
}
// 실무 활용: 이미지 업로드 순차 처리
const uploadQueue = new AsyncQueue(2); // 동시 2개 업로드
const uploadPromises = files.map(file =>
uploadQueue.enqueue(() => uploadFile(file))
);
await Promise.all(uploadPromises);
JavaScript
복사
3. 성능 최적화
3-1. 디바운스 & 쓰로틀
// 디바운스: 마지막 호출 이후 delay가 지나야 실행 (검색 자동완성)
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
// 쓰로틀: interval마다 최대 1번만 실행 (스크롤 이벤트)
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 실무 활용
const handleSearch = debounce(async (query) => {
if (!query.trim()) return;
const results = await searchAPI(query);
renderResults(results);
}, 300);
const handleScroll = throttle(() => {
const scrollTop = window.scrollY;
updateProgressBar(scrollTop);
if (isNearBottom(scrollTop)) loadMoreContent();
}, 100);
document.getElementById('search').addEventListener('input', e => handleSearch(e.target.value));
window.addEventListener('scroll', handleScroll, { passive: true });
JavaScript
복사
3-2. 가상 스크롤 (Virtual Scroll)
대용량 목록에서 실제 DOM에는 화면에 보이는 항목만 렌더링.
class VirtualScroller {
#container;
#items;
#itemHeight;
#visibleCount;
#scrollTop = 0;
constructor(container, items, itemHeight) {
this.#container = container;
this.#items = items;
this.#itemHeight = itemHeight;
this.#visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2; // 버퍼 2
this.#setup();
}
#setup() {
const totalHeight = this.#items.length * this.#itemHeight;
// 전체 높이를 잡아주는 스페이서
this.spacer = document.createElement('div');
this.spacer.style.height = `${totalHeight}px`;
this.spacer.style.position = 'relative';
this.#container.appendChild(this.spacer);
this.#container.addEventListener('scroll', () => {
this.#scrollTop = this.#container.scrollTop;
this.#render();
}, { passive: true });
this.#render();
}
#render() {
const startIndex = Math.floor(this.#scrollTop / this.#itemHeight);
const endIndex = Math.min(startIndex + this.#visibleCount, this.#items.length);
// 기존 항목 제거 후 재렌더링 (실제 구현에서는 diff 적용)
this.spacer.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const el = document.createElement('div');
el.style.cssText = `
position: absolute;
top: ${i * this.#itemHeight}px;
height: ${this.#itemHeight}px;
width: 100%;
`;
el.textContent = this.#items[i].label;
this.spacer.appendChild(el);
}
}
}
// 10만 개 항목도 부드럽게
const items = Array.from({ length: 100_000 }, (_, i) => ({ label: `항목 ${i + 1}` }));
new VirtualScroller(document.getElementById('list'), items, 40);
JavaScript
복사
3-3. Web Worker — 무거운 연산을 메인 스레드 밖으로
// worker.js
self.addEventListener('message', ({ data }) => {
const { type, payload } = data;
if (type === 'HEAVY_COMPUTE') {
// 메인 스레드를 블로킹하지 않고 연산 수행
const result = heavyComputation(payload);
self.postMessage({ type: 'RESULT', result });
}
});
function heavyComputation(data) {
// 예: 대용량 데이터 정렬/필터링
return data
.filter(item => item.score > 50)
.sort((a, b) => b.score - a.score)
.slice(0, 100);
}
// main.js — 워커 래퍼 (Promise 기반)
class WorkerWrapper {
#worker;
#pending = new Map();
#idCounter = 0;
constructor(scriptUrl) {
this.#worker = new Worker(scriptUrl);
this.#worker.addEventListener('message', ({ data }) => {
const { id, result, error } = data;
const { resolve, reject } = this.#pending.get(id) ?? {};
this.#pending.delete(id);
error ? reject(new Error(error)) : resolve(result);
});
}
postMessage(type, payload) {
return new Promise((resolve, reject) => {
const id = ++this.#idCounter;
this.#pending.set(id, { resolve, reject });
this.#worker.postMessage({ id, type, payload });
});
}
terminate() { this.#worker.terminate(); }
}
const worker = new WorkerWrapper('/worker.js');
const results = await worker.postMessage('HEAVY_COMPUTE', largeDataset);
JavaScript
복사
3-4. requestAnimationFrame & 레이아웃 렉 방지
// 나쁜 예: 레이아웃 스래싱 (read-write 교차)
function badAnimation(elements) {
elements.forEach(el => {
const height = el.offsetHeight; // read → 레이아웃 강제 실행
el.style.height = `${height * 2}px`; // write → 무효화
// 다음 요소에서 또 read → 또 레이아웃 강제 실행 (N번 반복)
});
}
// 좋은 예: read 먼저 일괄 → write 일괄
function goodAnimation(elements) {
// 1. 읽기 배치
const heights = elements.map(el => el.offsetHeight);
// 2. 쓰기 배치 (rAF로 페인트 직전에 적용)
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`;
});
});
}
// rAF 기반 부드러운 애니메이션 루프
function animateProgress(element, targetWidth, duration = 1000) {
const start = performance.now();
const from = parseFloat(element.style.width) || 0;
function step(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
// easeInOut 이징
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
element.style.width = `${from + (targetWidth - from) * eased}%`;
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
JavaScript
복사
4. 보안
4-1. XSS (Cross-Site Scripting) 방어
가장 흔한 웹 취약점. 사용자 입력을 그대로 DOM에 삽입하는 것이 원인.
// 나쁜 예: innerHTML 직접 삽입 → XSS 위험
function renderBad(userInput) {
document.getElementById('output').innerHTML = userInput;
// userInput = '<img src=x onerror="document.cookie를 탈취">' 같은 공격 가능
}
// 좋은 예 1: textContent 사용 (HTML 태그 무력화)
function renderSafe(userInput) {
document.getElementById('output').textContent = userInput;
}
// 좋은 예 2: HTML 이스케이프 함수
function escapeHTML(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return String(str).replace(/[&<>"'/]/g, char => escapeMap[char]);
}
// 좋은 예 3: DOM API로 안전하게 구성 (가장 권장)
function renderUserComment(comment) {
const article = document.createElement('article');
const author = document.createElement('strong');
const content = document.createElement('p');
const date = document.createElement('time');
author.textContent = comment.author; // 자동 이스케이프
content.textContent = comment.body;
date.textContent = comment.createdAt;
date.dateTime = comment.createdAt;
article.append(author, content, date);
return article;
}
// HTML을 허용해야 할 때: DOMPurify 라이브러리 사용
// import DOMPurify from 'dompurify';
// element.innerHTML = DOMPurify.sanitize(userHtml);
JavaScript
복사
4-2. CSRF 방어 & 안전한 요청
// CSRF 토큰을 모든 상태 변경 요청에 포함
class SecureAPI {
#baseURL;
#csrfToken;
constructor(baseURL) {
this.#baseURL = baseURL;
// 서버가 쿠키 또는 메타 태그에 심어둔 CSRF 토큰 읽기
this.#csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')?.[1]
?? document.querySelector('meta[name="csrf-token"]')?.content
?? '';
}
async #request(method, path, body) {
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // CSRF 방어 추가 레이어
};
// GET/HEAD 는 CSRF 토큰 불필요
if (!['GET', 'HEAD'].includes(method)) {
headers['X-CSRF-Token'] = this.#csrfToken;
}
const response = await fetch(`${this.#baseURL}${path}`, {
method,
headers,
credentials: 'same-origin', // 쿠키 전송 (same-origin만)
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 403) throw new Error('CSRF 검증 실패');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
get (path) { return this.#request('GET', path); }
post (path, body) { return this.#request('POST', path, body); }
put (path, body) { return this.#request('PUT', path, body); }
del (path) { return this.#request('DELETE', path); }
}
const api = new SecureAPI('/api');
await api.post('/users', { name: '홍길동' });
JavaScript
복사
4-3. 민감 데이터 처리
// 나쁜 예: 민감 데이터를 localStorage에 저장
localStorage.setItem('token', accessToken); // XSS에 탈취 위험
localStorage.setItem('password', userPassword); // 절대 금지
// 좋은 예: 토큰은 HttpOnly 쿠키 (서버 설정)
// Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Strict
// JavaScript에서 접근 불가 → XSS 탈취 차단
// 메모리 내 민감 데이터는 사용 후 즉시 초기화
function processPassword(rawPassword) {
try {
const hash = hashPassword(rawPassword);
return hash;
} finally {
// 원본 패스워드 변수 정리 (JS는 GC 기반이라 완전하지 않지만 습관화)
rawPassword = null;
}
}
// 로그에 민감 데이터가 포함되지 않도록
function sanitizeLog(data) {
const sensitiveKeys = ['password', 'token', 'secret', 'cardNumber', 'cvv'];
return JSON.parse(
JSON.stringify(data, (key, value) =>
sensitiveKeys.some(k => key.toLowerCase().includes(k)) ? '[REDACTED]' : value
)
);
}
console.log(sanitizeLog({
username: 'admin',
password: 'secret123', // [REDACTED]
token: 'eyJhbGc...', // [REDACTED]
}));
JavaScript
복사
4-4. Content Security Policy (CSP) & 안전한 동적 코드
// 나쁜 예: eval, new Function 사용 — CSP에서 차단되고 코드 인젝션 위험
const userFormula = 'price * 0.9'; // 사용자 입력
const result = eval(userFormula); // 위험!
// 좋은 예: 허용된 연산자만 지원하는 안전한 파서
function safeEvaluate(expression, variables) {
// 허용: 숫자, 사칙연산, 괄호, 변수명만
const allowedPattern = /^[\d+\-*/.()\s]+$|^[a-zA-Z_][\w]*([+\-*/.()\s\d[\w]*]*)?$/;
if (!allowedPattern.test(expression)) {
throw new Error('허용되지 않은 표현식');
}
// 변수를 값으로 치환
let safe = expression;
for (const [key, value] of Object.entries(variables)) {
if (typeof value !== 'number') throw new Error('변수는 숫자만 허용');
safe = safe.replaceAll(key, String(value));
}
// 이 시점에서는 숫자와 연산자만 남아 있음
return Function(`'use strict'; return (${safe})`)();
}
const price = safeEvaluate('price * 0.9', { price: 50000 }); // 45000
// URL 파라미터 안전하게 처리
function getSafeParam(name) {
const param = new URLSearchParams(window.location.search).get(name);
if (!param) return null;
// 리다이렉트 오픈 리다이렉트 방지: 상대경로만 허용
if (name === 'redirect') {
try {
const url = new URL(param, window.location.origin);
if (url.origin !== window.location.origin) return '/';
return url.pathname + url.search;
} catch {
return '/';
}
}
return escapeHTML(param);
}
JavaScript
복사
4-5. 서브리소스 무결성 (SRI) & 안전한 외부 스크립트
<!-- CDN 스크립트에 SRI 해시 적용: 변조된 파일 차단 -->
<script
src="https://cdn.example.com/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- CSP 헤더 예시 (서버에서 설정) -->
<!--
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
-->
HTML
복사
5. 모던 문법 실무 패턴
5-1. 옵셔널 체이닝 & Nullish 병합 실전 조합
// 안전한 중첩 객체 접근
const city = user?.address?.city ?? '주소 미입력';
const role = user?.roles?.[0]?.name ?? 'guest';
// 메서드 호출도 안전하게
const length = str?.trim()?.length ?? 0;
// 복잡한 API 응답 처리
function parseAPIResponse(response) {
return {
userId: response?.data?.user?.id ?? null,
username: response?.data?.user?.name ?? 'Anonymous',
isAdmin: response?.data?.user?.roles?.includes('admin') ?? false,
settings: response?.data?.settings ?? {},
tags: response?.data?.tags ?? [],
};
}
JavaScript
복사
5-2. 구조 분해 고급 활용
// 이름 변경 + 기본값 동시 적용
const { name: username = 'Guest', age: userAge = 0, ...rest } = user;
// 함수 파라미터 구조 분해
function createUser({
name,
email,
role = 'user',
isActive = true,
createdAt = new Date().toISOString(),
} = {}) {
return { name, email, role, isActive, createdAt };
}
// 중첩 구조 분해
const {
data: {
items: [firstItem, ...remainingItems] = [],
pagination: { page = 1, total = 0 } = {},
} = {},
} = apiResponse ?? {};
// 배열 스왑
let a = 1, b = 2;
[a, b] = [b, a];
JavaScript
복사
5-3. WeakMap & WeakRef — 메모리 누수 방지
// WeakMap: DOM 노드에 메타데이터 연결 (노드 제거 시 자동 GC)
const elementMetadata = new WeakMap();
function attachTooltip(element, text) {
elementMetadata.set(element, { tooltip: text, createdAt: Date.now() });
element.addEventListener('mouseenter', showTooltip);
}
// element가 DOM에서 제거되면 WeakMap 엔트리도 자동 해제
// 일반 Map 사용 시 → 메모리 누수 발생
// 실무 활용: 컴포넌트 프라이빗 상태
const _state = new WeakMap();
class Component {
constructor(element) {
_state.set(this, { element, count: 0, handlers: [] });
}
increment() {
const state = _state.get(this);
state.count++;
state.element.textContent = state.count;
}
destroy() {
const { element, handlers } = _state.get(this);
handlers.forEach(([evt, fn]) => element.removeEventListener(evt, fn));
// WeakMap에서 명시적 삭제 없이도 this 참조 소멸 시 자동 GC
}
}
// WeakRef: 캐시에서 약한 참조 (대용량 객체 GC 허용)
const imageCache = new Map();
function cacheImage(url, imageData) {
imageCache.set(url, new WeakRef(imageData));
}
function getCachedImage(url) {
const ref = imageCache.get(url);
if (!ref) return null;
const image = ref.deref();
if (!image) {
// GC에 의해 수집됨 → 캐시 정리
imageCache.delete(url);
return null;
}
return image;
}
JavaScript
복사
5-4. 제너레이터 — 지연 평가와 무한 시퀀스
// 무한 ID 생성기
function* idGenerator(prefix = 'id') {
let i = 1;
while (true) {
yield `${prefix}-${i++}`;
}
}
const gen = idGenerator('user');
console.log(gen.next().value); // 'user-1'
console.log(gen.next().value); // 'user-2'
// 페이지네이션 자동 처리
async function* fetchAllPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const data = await fetch(`${baseUrl}?page=${page}`).then(r => r.json());
yield data.items;
hasMore = data.hasNextPage;
page++;
}
}
// 사용: for-await-of 로 페이지 순회
for await (const items of fetchAllPages('/api/products')) {
processItems(items);
}
JavaScript
복사
요약
주제 | 핵심 원칙 |
클로저 | 상태를 캡슐화해 외부 노출 최소화 |
커링 | 범용 함수를 특화 함수로 부분 적용 |
비동기 | 병렬/순차/재시도/타임아웃을 상황에 맞게 조합 |
성능 | 디바운스·쓰로틀로 이벤트 제어, rAF로 렌더링 동기화 |
XSS 방어 | innerHTML 대신 textContent / DOM API / DOMPurify |
CSRF 방어 | 상태 변경 요청에 CSRF 토큰 포함, SameSite 쿠키 설정 |
민감 데이터 | localStorage 저장 금지, HttpOnly 쿠키, 로그 마스킹 |
메모리 | WeakMap으로 DOM 메타데이터, WeakRef로 캐시 설계 |
실무 JavaScript는 동작 여부보다 예측 가능한 동작, 적절한 성능, 취약점 없는 구현이 핵심임.

