JavaScript 중급 튜토리얼 실무 개발자를 위한 핵심 기술

JavaScript 중급 튜토리얼: 실무 개발자를 위한 핵심 기술

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) 학습하기
  • 웹팩, 바벨 같은 빌드 도구 이해하기
  • 함수형 프로그래밍 패러다임 심화 학습
중요한 포인트: JavaScript는 계속 발전하는 언어입니다. ES6 이후로 매년 새로운 기능들이 추가되고 있으므로, 최신 스펙을 지속적으로 학습하는 것이 중요합니다. MDN Web Docs와 JavaScript.info 같은 리소스를 활용하여 꾸준히 학습하세요.

댓글 남기기

AI, 코딩, 일상 및 다양한 정보 공유에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기