JavaScript 중급 튜토리얼: 실무 개발자를 위한 핵심 기술
JavaScript 중급 개요
JavaScript 중급 과정에서는 언어의 핵심 개념들을 깊이 있게 다루고, 실무에서 자주 사용되는 고급 기술들을 학습합니다. 클로저, 프로토타입, 비동기 처리 등 JavaScript의 강력한 기능들을 마스터하여 더 효율적이고 유지보수가 쉬운 코드를 작성할 수 있게 됩니다.
1. 클로저(Closures)와 스코프
클로저의 이해
클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합입니다. 내부 함수가 외부 함수의 변수에 접근할 수 있는 JavaScript의 강력한 기능입니다.
// 기본적인 클로저 예제
function outerFunction(x) {
// 외부 함수의 변수
const outerVariable = x;
// 내부 함수 (클로저)
function innerFunction(y) {
// 외부 함수의 변수에 접근
return outerVariable + y;
}
return innerFunction;
}
const addFive = outerFunction(5);
console.log(addFive(3)); // 8
console.log(addFive(7)); // 12
// 클로저를 활용한 프라이빗 변수
function createCounter() {
let count = 0; // 프라이빗 변수
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
스코프 체인
JavaScript는 렉시컬 스코프를 사용하며, 함수가 중첩될 때 스코프 체인이 형성됩니다.
// 스코프 체인 예제
const globalVar = '전역 변수';
function outerScope() {
const outerVar = '외부 함수 변수';
function middleScope() {
const middleVar = '중간 함수 변수';
function innerScope() {
const innerVar = '내부 함수 변수';
// 모든 스코프의 변수에 접근 가능
console.log(globalVar); // 전역 변수
console.log(outerVar); // 외부 함수 변수
console.log(middleVar); // 중간 함수 변수
console.log(innerVar); // 내부 함수 변수
}
innerScope();
}
middleScope();
}
outerScope();
// 클로저를 활용한 팩토리 함수
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
2. 프로토타입과 상속
JavaScript는 프로토타입 기반 언어로, 객체는 다른 객체로부터 직접 상속받을 수 있습니다. ES6의 클래스 문법도 내부적으로는 프로토타입을 사용합니다.
// 프로토타입 기본
function Person(name, age) {
this.name = name;
this.age = age;
}
// 프로토타입에 메서드 추가
Person.prototype.greet = function() {
return `안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`;
};
Person.prototype.haveBirthday = function() {
this.age++;
return `생일 축하합니다! 이제 ${this.age}살이 되었습니다.`;
};
const person1 = new Person('김철수', 25);
console.log(person1.greet());
console.log(person1.haveBirthday());
// 프로토타입 체인
function Student(name, age, school) {
Person.call(this, name, age); // 부모 생성자 호출
this.school = school;
}
// 프로토타입 상속 설정
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// Student만의 메서드 추가
Student.prototype.study = function(subject) {
return `${this.name}은(는) ${this.school}에서 ${subject}을(를) 공부합니다.`;
};
const student1 = new Student('이영희', 20, '서울대학교');
console.log(student1.greet()); // Person의 메서드 사용 가능
console.log(student1.study('컴퓨터공학'));
// ES6 클래스 문법 (내부적으로는 프로토타입 사용)
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
makeSound() {
return `${this.name}이(가) 소리를 냅니다.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, '개');
this.breed = breed;
}
makeSound() {
return `${this.name}이(가) 멍멍 짖습니다.`;
}
wagTail() {
return `${this.name}이(가) 꼬리를 흔듭니다.`;
}
}
const myDog = new Dog('바둑이', '진돗개');
console.log(myDog.makeSound());
console.log(myDog.wagTail());
3. this 바인딩과 컨텍스트
JavaScript에서 this는 함수가 호출되는 방식에 따라 달라지는 동적 바인딩을 합니다. 이를 정확히 이해하는 것은 매우 중요합니다.
// this 바인딩의 다양한 경우
// 1. 일반 함수 호출
function normalFunction() {
console.log(this); // window (strict mode에서는 undefined)
}
normalFunction();
// 2. 메서드 호출
const obj = {
name: '객체',
method() {
console.log(this.name); // '객체'
}
};
obj.method();
// 3. 생성자 함수
function Constructor(value) {
this.value = value;
console.log(this); // 새로 생성된 인스턴스
}
const instance = new Constructor(10);
// 4. call, apply, bind를 사용한 명시적 바인딩
const person = {
name: '홍길동',
age: 30
};
function introduce(city, job) {
return `저는 ${city}에 사는 ${this.age}살 ${this.name}이고, ${job}입니다.`;
}
// call: 즉시 실행, 인수를 개별적으로 전달
console.log(introduce.call(person, '서울', '개발자'));
// apply: 즉시 실행, 인수를 배열로 전달
console.log(introduce.apply(person, ['부산', '디자이너']));
// bind: 새로운 함수 반환, this가 고정됨
const boundIntroduce = introduce.bind(person);
console.log(boundIntroduce('대구', '기획자'));
// 5. 화살표 함수의 this (렉시컬 바인딩)
const obj2 = {
name: '객체2',
regularMethod() {
console.log('Regular:', this.name); // '객체2'
const arrowFunction = () => {
console.log('Arrow:', this.name); // '객체2' (상위 스코프의 this)
};
arrowFunction();
},
arrowMethod: () => {
console.log('Arrow method:', this.name); // undefined (window.name)
}
};
obj2.regularMethod();
obj2.arrowMethod();
// 실용적인 예제: 이벤트 핸들러에서의 this
class Button {
constructor(text) {
this.text = text;
this.clickCount = 0;
}
// 메서드를 화살표 함수로 정의 (this 바인딩 유지)
handleClick = () => {
this.clickCount++;
console.log(`${this.text} 버튼이 ${this.clickCount}번 클릭되었습니다.`);
}
attach(element) {
element.addEventListener('click', this.handleClick);
}
}
4. 비동기 프로그래밍 심화
JavaScript의 비동기 처리는 콜백, Promise, async/await를 통해 구현됩니다. 각각의 특징과 사용법을 깊이 있게 이해해봅시다.
// 1. 콜백 지옥 문제
function getData(callback) {
setTimeout(() => {
callback('데이터1');
}, 1000);
}
function getMoreData(data, callback) {
setTimeout(() => {
callback(data + ' + 데이터2');
}, 1000);
}
// 콜백 지옥
getData((data1) => {
getMoreData(data1, (data2) => {
getMoreData(data2, (data3) => {
console.log(data3); // 복잡한 중첩
});
});
});
// 2. Promise로 개선
function fetchData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (data) {
resolve(`처리된: ${data}`);
} else {
reject('데이터가 없습니다');
}
}, 1000);
});
}
// Promise 체이닝
fetchData('초기 데이터')
.then(result => {
console.log(result);
return fetchData(result + ' 추가');
})
.then(result => {
console.log(result);
return fetchData(result + ' 더 추가');
})
.catch(error => {
console.error('에러:', error);
})
.finally(() => {
console.log('작업 완료');
});
// 3. async/await로 더 깔끔하게
async function processDataSequentially() {
try {
const data1 = await fetchData('데이터1');
console.log(data1);
const data2 = await fetchData(data1 + ' 추가');
console.log(data2);
const data3 = await fetchData(data2 + ' 더 추가');
console.log(data3);
return data3;
} catch (error) {
console.error('에러 발생:', error);
}
}
// 4. 병렬 처리
async function processDataInParallel() {
try {
// 모든 Promise가 완료될 때까지 대기
const results = await Promise.all([
fetchData('데이터A'),
fetchData('데이터B'),
fetchData('데이터C')
]);
console.log('모든 결과:', results);
// 가장 빨리 완료되는 것 선택
const fastest = await Promise.race([
fetchData('빠른 데이터'),
fetchData('느린 데이터')
]);
console.log('가장 빠른 결과:', fastest);
} catch (error) {
console.error('에러:', error);
}
}
// 5. 실용적인 예제: API 호출 with 재시도 로직
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`시도 ${i + 1} 실패:${error.message}`);
if (i === maxRetries - 1) {
throw error;
}
// 재시도 전 대기
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
5. 모듈 시스템
모듈 시스템을 사용하면 코드를 재사용 가능한 단위로 분리하고 구조화할 수 있습니다. ES6 모듈과 CommonJS 모듈을 모두 이해해봅시다.
// === math.js - ES6 모듈 ===
// 개별 export
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// default export
export default class Calculator {
constructor() {
this.result = 0;
}
add(value) {
this.result += value;
return this;
}
multiply(value) {
this.result *= value;
return this;
}
getResult() {
return this.result;
}
}
// === main.js - 모듈 import ===
// named import
import { PI, add, multiply } from './math.js';
// default import
import Calculator from './math.js';
// 별칭 사용
import { add as sum } from './math.js';
// 모든 것을 import
import * as mathUtils from './math.js';
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
const calc = new Calculator();
calc.add(10).multiply(2);
console.log(calc.getResult()); // 20
// === 동적 import ===
async function loadModule() {
const module = await import('./math.js');
console.log(module.add(2, 3));
}
// 조건부 모듈 로딩
if (userNeedsAdvancedFeatures) {
import('./advanced-features.js')
.then(module => {
module.useAdvancedFeature();
});
}
// === CommonJS (Node.js) ===
// utils.js
const formatDate = (date) => {
return date.toLocaleDateString('ko-KR');
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(amount);
};
module.exports = {
formatDate,
formatCurrency
};
// app.js
const { formatDate, formatCurrency } = require('./utils');
console.log(formatDate(new Date()));
console.log(formatCurrency(1000000));
6. 구조 분해와 스프레드 연산자
구조 분해 할당과 스프레드 연산자는 코드를 더 간결하고 읽기 쉽게 만들어주는 강력한 기능입니다.
// 1. 배열 구조 분해
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// 변수 교환
let a = 10, b = 20;
[a, b] = [b, a];
console.log(a, b); // 20, 10
// 기본값 설정
const [x = 1, y = 2] = [10];
console.log(x, y); // 10, 2
// 2. 객체 구조 분해
const user = {
name: '김철수',
age: 30,
email: 'kim@example.com',
address: {
city: '서울',
district: '강남구'
}
};
// 기본 구조 분해
const { name, age, email } = user;
console.log(name, age, email);
// 별칭 사용
const { name: userName, age: userAge } = user;
console.log(userName, userAge);
// 중첩 구조 분해
const { address: { city, district } } = user;
console.log(city, district);
// 기본값과 함께
const { phone = '없음', name: fullName } = user;
console.log(phone, fullName); // '없음', '김철수'
// 3. 함수 매개변수에서 구조 분해
function createUser({ name, age, role = 'user' }) {
return {
id: Date.now(),
name,
age,
role,
createdAt: new Date()
};
}
const newUser = createUser({
name: '이영희',
age: 25
});
// 4. 스프레드 연산자 - 배열
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 배열 합치기
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// 배열 복사
const copy = [...arr1];
copy.push(4);
console.log(arr1); // [1, 2, 3] (원본 유지)
console.log(copy); // [1, 2, 3, 4]
// 함수 인수로 전개
const nums = [5, 6, 8, 2, 9];
console.log(Math.max(...nums)); // 9
// 5. 스프레드 연산자 - 객체
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
// 객체 합치기
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
// 객체 복사와 속성 추가/수정
const updatedUser = {
...user,
age: 31,
phone: '010-1234-5678'
};
// 조건부 속성 추가
const config = {
baseURL: 'https://api.example.com',
...(isDevelopment && { debug: true }),
...(hasAuth && { token: authToken })
};
// 6. Rest 파라미터
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
7. 함수형 프로그래밍
JavaScript는 함수형 프로그래밍 패러다임을 잘 지원합니다. 순수 함수, 불변성, 고차 함수 등의 개념을 활용해봅시다.
// 1. 순수 함수 (Pure Functions)
// 순수 함수: 같은 입력에 항상 같은 출력, 부작용 없음
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
// 순수하지 않은 함수
let counter = 0;
const incrementCounter = () => {
counter++; // 외부 상태 변경 (부작용)
return counter;
};
// 2. 불변성 (Immutability)
const originalArray = [1, 2, 3, 4, 5];
// 불변성을 지키는 배열 조작
const doubled = originalArray.map(x => x * 2);
const filtered = originalArray.filter(x => x > 2);
const sum = originalArray.reduce((acc, curr) => acc + curr, 0);
console.log(originalArray); // [1, 2, 3, 4, 5] (변경되지 않음)
// 객체의 불변성
const originalObj = { name: '김철수', age: 30 };
const updatedObj = { ...originalObj, age: 31 };
// 3. 고차 함수 (Higher-Order Functions)
// 함수를 반환하는 함수
const createMultiplier = (multiplier) => {
return (number) => number * multiplier;
};
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 함수를 인자로 받는 함수
const compose = (f, g) => (x) => f(g(x));
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const addOneThenDouble = compose(multiplyByTwo, addOne);
console.log(addOneThenDouble(5)); // 12
// 4. 커링 (Currying)
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
};
const regularAdd = (a, b, c) => a + b + c;
const curriedAdd = curry(regularAdd);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
// 5. 파이프와 컴포즈
const pipe = (...fns) => (x) =>
fns.reduce((v, f) => f(v), x);
const compose2 = (...fns) => (x) =>
fns.reduceRight((v, f) => f(v), x);
const toLowerCase = str => str.toLowerCase();
const split = separator => str => str.split(separator);
const join = separator => arr => arr.join(separator);
const map = fn => arr => arr.map(fn);
// 파이프라인 예제
const processString = pipe(
toLowerCase,
split(' '),
map(word => word.charAt(0).toUpperCase() + word.slice(1)),
join(' ')
);
console.log(processString('HELLO WORLD FROM JAVASCRIPT'));
// 'Hello World From Javascript'
// 6. 메모이제이션
const memoize = (fn) => {
const cache = {};
return (...args) => {
const key = JSON.stringify(args);
if (key in cache) {
console.log('캐시에서 반환:', key);
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
};
const expensiveOperation = (n) => {
console.log('계산 중...');
return n * n;
};
const memoizedOperation = memoize(expensiveOperation);
console.log(memoizedOperation(5)); // 계산 중... 25
console.log(memoizedOperation(5)); // 캐시에서 반환: [5] 25
8. 정규 표현식
정규 표현식(Regular Expression)은 문자열 패턴 매칭과 처리를 위한 강력한 도구입니다.
// 1. 정규 표현식 생성
const regex1 = /pattern/flags; // 리터럴 방식
const regex2 = new RegExp('pattern', 'flags'); // 생성자 방식
// 2. 기본 패턴 매칭
const text = 'The quick brown fox jumps over the lazy dog';
// test(): 매치 여부 확인
console.log(/fox/.test(text)); // true
// match(): 매치 결과 반환
console.log(text.match(/o./g)); // ['ow', 'ox', 'ov', 'og']
// search(): 첫 매치 위치 반환
console.log(text.search(/fox/)); // 16
// 3. 유용한 정규 표현식 패턴
const patterns = {
// 이메일 검증
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
// 전화번호 (한국)
phoneKR: /^01[0-9]-?\d{3,4}-?\d{4}$/,
// URL
url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
// 비밀번호 (8자 이상, 대소문자, 숫자, 특수문자 포함)
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
// 한글만
koreanOnly: /^[가-힣]+$/,
// 숫자만
numbersOnly: /^\d+$/,
// IP 주소
ipAddress: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
};
// 사용 예제
const validateEmail = (email) => patterns.email.test(email);
console.log(validateEmail('user@example.com')); // true
console.log(validateEmail('invalid.email')); // false
// 4. 문자열 치환
const sentence = 'JavaScript is awesome. JavaScript is powerful.';
// 첫 번째 매치만 치환
console.log(sentence.replace(/JavaScript/, 'JS'));
// 'JS is awesome. JavaScript is powerful.'
// 모든 매치 치환 (g 플래그)
console.log(sentence.replace(/JavaScript/g, 'JS'));
// 'JS is awesome. JS is powerful.'
// 캡처 그룹 사용
const dateStr = '2024-03-15';
const formattedDate = dateStr.replace(
/(\d{4})-(\d{2})-(\d{2})/,
'$3/$2/$1'
);
console.log(formattedDate); // '15/03/2024'
// 5. 고급 기능
// Named Capture Groups
const logPattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const logMatch = '2024-03-15'.match(logPattern);
console.log(logMatch.groups); // { year: '2024', month: '03', day: '15' }
// Lookahead와 Lookbehind
const prices = 'Item1: $25, Item2: €30, Item3: $45';
// Positive Lookahead: $ 뒤의 숫자만
const dollarAmounts = prices.match(/\$(?=\d+)/g);
console.log(dollarAmounts); // ['$', '$']
// 숫자 추출 ($ 뒤의 숫자)
const dollarValues = prices.match(/(?<=\$)\d+/g);
console.log(dollarValues); // ['25', '45']
// 6. 실용적인 예제: 입력 검증 함수
class Validator {
static isEmail(email) {
return patterns.email.test(email);
}
static isPhoneNumber(phone) {
return patterns.phoneKR.test(phone);
}
static isStrongPassword(password) {
return patterns.password.test(password);
}
static sanitizeInput(input) {
// HTML 태그 제거
return input.replace(/<[^>]*>/g, '');
}
static extractUrls(text) {
const urlPattern = /https?:\/\/[^\s]+/g;
return text.match(urlPattern) || [];
}
}
9. 에러 처리와 디버깅
안정적인 애플리케이션을 만들기 위해서는 적절한 에러 처리와 디버깅 기술이 필수적입니다.
// 1. 기본 에러 처리
try {
// 위험한 코드
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error('에러 발생:', error.message);
} finally {
console.log('정리 작업 수행');
}
// 2. 커스텀 에러 클래스
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// 사용 예제
function validateUser(user) {
if (!user.email) {
throw new ValidationError('이메일은 필수입니다', 'email');
}
if (!user.password || user.password.length < 8) {
throw new ValidationError('비밀번호는 8자 이상이어야 합니다', 'password');
}
return true;
}
// 3. 에러 처리 패턴
class ErrorHandler {
static handle(error) {
if (error instanceof ValidationError) {
console.error(`검증 오류 [${error.field}]: ${error.message}`);
// UI에 에러 표시
this.showValidationError(error);
} else if (error instanceof NetworkError) {
console.error(`네트워크 오류 [${error.statusCode}]: ${error.message}`);
// 재시도 로직
this.retryRequest();
} else {
console.error('알 수 없는 오류:', error);
// 일반 에러 처리
this.showGeneralError();
}
}
static showValidationError(error) {
// UI 업데이트 로직
}
static retryRequest() {
// 재시도 로직
}
static showGeneralError() {
// 일반 에러 메시지 표시
}
}
// 4. 비동기 에러 처리
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new NetworkError(
'사용자 데이터를 가져올 수 없습니다',
response.status
);
}
const data = await response.json();
return data;
} catch (error) {
ErrorHandler.handle(error);
throw error; // 상위로 전파
}
}
// 5. 디버깅 도구
class Logger {
static logLevel = 'debug'; // 'debug', 'info', 'warn', 'error'
static debug(...args) {
if (['debug'].includes(this.logLevel)) {
console.log('🔍 DEBUG:', ...args);
}
}
static info(...args) {
if (['debug', 'info'].includes(this.logLevel)) {
console.info('ℹ️ INFO:', ...args);
}
}
static warn(...args) {
if (['debug', 'info', 'warn'].includes(this.logLevel)) {
console.warn('⚠️ WARN:', ...args);
}
}
static error(...args) {
console.error('❌ ERROR:', ...args);
}
static table(data) {
console.table(data);
}
static time(label) {
console.time(label);
}
static timeEnd(label) {
console.timeEnd(label);
}
}
// 사용 예제
Logger.debug('디버그 정보', { userId: 123 });
Logger.info('사용자 로그인');
Logger.warn('메모리 사용량 높음');
Logger.error('치명적 오류 발생');
// 성능 측정
Logger.time('작업 시간');
// ... 작업 수행 ...
Logger.timeEnd('작업 시간');
10. 성능 최적화
JavaScript 애플리케이션의 성능을 최적화하는 다양한 기법들을 알아봅시다.
// 1. 디바운싱 (Debouncing)
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 사용 예제: 검색 입력
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
console.log('검색 실행:', query);
// API 호출
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 2. 쓰로틀링 (Throttling)
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 사용 예제: 스크롤 이벤트
const throttledScroll = throttle(() => {
console.log('스크롤 위치:', window.scrollY);
}, 100);
window.addEventListener('scroll', throttledScroll);
// 3. 레이지 로딩 (Lazy Loading)
class LazyLoader {
constructor() {
this.imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
}
);
}
observe(selector) {
const images = document.querySelectorAll(selector);
images.forEach(img => this.imageObserver.observe(img));
}
}
// 사용
const lazyLoader = new LazyLoader();
lazyLoader.observe('img[data-src]');
// 4. 웹 워커 (Web Workers)
// worker.js
self.addEventListener('message', (e) => {
const { data } = e;
let result = 0;
// CPU 집약적인 작업
for (let i = 0; i < data.iterations; i++) {
result += Math.sqrt(i);
}
self.postMessage({ result });
});
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ iterations: 1000000 });
worker.addEventListener('message', (e) => {
console.log('계산 결과:', e.data.result);
});
// 5. 가상 스크롤링 (Virtual Scrolling)
class VirtualScroller {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = 10;
this.init();
}
init() {
this.container.style.height =
`${this.items.length * this.itemHeight}px`;
this.render();
this.container.addEventListener('scroll', () => {
this.render();
});
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleItems;
// 가시 영역의 아이템만 렌더링
const visibleItems = this.items.slice(startIndex, endIndex);
// DOM 업데이트
this.updateDOM(visibleItems, startIndex);
}
updateDOM(visibleItems, startIndex) {
// DOM 업데이트 로직
}
}
// 6. 메모리 관리
class ResourceManager {
constructor() {
this.resources = new Map();
this.cleanupInterval = 60000; // 1분
this.startCleanup();
}
add(key, resource) {
this.resources.set(key, {
resource,
lastAccessed: Date.now()
});
}
get(key) {
const item = this.resources.get(key);
if (item) {
item.lastAccessed = Date.now();
return item.resource;
}
return null;
}
cleanup() {
const now = Date.now();
const maxAge = 300000; // 5분
for (const [key, item] of this.resources) {
if (now - item.lastAccessed > maxAge) {
this.resources.delete(key);
console.log(`리소스 정리: ${key}`);
}
}
}
startCleanup() {
setInterval(() => this.cleanup(), this.cleanupInterval);
}
}
11. 디자인 패턴
JavaScript에서 자주 사용되는 디자인 패턴들을 구현해봅시다.
// 1. 싱글톤 패턴
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = [];
Singleton.instance = this;
}
addData(item) {
this.data.push(item);
}
getData() {
return this.data;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
// 2. 팩토리 패턴
class VehicleFactory {
static create(type, options) {
switch(type) {
case 'car':
return new Car(options);
case 'truck':
return new Truck(options);
case 'motorcycle':
return new Motorcycle(options);
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
}
}
class Car {
constructor({ brand, model }) {
this.type = 'car';
this.brand = brand;
this.model = model;
this.wheels = 4;
}
}
const myCar = VehicleFactory.create('car', {
brand: 'Toyota',
model: 'Camry'
});
// 3. 옵저버 패턴
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listenerToRemove) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(
listener => listener !== listenerToRemove
);
}
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(listener => {
listener.apply(this, args);
});
}
}
// 사용 예제
const emitter = new EventEmitter();
emitter.on('userLogin', (user) => {
console.log(`${user.name} 로그인`);
});
emitter.on('userLogin', (user) => {
console.log(`이메일 발송: ${user.email}`);
});
emitter.emit('userLogin', {
name: '김철수',
email: 'kim@example.com'
});
// 4. 전략 패턴
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
return this.strategy.pay(amount);
}
}
class CreditCardStrategy {
pay(amount) {
console.log(`신용카드로 ${amount}원 결제`);
return { success: true, method: 'credit_card' };
}
}
class PayPalStrategy {
pay(amount) {
console.log(`PayPal로 ${amount}원 결제`);
return { success: true, method: 'paypal' };
}
}
const processor = new PaymentProcessor(new CreditCardStrategy());
processor.processPayment(50000);
processor.setStrategy(new PayPalStrategy());
processor.processPayment(30000);
// 5. 프록시 패턴
class ApiService {
fetchData(endpoint) {
console.log(`API 호출: ${endpoint}`);
// 실제 API 호출 로직
return { data: 'response data' };
}
}
class ApiServiceProxy {
constructor() {
this.apiService = new ApiService();
this.cache = new Map();
}
fetchData(endpoint) {
if (this.cache.has(endpoint)) {
console.log(`캐시에서 반환: ${endpoint}`);
return this.cache.get(endpoint);
}
const result = this.apiService.fetchData(endpoint);
this.cache.set(endpoint, result);
return result;
}
}
12. 실전 프로젝트: Todo 앱 구현
지금까지 배운 내용을 종합하여 실제 Todo 애플리케이션을 구현해봅시다.
// Todo 앱 - 완전한 구현
// 1. Todo 모델
class Todo {
constructor(text, category = '일반') {
this.id = Date.now().toString(36) + Math.random().toString(36);
this.text = text;
this.category = category;
this.completed = false;
this.createdAt = new Date();
this.updatedAt = new Date();
}
toggle() {
this.completed = !this.completed;
this.updatedAt = new Date();
}
update(text) {
this.text = text;
this.updatedAt = new Date();
}
}
// 2. Todo 저장소 (LocalStorage 사용)
class TodoStorage {
constructor() {
this.storageKey = 'todos';
}
getAll() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
save(todos) {
localStorage.setItem(this.storageKey, JSON.stringify(todos));
}
clear() {
localStorage.removeItem(this.storageKey);
}
}
// 3. Todo 서비스 (비즈니스 로직)
class TodoService extends EventEmitter {
constructor() {
super();
this.storage = new TodoStorage();
this.todos = this.loadTodos();
this.filter = 'all'; // all, active, completed
}
loadTodos() {
const data = this.storage.getAll();
return data.map(item => {
const todo = Object.assign(new Todo(), item);
todo.createdAt = new Date(todo.createdAt);
todo.updatedAt = new Date(todo.updatedAt);
return todo;
});
}
saveTodos() {
this.storage.save(this.todos);
this.emit('change', this.todos);
}
addTodo(text, category) {
if (!text.trim()) {
throw new Error('Todo 텍스트는 비어있을 수 없습니다');
}
const todo = new Todo(text, category);
this.todos.unshift(todo);
this.saveTodos();
this.emit('add', todo);
return todo;
}
removeTodo(id) {
const index = this.todos.findIndex(todo => todo.id === id);
if (index !== -1) {
const removed = this.todos.splice(index, 1)[0];
this.saveTodos();
this.emit('remove', removed);
}
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.toggle();
this.saveTodos();
this.emit('toggle', todo);
}
}
updateTodo(id, text) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.update(text);
this.saveTodos();
this.emit('update', todo);
}
}
getFilteredTodos() {
switch(this.filter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
setFilter(filter) {
this.filter = filter;
this.emit('filter', filter);
}
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
this.saveTodos();
}
getStats() {
const total = this.todos.length;
const completed = this.todos.filter(t => t.completed).length;
const active = total - completed;
return { total, completed, active };
}
searchTodos(query) {
const lowerQuery = query.toLowerCase();
return this.todos.filter(todo =>
todo.text.toLowerCase().includes(lowerQuery) ||
todo.category.toLowerCase().includes(lowerQuery)
);
}
}
// 4. Todo View (UI 렌더링)
class TodoView {
constructor(service) {
this.service = service;
this.init();
this.bindEvents();
}
init() {
this.app = document.getElementById('app');
this.app.innerHTML = `
<div class="todo-app">
<h1>📝 Todo App</h1>
<div class="todo-input-section">
<input
type="text"
id="todo-input"
placeholder="할 일을 입력하세요..."
class="todo-input"
/>
<select id="category-select" class="category-select">
<option value="일반">일반</option>
<option value="업무">업무</option>
<option value="개인">개인</option>
<option value="쇼핑">쇼핑</option>
</select>
<button id="add-btn" class="add-btn">추가</button>
</div>
<div class="search-section">
<input
type="text"
id="search-input"
placeholder="검색..."
class="search-input"
/>
</div>
<div class="filter-section">
<button class="filter-btn active" data-filter="all">전체</button>
<button class="filter-btn" data-filter="active">진행중</button>
<button class="filter-btn" data-filter="completed">완료</button>
</div>
<div class="stats-section">
<span id="stats"></span>
</div>
<ul id="todo-list" class="todo-list"></ul>
<div class="actions-section">
<button id="clear-completed" class="clear-btn">
완료 항목 삭제
</button>
</div>
</div>
`;
this.cacheElements();
this.render();
}
cacheElements() {
this.todoInput = document.getElementById('todo-input');
this.categorySelect = document.getElementById('category-select');
this.addBtn = document.getElementById('add-btn');
this.searchInput = document.getElementById('search-input');
this.todoList = document.getElementById('todo-list');
this.stats = document.getElementById('stats');
this.clearBtn = document.getElementById('clear-completed');
this.filterBtns = document.querySelectorAll('.filter-btn');
}
bindEvents() {
// 서비스 이벤트 리스너
this.service.on('change', () => this.render());
this.service.on('filter', () => this.render());
// UI 이벤트 리스너
this.addBtn.addEventListener('click', () => this.addTodo());
this.todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.addTodo();
}
});
this.searchInput.addEventListener('input',
debounce((e) => this.search(e.target.value), 300)
);
this.clearBtn.addEventListener('click', () => {
if (confirm('완료된 항목을 모두 삭제하시겠습니까?')) {
this.service.clearCompleted();
}
});
this.filterBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
this.filterBtns.forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.service.setFilter(e.target.dataset.filter);
});
});
// 이벤트 위임을 사용한 Todo 항목 이벤트
this.todoList.addEventListener('click', (e) => {
const todoItem = e.target.closest('.todo-item');
if (!todoItem) return;
const id = todoItem.dataset.id;
if (e.target.classList.contains('todo-checkbox')) {
this.service.toggleTodo(id);
} else if (e.target.classList.contains('delete-btn')) {
this.service.removeTodo(id);
} else if (e.target.classList.contains('edit-btn')) {
this.editTodo(id);
}
});
}
addTodo() {
const text = this.todoInput.value.trim();
const category = this.categorySelect.value;
if (!text) return;
try {
this.service.addTodo(text, category);
this.todoInput.value = '';
this.showNotification('할 일이 추가되었습니다', 'success');
} catch (error) {
this.showNotification(error.message, 'error');
}
}
editTodo(id) {
const todo = this.service.todos.find(t => t.id === id);
if (!todo) return;
const newText = prompt('할 일 수정:', todo.text);
if (newText && newText.trim()) {
this.service.updateTodo(id, newText.trim());
this.showNotification('수정되었습니다', 'success');
}
}
search(query) {
if (query) {
const results = this.service.searchTodos(query);
this.renderTodos(results);
} else {
this.render();
}
}
render() {
const todos = this.service.getFilteredTodos();
this.renderTodos(todos);
this.renderStats();
}
renderTodos(todos) {
if (todos.length === 0) {
this.todoList.innerHTML = `
<li class="empty-state">
할 일이 없습니다 ✨
</li>
`;
return;
}
this.todoList.innerHTML = todos.map(todo => `
<li class="todo-item ${todo.completed ? 'completed' : ''}"
data-id="${todo.id}">
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? 'checked' : ''}
/>
<div class="todo-content">
<span class="todo-text">${todo.text}</span>
<span class="todo-category">${todo.category}</span>
<span class="todo-date">${this.formatDate(todo.createdAt)}</span>
</div>
<div class="todo-actions">
<button class="edit-btn">✏️</button>
<button class="delete-btn">🗑️</button>
</div>
</li>
`).join('');
}
renderStats() {
const stats = this.service.getStats();
this.stats.innerHTML = `
전체: ${stats.total} |
진행중: ${stats.active} |
완료: ${stats.completed}
`;
}
formatDate(date) {
const options = {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString('ko-KR', options);
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
}
// 5. CSS 스타일
const styles = `
<style>
.todo-app {
max-width: 600px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
}
.todo-app h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.todo-input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
}
.category-select {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
}
.add-btn {
padding: 12px 24px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
.add-btn:hover {
background: #2980b9;
}
.search-section {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 14px;
}
.filter-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-btn {
flex: 1;
padding: 10px;
background: #f0f0f0;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn.active {
background: #3498db;
color: white;
}
.stats-section {
text-align: center;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
margin-bottom: 20px;
color: #666;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 5px;
margin-bottom: 10px;
transition: all 0.3s;
}
.todo-item:hover {
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.todo-checkbox {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
}
.todo-text {
font-size: 16px;
color: #333;
}
.todo-category {
display: inline-block;
padding: 2px 8px;
background: #e3f2fd;
color: #1976d2;
border-radius: 3px;
font-size: 12px;
width: fit-content;
}
.todo-date {
font-size: 12px;
color: #999;
}
.todo-actions {
display: flex;
gap: 5px;
}
.edit-btn, .delete-btn {
padding: 5px 10px;
background: transparent;
border: none;
cursor: pointer;
font-size: 18px;
transition: transform 0.2s;
}
.edit-btn:hover, .delete-btn:hover {
transform: scale(1.2);
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
font-size: 18px;
}
.actions-section {
margin-top: 20px;
text-align: center;
}
.clear-btn {
padding: 10px 20px;
background: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.clear-btn:hover {
background: #c0392b;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 5px;
color: white;
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
}
.notification.show {
opacity: 1;
}
.notification.success {
background: #27ae60;
}
.notification.error {
background: #e74c3c;
}
.notification.info {
background: #3498db;
}
</style>
`;
// 6. 앱 초기화
document.addEventListener('DOMContentLoaded', () => {
// 스타일 추가
document.head.insertAdjacentHTML('beforeend', styles);
// 앱 컨테이너 생성
if (!document.getElementById('app')) {
const app = document.createElement('div');
app.id = 'app';
document.body.appendChild(app);
}
// Todo 앱 시작
const todoService = new TodoService();
const todoView = new TodoView(todoService);
// 전역 객체에 노출 (디버깅용)
window.todoApp = {
service: todoService,
view: todoView
};
console.log('✅ Todo 앱이 시작되었습니다!');
});
// 7. 추가 유틸리티 - 데이터 내보내기/가져오기
class TodoExporter {
static exportToJSON(todos) {
const dataStr = JSON.stringify(todos, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'
+ encodeURIComponent(dataStr);
const exportFileDefaultName = `todos_${Date.now()}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
static importFromJSON(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const todos = JSON.parse(e.target.result);
resolve(todos);
} catch (error) {
reject(new Error('잘못된 JSON 파일입니다'));
}
};
reader.onerror = () => {
reject(new Error('파일을 읽을 수 없습니다'));
};
reader.readAsText(file);
});
}
}
console.log('🎉 Todo 앱 코드가 로드되었습니다!');
마무리
이 튜토리얼에서는 JavaScript 중급 수준의 핵심 개념들을 실제 예제와 함께 살펴보았습니다. 클로저, 프로토타입, 비동기 처리, 모듈 시스템 등 JavaScript의 강력한 기능들을 마스터하면 더 효율적이고 유지보수가 쉬운 코드를 작성할 수 있습니다.
- TypeScript를 학습하여 타입 안정성 확보하기
- React, Vue, Angular 같은 프레임워크 학습하기
- Node.js로 백엔드 개발 시작하기
- 테스팅 프레임워크(Jest, Mocha) 학습하기
- 웹팩, 바벨 같은 빌드 도구 이해하기
- 함수형 프로그래밍 패러다임 심화 학습