Backend
home

JavaScript 실무 활용 — 패턴, 성능, 보안

생성일
2026/04/20 11:31
태그
JavaScript
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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;', }; 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는 동작 여부보다 예측 가능한 동작, 적절한 성능, 취약점 없는 구현이 핵심임.