디버깅 도구 활용 (Using Debugging Tools)

디버깅 도구 활용 (Using Debugging Tools)

자바스크립트 개발에서 버그는 피할 수 없다. 디버깅 도구 활용을 잘하면 문제를 빠르게 찾아내고 해결할 수 있다. 브라우저의 개발자 도구나 IDE의 기능을 활용하면 코드의 흐름을 추적하고, 에러를 분석하기 쉬워진다. 이번에는 디버깅 방법을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


디버깅을 잘 다루면 개발 속도가 빨라지고, 코드 품질도 높아진다. 하나씩 단계별로 살펴보자.


console.log로 시작하기

가장 기본적인 디버깅 방법은 console.log를 사용하는 것이다:

function calculate(a, b) {
    console.log("입력값: ", a, b);
    const result = a + b;
    console.log("결과: ", result);
    return result;
}

calculate(5, "3");
// "입력값: " 5 "3"
// "결과: " "53"

console.log로 변수 값을 출력해서 타입 오류를 발견했다. 간단하지만 강력한 첫걸음이다.


1. 브라우저 개발자 도구 열기

브라우저(Chrome, Firefox 등)의 개발자 도구는 디버깅의 핵심이다. F12나 우클릭 후 "검사"로 열 수 있다:

function buggyFunction() {
    let x = 10;
    x = x.toUpperCase(); // 에러 발생
    console.log(x);
}

buggyFunction();

위 코드를 실행하면 콘솔에 "x.toUpperCase is not a function" 에러가 뜬다. 개발자 도구의 콘솔 탭에서 에러 메시지와 스택 트레이스를 확인할 수 있다.


2. 브레이크포인트 설정

개발자 도구의 Sources 탭에서 브레이크포인트를 설정하면 코드 실행을 멈추고 변수 값을 볼 수 있다:

function processData(data) {
    let result = 0;
    for (let i = 0; i < data.length; i++) {
        result += data[i]; // 여기서 브레이크포인트 설정
    }
    return result;
}

processData([1, 2, "3"]);

Sources 탭에서 해당 줄을 클릭해 브레이크포인트를 설정하고, 실행하면 멈춘 상태에서 resulti 값을 확인할 수 있다.


3. 디버거 문 사용

debugger 키워드로 코드 내에서 실행을 멈출 수 있다:

function calculateTotal(items) {
    let total = 0;
    for (let item of items) {
        debugger; // 여기서 멈춤
        total += item;
    }
    return total;
}

calculateTotal([1, 2, 3]);

브라우저에서 실행하면 debugger가 있는 줄에서 멈추고, 개발자 도구가 열려 변수 값을 볼 수 있다.


4. 네트워크 디버깅

Network 탭으로 비동기 요청의 문제를 추적할 수 있다:

fetch("https://api.example.com/data")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log("에러: " + error));

Network 탭에서 요청 상태(404, 500 등), 응답 시간, 헤더를 확인해서 문제를 파악할 수 있다.


5. 성능 분석

Performance 탭으로 코드 실행 시간을 분석한다:

function heavyTask() {
    let sum = 0;
    for (let i = 0; i < 1e7; i++) {
        sum += i;
    }
    return sum;
}

console.time("heavyTask");
heavyTask();
console.timeEnd("heavyTask");

console.time으로 간단히 측정하거나, Performance 탭에서 전체 실행 흐름을 녹화해서 병목 지점을 찾을 수 있다.


6. 조건부 브레이크포인트

특정 조건에서만 멈추게 설정할 수 있다:

function findBug(list) {
    for (let i = 0; i < list.length; i++) {
        if (list[i] < 0) { // i === 2일 때 브레이크포인트
            console.log("음수 발견: " + list[i]);
        }
    }
}

findBug([1, 2, -3, 4]);

Sources 탭에서 해당 줄에 우클릭 후 "조건부 브레이크포인트 추가"를 선택하고 i === 2를 입력하면 특정 조건에서만 멈춘다.


7. 비동기 디버깅

비동기 코드의 흐름을 추적해보자:

async function fetchData() {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await response.json(); // 여기서 브레이크포인트
    console.log(data);
}

fetchData();

브레이크포인트를 설정하고 "Step Over"나 "Step Into"를 사용해서 비동기 호출의 각 단계를 확인할 수 있다.


8. 성능과 가독성에 미치는 영향

디버깅 방법이 개발 과정에 어떤 영향을 주는지 보자:

- 성능: 브레이크포인트와 로깅은 실행 속도를 약간 늦출 수 있지만, 문제를 빨리 해결해서 전체 효율을 높인다.

- 가독성: 디버깅 코드를 적절히 정리하면 유지보수가 쉬워진다.

브레이크포인트로 흐름을 추적하고, 네트워크 탭으로 외부 요청을 분석하는 점이 디버깅의 핵심이다.


마무리

디버깅은 단순히 console.log에서 시작해서 브라우저 개발자 도구의 강력한 기능을 활용하는 방향으로 나아간다. 브레이크포인트, 네트워크 분석, 성능 추적 등을 통해 코드의 문제를 빠르고 정확하게 파악할 수 있다.


비동기 에러 처리 (Asynchronous Error Handling)

비동기 에러 처리 (Asynchronous Error Handling)

자바스크립트에서 비동기 작업은 네트워크 요청이나 시간 지연 작업처럼 필연적으로 에러가 발생할 수 있는 상황을 동반한다. 비동기 에러 처리를 잘 다루면 이런 에러를 효과적으로 잡아내고, 코드가 안정적으로 흐를 수 있게 한다. 이번에는 비동기 환경에서의 에러 처리 방법을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


비동기 에러를 잘 관리하면 코드가 더 튼튼해지고, 디버깅도 쉬워진다. 하나씩 단계별로 살펴보자.


콜백에서의 에러 처리

비동기의 초창기 방식인 콜백은 에러를 첫 번째 인자로 전달하는 관례를 따른다:

function fetchData(callback) {
    setTimeout(() => {
        const error = new Error("데이터 가져오기 실패");
        callback(error, null);
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.log("에러: " + error.message);
    } else {
        console.log(data);
    }
}); // 1초 후 "에러: 데이터 가져오기 실패"

콜백의 첫 번째 인자로 에러를 받고, 조건문으로 처리했다. 하지만 콜백이 깊어지면 관리가 힘들어진다.


1. Promise와 catch로 에러 잡기

Promise는 catch를 통해 에러를 깔끔하게 처리한다:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error("데이터 가져오기 실패"));
        }, 1000);
    });
}

fetchData()
    .then(data => console.log(data))
    .catch(error => console.log("에러: " + error.message));
// 1초 후 "에러: 데이터 가져오기 실패"

reject로 에러를 발생시키고, catch로 잡았다. 체이닝 덕분에 흐름이 명확해졌다.


2. async/await와 try-catch 조합

async/await에서는 try-catch로 비동기 에러를 잡는다:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error("데이터 가져오기 실패")), 1000);
    });
}

async function process() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.log("에러: " + error.message);
    }
}

process(); // 1초 후 "에러: 데이터 가져오기 실패"

await로 비동기 작업을 기다리며, try-catch로 에러를 처리했다. 동기 코드처럼 읽기 쉬워졌다.


3. Promise.all에서의 에러 처리

여러 비동기 작업을 병렬로 실행할 때 에러를 다루는 방법이다:

function delay(ms, fail) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            fail ? reject(new Error("실패")) : resolve("성공");
        }, ms);
    });
}

async function processAll() {
    try {
        const results = await Promise.all([
            delay(1000, false),
            delay(500, true)
        ]);
        console.log(results);
    } catch (error) {
        console.log("에러: " + error.message);
    }
}

processAll(); // 0.5초 후 "에러: 실패"

Promise.all은 하나라도 실패하면 즉시 에러를 뱉는다. catch로 이를 잡아서 처리했다.


4. 순차적 비동기 에러 처리

작업을 순차적으로 실행하며 에러를 다룰 때는 이렇게 한다:

function delay(ms, fail) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            fail ? reject(new Error("실패")) : resolve("성공");
        }, ms);
    });
}

async function processSequential() {
    try {
        const first = await delay(1000, false);
        console.log(first);
        const second = await delay(1000, true);
        console.log(second);
    } catch (error) {
        console.log("에러: " + error.message);
    }
}

processSequential(); 
// 1초 후 "성공"
// 2초 후 "에러: 실패"

순차적으로 실행하다가 에러가 나면 즉시 catch로 넘어갔다. 순서가 중요한 경우에 적합하다.


5. 커스텀 에러로 비동기 처리

커스텀 에러를 만들어 비동기 상황을 구체적으로 다룰 수 있다:

class FetchError extends Error {
    constructor(message, status) {
        super(message);
        this.name = "FetchError";
        this.status = status;
    }
}

async function fetchData() {
    const response = await fetch("https://invalid.url");
    if (!response.ok) {
        throw new FetchError("요청 실패", response.status);
    }
    return response.json();
}

async function process() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        if (error instanceof FetchError) {
            console.log("요청 에러: " + error.message + ", 상태: " + error.status);
        } else {
            console.log("기타 에러: " + error.message);
        }
    }
}

process(); // "기타 에러: Failed to fetch" (URL이 잘못된 경우)

커스텀 에러로 에러의 원인을 명확히 해서 상황별로 처리했다.


6. 재시도 로직으로 에러 복구

비동기 에러가 나면 재시도해서 복구하려는 방법이다:

function delay(ms, fail) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            fail ? reject(new Error("실패")) : resolve("성공");
        }, ms);
    });
}

async function retry(fn, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === retries - 1) {
                console.log("최종 실패: " + error.message);
            } else {
                console.log(`재시도 ${i + 1}번`);
            }
        }
    }
}

retry(() => delay(1000, true));
// "재시도 1번"
// "재시도 2번"
// "최종 실패: 실패"

최대 3번 재시도하고, 실패하면 에러를 출력했다. 네트워크 문제처럼 일시적인 에러에 효과적이다.


7. 비동기 에러 로깅

에러를 기록해서 추적성을 높이는 방법이다:

async function fetchData() {
    throw new Error("비동기 실패");
}

async function processWithLog() {
    try {
        await fetchData();
    } catch (error) {
        const log = {
            message: error.message,
            stack: error.stack,
            time: new Date()
        };
        console.log("에러 로그: ", log);
    }
}

processWithLog(); 
// "에러 로그: { message: '비동기 실패', stack: ..., time: ... }"

에러의 세부 정보를 기록해서 나중에 분석할 수 있게 했다.


8. 성능과 가독성에 미치는 영향

비동기 에러 처리가 성능과 가독성에 어떤 영향을 주는지 보자:

- 성능: 재시도 로직은 실행 시간을 늘릴 수 있지만, 안정성을 높인다.

- 가독성: async/awaittry-catch를 쓰면 비동기 코드가 직관적이 된다.

커스텀 에러로 구체성을 더하고, 재시도로 복구 가능성을 높이는 점이 핵심이다.


마무리

비동기 에러 처리는 콜백, Promise, async/await를 통해 점점 더 간결해졌다. 병렬과 순차 처리, 재시도, 로깅까지 결합하면 비동기 작업이 더 안정적이고 관리하기 쉬워진다.


에러 핸들링 전략 (Error Handling Strategies)

에러 핸들링 전략 (Error Handling Strategies)

자바스크립트에서 에러는 피할 수 없는 존재다. 하지만 에러 핸들링 전략을 잘 세우면 코드가 예상치 못한 상황에서도 꺾이지 않고 안정적으로 흐를 수 있다. 이번에는 에러를 다루는 다양한 접근법을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


에러를 잘 관리하면 코드가 더 튼튼해지고, 디버깅도 쉬워진다. 하나씩 단계별로 살펴보자.


기본적인 try-catch 전략

가장 기본적인 에러 핸들링은 try-catch로 시작한다. 에러가 날 만한 부분을 감싸고, 발생 시 처리하는 방식이다:

try {
    let data = JSON.parse("잘못된 JSON");
    console.log(data);
} catch (error) {
    console.log("파싱 실패: " + error.message);
} // "파싱 실패: Unexpected token � in JSON at position 0"

try 안에서 문제가 생기면 catch가 잡아서 흐름을 유지한다. 이건 에러 핸들링의 첫걸음이다.


1. 에러를 무시하고 기본값 제공하기

에러가 나도 흐름을 멈추지 않고 기본값으로 대체하는 전략이다:

function getValue(input) {
    try {
        return JSON.parse(input);
    } catch (error) {
        console.log("파싱 에러, 기본값 반환");
        return {};
    }
}

const result = getValue("잘못된 데이터");
console.log(result); // "파싱 에러, 기본값 반환" / {}

에러가 나도 빈 객체를 반환해서 코드가 계속 실행되도록 했다. 이 방식은 앱이 멈추는 걸 방지할 때 유용하다.


2. 에러를 상위로 전달하기

에러를 직접 처리하지 않고 호출한 쪽으로 넘기는 전략도 있다:

function parseData(raw) {
    if (!raw) {
        throw new Error("데이터가 없다");
    }
    return JSON.parse(raw);
}

try {
    const data = parseData(null);
    console.log(data);
} catch (error) {
    console.log("상위에서 처리: " + error.message);
} // "상위에서 처리: 데이터가 없다"

parseData는 에러를 던지고, 상위에서 catch로 잡았다. 책임을 분리해서 로직을 깔끔하게 유지할 수 있다.


3. 비동기 에러를 다루는 전략

비동기 코드에서는 try-catchasync/await와 조합하거나 Promise의 catch를 쓴다:

async function fetchData() {
    try {
        const response = await fetch("https://invalid.url");
        if (!response.ok) {
            throw new Error("응답 실패");
        }
        console.log("데이터 가져오기 성공");
    } catch (error) {
        console.log("비동기 에러: " + error.message);
    }
}

fetchData(); // "비동기 에러: Failed to fetch" (URL이 잘못된 경우)

await로 비동기 작업을 기다리며 에러를 잡았다. 비동기 흐름을 깔끔하게 관리할 수 있다.


4. 에러 타입에 따른 분기 처리

에러의 종류를 구분해서 다르게 처리하는 전략이다:

function processInput(input) {
    try {
        if (typeof input !== "string") {
            throw new TypeError("문자열이 아니다");
        }
        if (!input) {
            throw new Error("값이 비었다");
        }
        console.log("입력 처리: " + input);
    } catch (error) {
        if (error instanceof TypeError) {
            console.log("타입 문제: " + error.message);
        } else {
            console.log("일반 에러: " + error.message);
        }
    }
}

processInput(42); // "타입 문제: 문자열이 아니다"
processInput(""); // "일반 에러: 값이 비었다"

instanceof로 에러 타입을 체크해서 상황에 맞게 처리했다. 에러를 구체적으로 다룰 때 유용하다.


5. 재시도 로직 추가하기

에러가 나면 바로 포기하지 않고 재시도하는 전략이다:

async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error("응답 실패");
            }
            console.log("성공");
            return;
        } catch (error) {
            if (i === retries - 1) {
                console.log("최종 실패: " + error.message);
            } else {
                console.log(`재시도 ${i + 1}번`);
            }
        }
    }
}

fetchWithRetry("https://invalid.url");
// "재시도 1번"
// "재시도 2번"
// "최종 실패: Failed to fetch"

최대 3번 재시도하고, 마지막에 실패하면 에러를 출력했다. 네트워크 작업처럼 불안정한 경우에 효과적이다.


6. 에러 로깅으로 추적 강화

에러를 기록해서 나중에 분석할 수 있게 하는 전략이다:

function logError(error) {
    const log = {
        message: error.message,
        stack: error.stack,
        time: new Date()
    };
    console.log("에러 로그: ", log);
}

try {
    throw new Error("테스트 에러");
} catch (error) {
    logError(error);
    console.log("계속 진행");
} 
// "에러 로그: { message: '테스트 에러', stack: ..., time: ... }"
// "계속 진행"

에러의 메시지, 스택, 시간을 기록해서 추적성을 높였다. 디버깅이나 모니터링에 유용하다.


7. 중앙 집중식 에러 처리

에러 처리를 한 곳에서 관리하는 전략이다:

function handleError(error) {
    if (error instanceof TypeError) {
        console.log("타입 에러: " + error.message);
    } else {
        console.log("기타 에러: " + error.message);
    }
}

function process(value) {
    try {
        if (typeof value !== "number") {
            throw new TypeError("숫자가 아니다");
        }
        console.log(value * 2);
    } catch (error) {
        handleError(error);
    }
}

process("abc"); // "타입 에러: 숫자가 아니다"

handleError 함수에서 모든 에러를 중앙에서 처리했다. 코드 중복을 줄이고 일관성을 유지할 수 있다.


8. 성능과 가독성에 미치는 영향

에러 핸들링 전략이 성능과 가독성에 어떤 영향을 주는지 보자:

- 성능: 재시도나 로깅은 약간의 오버헤드를 추가하지만, 앱의 안정성을 높이는 데 큰 역할을 한다.

- 가독성: 에러를 체계적으로 처리하면 코드가 명확해지고, 문제를 빠르게 파악할 수 있다.

재시도 로직으로 안정성을 높이고, 중앙 집중식 처리로 일관성을 유지하는 점이 핵심이다.


마무리

에러 핸들링은 단순히 에러를 잡는 데 그치지 않는다. 기본값 제공, 상위 전달, 비동기 처리, 재시도 등 다양한 전략을 통해 흐름을 안정적으로 유지하고, 로깅과 중앙 처리를 더하면 코드가 더 단단해진다.


커스텀 에러 클래스 (Custom Error Classes)

커스텀 에러 클래스 (Custom Error Classes)

자바스크립트에서 에러 처리는 기본 Error 객체로 충분할 때도 있지만, 상황에 따라 더 구체적인 에러를 정의하고 싶을 때가 있다. 이때 커스텀 에러 클래스를 만들면 흐름을 더 명확하게 제어할 수 있다. 이번에는 커스텀 에러 클래스를 어떻게 만들고 활용하는지, 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


커스텀 에러를 잘 다루면 코드가 더 읽기 쉽고 안정적으로 변한다. 하나씩 단계별로 살펴보자.


기본 에러 클래스와의 차이

자바스크립트는 기본적으로 Error 객체를 제공한다. 근데 이건 단순히 메시지만 담고 있어서 구체적인 상황을 표현하기엔 좀 부족하다. 기본 사용법부터 보자:

try {
    throw new Error("뭔가 잘못됐다");
} catch (error) {
    console.log(error.name + ": " + error.message);
} // "Error: 뭔가 잘못됐다"

기본 Error는 이름이 "Error"로 고정돼 있고, 메시지만 커스터마이징할 수 있다. 이걸 확장해서 더 유용하게 만들어보자.


1. 간단한 커스텀 에러 클래스 만들기

Error를 상속받아서 커스텀 에러 클래스를 만들면 된다. 간단한 예시를 보자:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

try {
    throw new ValidationError("입력값이 잘못됐다");
} catch (error) {
    console.log(error.name + ": " + error.message);
} // "ValidationError: 입력값이 잘못됐다"

extends Error로 상속받고, super로 메시지를 전달한다. name을 바꿔서 에러의 종류를 구체적으로 나타냈다.


2. 추가 속성 넣기

커스텀 에러에 속성을 추가하면 더 많은 정보를 담을 수 있다:

class AuthError extends Error {
    constructor(message, code) {
        super(message);
        this.name = "AuthError";
        this.code = code;
    }
}

try {
    throw new AuthError("로그인 실패", 401);
} catch (error) {
    console.log(error.name + ": " + error.message + ", 코드: " + error.code);
} // "AuthError: 로그인 실패, 코드: 401"

code 속성을 추가해서 에러의 세부 정보를 담았다. 이런 식으로 에러에 맥락을 더할 수 있다.


3. 에러 타입에 따라 분기하기

커스텀 에러를 쓰면 catch에서 에러 타입을 구분해서 다룰 수 있다:

class InputError extends Error {
    constructor(message) {
        super(message);
        this.name = "InputError";
    }
}

class NetworkError extends Error {
    constructor(message) {
        super(message);
        this.name = "NetworkError";
    }
}

function process(value) {
    if (!value) {
        throw new InputError("값이 없다");
    }
    if (value === "network") {
        throw new NetworkError("네트워크 문제");
    }
    console.log("처리 완료: " + value);
}

try {
    process("");
} catch (error) {
    if (error instanceof InputError) {
        console.log("입력 문제: " + error.message);
    } else if (error instanceof NetworkError) {
        console.log("네트워크 문제: " + error.message);
    }
} // "입력 문제: 값이 없다"

instanceof로 에러 타입을 체크해서 상황에 맞게 처리했다. 흐름을 세밀하게 조정할 때 유용하다.


4. 비동기에서 커스텀 에러 활용

비동기 코드에서도 커스텀 에러를 잘 쓸 수 있다:

class FetchError extends Error {
    constructor(message, status) {
        super(message);
        this.name = "FetchError";
        this.status = status;
    }
}

async function getData() {
    try {
        const response = await fetch("https://invalid.url");
        if (!response.ok) {
            throw new FetchError("데이터 가져오기 실패", response.status);
        }
        console.log("데이터 가져오기 성공");
    } catch (error) {
        if (error instanceof FetchError) {
            console.log(error.name + ": " + error.message + ", 상태: " + error.status);
        } else {
            console.log("알 수 없는 에러: " + error.message);
        }
    }
}

getData(); // "FetchError: 데이터 가져오기 실패, 상태: undefined" (URL이 잘못된 경우)

비동기 작업에서 에러를 구체적으로 정의해서 처리했다. status 같은 속성을 추가해서 더 많은 정보를 전달할 수 있다.


5. 계층적인 에러 구조 만들기

에러를 계층적으로 설계하면 복잡한 상황도 체계적으로 다룰 수 있다:

class AppError extends Error {
    constructor(message) {
        super(message);
        this.name = "AppError";
    }
}

class DatabaseError extends AppError {
    constructor(message, query) {
        super(message);
        this.name = "DatabaseError";
        this.query = query;
    }
}

try {
    throw new DatabaseError("쿼리 실패", "SELECT * FROM users");
} catch (error) {
    if (error instanceof DatabaseError) {
        console.log(error.name + ": " + error.message + ", 쿼리: " + error.query);
    } else if (error instanceof AppError) {
        console.log("앱 에러: " + error.message);
    }
} // "DatabaseError: 쿼리 실패, 쿼리: SELECT * FROM users"

AppError를 부모로 두고 DatabaseError를 자식으로 만들어 계층을 나눴다. 이렇게 하면 에러를 체계적으로 분류할 수 있다.


6. 커스텀 에러로 흐름 제어하기

커스텀 에러를 활용하면 복잡한 로직의 흐름도 깔끔하게 정리된다:

class UserError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "UserError";
        this.field = field;
    }
}

function validateUser(user) {
    if (!user.name) {
        throw new UserError("이름이 없다", "name");
    }
    if (user.age < 18) {
        throw new UserError("미성년자다", "age");
    }
    console.log(`유저 확인: ${user.name}, ${user.age}세`);
}

try {
    validateUser({ name: "철수", age: 15 });
} catch (error) {
    console.log(error.name + ": " + error.message + ", 문제 필드: " + error.field);
} // "UserError: 미성년자다, 문제 필드: age"

field 속성을 추가해서 어떤 부분에서 문제가 생겼는지 명확히 했다. 복잡한 검증 로직을 간결하게 처리할 수 있다.


7. 커스텀 에러와 함께 디버깅 정보 제공

에러에 디버깅 정보를 추가하면 문제를 추적하기 쉬워진다:

class ProcessingError extends Error {
    constructor(message, data) {
        super(message);
        this.name = "ProcessingError";
        this.data = data;
        this.timestamp = new Date();
    }
}

try {
    const input = { value: null };
    if (!input.value) {
        throw new ProcessingError("값이 유효하지 않다", input);
    }
} catch (error) {
    console.log(error.name + ": " + error.message);
    console.log("데이터: ", error.data);
    console.log("시간: ", error.timestamp);
} 
// "ProcessingError: 값이 유효하지 않다"
// "데이터: { value: null }"
// "시간: (현재 날짜와 시간)"

datatimestamp를 추가해서 에러 발생 시점과 원인을 추적할 수 있게 했다.


8. 성능과 가독성에 미치는 영향

커스텀 에러 클래스가 성능과 가독성에 어떤 영향을 주는지 보자:

- 성능: 기본 Error와 큰 차이는 없지만, 객체 생성 오버헤드가 조금 더 생길 수 있다. 그래도 안정성과 디버깅 이점이 크다.

- 가독성: 에러를 명확히 구분하고 추가 정보를 담으면 코드가 훨씬 읽기 쉬워진다.

커스텀 속성으로 맥락을 추가하고 계층적 설계로 체계성을 높이는 점이 커스텀 에러의 강점이다.


마무리

커스텀 에러 클래스는 기본 에러를 넘어 더 구체적이고 유연한 에러 처리를 가능하게 한다. 속성 추가, 계층 설계, 비동기 활용 등 다양한 방법으로 흐름을 제어하면 코드가 더 명확해진다.


제어 흐름 예외 처리 (Control Flow with Exceptions)

제어 흐름 예외 처리 (Control Flow with Exceptions)

자바스크립트에서 예외 처리는 코드가 예상치 못한 상황에 부딪혔을 때 흐름을 조정하는 강력한 방법이다. try-catchthrow를 활용하면 에러를 잡아내고, 그에 맞춰 로직을 유연하게 다룰 수 있다. 이번에는 예외 처리를 통해 제어 흐름을 어떻게 관리할 수 있는지, 알아보려고 한다.


예외를 잘 다루면 코드가 더 안정적이고 읽기 쉽게 변한다. 단계별로 하나씩 살펴보자.


try-catch의 기본 구조

try-catch는 에러가 발생할 가능성이 있는 코드를 감싸고, 에러가 나면 그걸 잡아서 처리한다. 간단한 예시로 시작해보자:

try {
    let result = undefinedVariable;
    console.log(result);
} catch (error) {
    console.log("문제가 생겼다: " + error.message);
} // "문제가 생겼다: undefinedVariable is not defined"

try 안에서 정의되지 않은 변수를 호출하면 에러가 발생하고, catch가 그걸 잡아서 메시지를 출력한다. 이 기본 구조를 이해하면 흐름을 제어하는 첫걸음이 된다.


1. throw로 사용자 정의 에러 던지기

throw를 쓰면 특정 조건에서 의도적으로 에러를 발생시킬 수 있다. 이를 통해 흐름을 원하는 방향으로 바꿀 수 있다:

function divide(a, b) {
    if (b === 0) {
        throw new Error("0으로 나눌 수 없다!");
    }
    return a / b;
}

try {
    let result = divide(10, 0);
    console.log(result);
} catch (error) {
    console.log("에러: " + error.message);
} // "에러: 0으로 나눌 수 없다!"

0으로 나누려는 시도를 throw로 막고, catch에서 처리했다. 이렇게 하면 코드가 비정상적으로 끝나는 걸 막을 수 있다.


2. finally로 항상 실행되는 코드 추가

finally는 에러가 나든 안 나든 무조건 실행되는 블록이다. 정리 작업을 할 때 유용하다:

try {
    console.log("작업 시작");
    throw new Error("강제로 에러 발생");
} catch (error) {
    console.log("잡았다: " + error.message);
} finally {
    console.log("작업 끝, 정리 완료");
} 
// "작업 시작"
// "잡았다: 강제로 에러 발생"
// "작업 끝, 정리 완료"

finally는 에러 여부와 상관없이 마지막에 실행된다. 자원을 정리하거나 마무리 로직을 넣기에 딱 맞다.


3. 중첩된 try-catch로 세밀한 제어

try-catch를 중첩하면 에러를 더 세밀하게 다룰 수 있다:

try {
    try {
        throw new Error("내부 에러");
    } catch (innerError) {
        console.log("내부에서 잡음: " + innerError.message);
        throw new Error("외부로 전달");
    }
} catch (outerError) {
    console.log("외부에서 잡음: " + outerError.message);
} 
// "내부에서 잡음: 내부 에러"
// "외부에서 잡음: 외부로 전달"

안쪽에서 에러를 잡고, 필요하면 다시 던져서 바깥에서 처리했다. 이런 식으로 계층적으로 흐름을 조정할 수 있다.


4. 조건에 따른 흐름 분기

예외 처리를 조건문과 섞으면 흐름을 더 유연하게 만들 수 있다:

function checkValue(value) {
    try {
        if (value < 0) {
            throw new Error("음수는 안 된다");
        }
        console.log("값이 유효하다: " + value);
    } catch (error) {
        console.log("문제 발생: " + error.message);
    }
}

checkValue(5);  // "값이 유효하다: 5"
checkValue(-3); // "문제 발생: 음수는 안 된다"

조건에 따라 에러를 던지고, 그에 맞춰 흐름을 분기했다. 입력값을 검증할 때 특히 유용하다.


5. 비동기 코드에서의 예외 처리

비동기 작업에서는 try-catch를 약간 다르게 써야 한다. Promise와 함께 보자:

function fetchData() {
    return new Promise((resolve, reject) => {
        reject(new Error("데이터 가져오기 실패"));
    });
}

async function process() {
    try {
        await fetchData();
    } catch (error) {
        console.log("비동기 에러: " + error.message);
    }
}

process(); // "비동기 에러: 데이터 가져오기 실패"

async/await와 함께 쓰면 비동기 에러도 깔끔하게 잡아낸다. 비동기 흐름을 제어할 때 필수적인 방법이다.


6. 커스텀 에러 객체 활용

기본 Error 대신 커스텀 에러를 만들어 쓰면 더 구체적으로 흐름을 다룰 수 있다:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

function validate(input) {
    if (!input) {
        throw new ValidationError("입력값이 비었다");
    }
    console.log("입력값: " + input);
}

try {
    validate("");
} catch (error) {
    console.log(error.name + ": " + error.message);
} // "ValidationError: 입력값이 비었다"

커스텀 에러를 만들면 에러의 종류를 구분해서 처리하기 쉬워진다.


7. 에러를 활용한 복잡한 흐름 제어

예외를 적극적으로 활용하면 복잡한 로직도 깔끔하게 정리된다:

function processUser(user) {
    try {
        if (!user.name) {
            throw new Error("이름이 없다");
        }
        if (user.age < 18) {
            throw new Error("미성년자다");
        }
        console.log(`처리 완료: ${user.name}, ${user.age}세`);
    } catch (error) {
        console.log("처리 실패: " + error.message);
    }
}

processUser({ name: "철수", age: 20 }); // "처리 완료: 철수, 20세"
processUser({ name: "영희", age: 15 }); // "처리 실패: 미성년자다"

여러 조건을 검사하고, 문제가 생기면 바로 에러를 던져서 흐름을 중단했다. 복잡한 조건문 대신 예외로 처리하니 깔끔해졌다.


8. 성능과 가독성에 미치는 영향

예외 처리가 성능과 가독성에 어떤 영향을 주는지 보자:

- 성능: 예외 처리는 약간의 오버헤드가 있지만, 코드가 비정상적으로 끝나는 걸 막아 안정성을 높인다.

- 가독성: 긴 조건문 대신 에러를 던지고 잡으면 로직이 명확해진다.

throw로 의도적인 흐름 변경과 finally로 정리 작업을 보장하는 점이 예외 처리의 강점이다.


마무리

예외 처리는 단순히 에러를 잡는 데 그치지 않는다. try-catch, throw, finally를 통해 흐름을 세밀하게 조정하고, 비동기와 커스텀 에러까지 다루면 코드가 더 튼튼해진다. 이런 도구들을 잘 활용하면 안정적이고 깔끔한 코드를 만들 수 있다.


break와 continue 심화 (Break and Continue Deep Dive)

break와 continue 심화 (Break and Continue Deep Dive)

자바스크립트에서 breakcontinue는 반복문이나 조건문을 다룰 때 흐름을 조정하는 데 유용한 도구다. 단순히 루프를 멈추거나 건너뛰는 수준을 넘어, 조금 더 깊이 들어가면 코드를 훨씬 유연하고 효율적으로 만들 수 있다. 이번에는 breakcontinue를 기본부터 심화까지 코드와 함께 자세히 풀어볼게.


이 두 도구를 잘 다루면 복잡한 로직도 깔끔하게 정리된다. 하나씩 단계별로 살펴보자.


break와 continue의 기본 역할

먼저 breakcontinue가 뭘 하는지 간단히 짚고 넘어가자. break는 반복문을 즉시 끝내고 밖으로 빠져나오게 한다. 반면 continue는 현재 반복을 스킵하고 다음 반복으로 넘어간다. 기본적인 코드를 먼저 보자:

for (let i = 0; i < 5; i++) {
    if (i === 3) {
        break;
    }
    console.log(i);
} // 0, 1, 2

여기서 i가 3이 되면 break가 루프를 끝낸다. 다음으로 continue를 보자:

for (let i = 0; i < 5; i++) {
    if (i === 3) {
        continue;
    }
    console.log(i);
} // 0, 1, 2, 4

continue는 3을 건너뛰고 다음으로 넘어간다. 기본적인 차이를 확인했으니, 이제 더 깊은 활용법으로 들어가보자.


1. 중첩 루프에서 break 다루기

break는 기본적으로 자신이 속한 가장 가까운 루프만 종료한다. 중첩된 루프에서 어떻게 움직이는지 확인해보자:

for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (j === 1) {
            break;
        }
        console.log(`i: ${i}, j: ${j}`);
    }
} // i: 0, j: 0 / i: 1, j: 0 / i: 2, j: 0

안쪽 루프에서 break가 걸리면 안쪽만 끝나고 바깥 루프는 계속 돈다. 근데 때로는 바깥 루프까지 한 번에 멈추고 싶을 때가 있다. 그럴 땐 레이블(label)을 활용해보자.

outerLoop: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (j === 1) {
            break outerLoop;
        }
        console.log(`i: ${i}, j: ${j}`);
    }
} // i: 0, j: 0

outerLoop라는 레이블을 붙이고 break outerLoop를 쓰면 바깥 루프까지 한 번에 끝난다. 이런 방식은 흐름을 세밀하게 조정할 때 꽤 유용하다.


2. 중첩 루프에서 continue 활용

continue도 중첩 루프와 함께 쓰면 재밌는 흐름을 만들어낸다. 기본 동작부터 확인해보자:

for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (j === 1) {
            continue;
        }
        console.log(`i: ${i}, j: ${j}`);
    }
} // i: 0, j: 0 / i: 0, j: 2 / i: 1, j: 0 / i: 1, j: 2 / i: 2, j: 0 / i: 2, j: 2

안쪽 루프에서 j가 1일 때 스킵되니까 1은 출력되지 않는다. 근데 바깥 루프의 다음 반복으로 바로 넘어가고 싶다면? 레이블을 써보자:

outerLoop: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (j === 1) {
            continue outerLoop;
        }
        console.log(`i: ${i}, j: ${j}`);
    }
} // i: 0, j: 0 / i: 1, j: 0 / i: 2, j: 0

continue outerLoop를 쓰면 안쪽 루프의 나머지를 건너뛰고 바깥 루프의 다음 반복으로 바로 넘어간다. 이런 식으로 흐름을 더 세밀하게 다룰 수 있다.


3. switch 문과 조합하기

breakswitch 문에서도 자주 등장한다. 각 케이스를 끝낼 때 쓰이는데, 이걸 반복문과 섞으면 흥미로운 결과가 나온다:

for (let i = 0; i < 5; i++) {
    switch (i) {
        case 2:
            console.log("2다! 스킵하자");
            continue;
        case 4:
            console.log("4다! 멈춰");
            break;
    }
    console.log(i);
} // 0, 1, "2다! 스킵하자", 3, "4다! 멈춰"

continue는 루프의 다음 반복으로 넘어가고, break는 루프 전체를 끝낸다. switch와 함께 쓰면 조건에 따라 흐름을 자유롭게 바꿀 수 있다.


4. while 루프와 함께 쓰기

for뿐만 아니라 while에서도 breakcontinue를 활용할 수 있다:

let count = 0;
while (count < 10) {
    count++;
    if (count % 2 === 0) {
        continue;
    }
    if (count > 7) {
        break;
    }
    console.log(count);
} // 1, 3, 5, 7

짝수는 continue로 건너뛰고, 7을 넘으면 break로 끝낸다. while과 함께 쓰면 조건을 더 유연하게 다룰 수 있다.


5. 배열 순회에서 활용하기

배열을 다룰 때도 breakcontinue가 꽤 유용하다. 특정 조건에서 멈추거나 건너뛸 수 있으니까:

const numbers = [1, 2, 3, 4, 5];
for (const num of numbers) {
    if (num === 3) {
        break;
    }
    console.log(num);
} // 1, 2

3을 만나면 바로 멈춘다. 이렇게 하면 배열에서 원하는 지점까지만 순회할 수 있다. 이번엔 continue를 써보자:

const items = ["사과", "바나나", "포도", "오렌지"];
for (const item of items) {
    if (item === "바나나") {
        continue;
    }
    console.log(item);
} // "사과", "포도", "오렌지"

"바나나"는 건너뛰고 나머지만 출력된다. 배열 순회에서 조건에 따라 필터링하는 느낌으로 쓸 수 있다.


6. 함수와 결합해서 흐름 제어하기

함수와 함께 쓰면 더 강력한 제어가 가능하다. 함수 안에서 breakcontinue를 활용해보자:

function processNumbers(limit) {
    for (let i = 0; i < 10; i++) {
        if (i > limit) {
            break;
        }
        if (i % 2 === 0) {
            continue;
        }
        console.log(i);
    }
}

processNumbers(5); // 1, 3, 5

함수에 전달된 limit에 따라 루프를 멈추고, 짝수는 건너뛴다. 이렇게 하면 로직을 함수로 묶으면서도 흐름을 자유롭게 다룰 수 있다.


7. 복잡한 조건에서 흐름 정리하기

조건이 복잡해질 때 breakcontinue를 잘 쓰면 코드가 깔끔해진다. 데이터 검색 로직을 예로 들어보자:

const data = [
    { id: 1, name: "철수", active: true },
    { id: 2, name: "영희", active: false },
    { id: 3, name: "민수", active: true }
];

for (const entry of data) {
    if (!entry.active) {
        continue;
    }
    if (entry.id === 3) {
        console.log(`찾았다: ${entry.name}`);
        break;
    }
} // "찾았다: 민수"

활성 상태가 아닌 건 건너뛰고, id가 3인 걸 찾으면 바로 멈춘다. 조건을 걸러내고 원하는 지점에서 끝내는 데 유용하다.


8. 성능과 가독성에 미치는 영향

breakcontinue를 쓰면 성능과 가독성에 어떤 변화가 생길까?

- 성능: 불필요한 반복을 줄여 속도를 높인다. 특히 큰 데이터에서 break로 조기 종료하면 효과가 크다.

- 가독성: 조건문이 많아질 때 if-else로만 다루면 복잡해지는데, continue로 스킵하거나 break로 끝내면 코드가 간결해진다.

레이블을 활용한 중첩 제어와 복잡한 조건에서의 흐름 정리는 이 두 도구의 강점을 잘 보여준다.


마무리

breakcontinue는 단순히 루프를 멈추거나 건너뛰는 데 그치지 않는다. 중첩 루프, 레이블, 배열 순회, 함수와의 조합 등 다양한 상황에서 코드를 더 유연하고 깔끔하게 만들어준다.


스위치 문 활용 (Advanced Switch Statement Usage)

스위치 문 활용 (Advanced Switch Statement Usage)

자바스크립트에서 스위치 문(switch statement)은 다중 조건을 처리할 때 유용한 제어 구조다. 단순히 조건을 나열하는 것을 넘어 다양한 방식으로 활용하면 코드의 가독성과 효율성을 높일 수 있다. 스위치 문의 기본부터 고급 활용법까지 코드와 함께 자세히 알아보자.


스위치 문을 잘 다루면 복잡한 로직도 깔끔하게 정리할 수 있다. 기본 동작부터 심화된 사용법까지 단계별로 살펴보자.


스위치 문의 기본 구조

switch는 값을 비교해 해당 케이스를 실행한다. 기본적인 코드를 보자:

const day = 3;
switch (day) {
    case 1:
        console.log("월요일");
        break;
    case 2:
        console.log("화요일");
        break;
    case 3:
        console.log("수요일");
        break;
    default:
        console.log("알 수 없는 요일");
} // "수요일"

break로 각 케이스를 종료하며, default는 일치하는 경우가 없을 때 실행된다. 이제 더 나은 활용법을 찾아보자.


1. Fall-Through 기법

break를 생략하면 여러 케이스가 연속적으로 실행된다. 이를 활용한 코드를 보자:

const month = 4;
let season;
switch (month) {
    case 12:
    case 1:
    case 2:
        season = "겨울";
        break;
    case 3:
    case 4:
    case 5:
        season = "봄";
        break;
    default:
        season = "알 수 없음";
}
console.log(season); // "봄"

Fall-through는 비슷한 조건을 묶을 때 유용하다.


2. 표현식 사용

switch의 조건에 단순 값뿐 아니라 표현식을 넣을 수 있다:

const score = 85;
switch (true) {
    case score >= 90:
        console.log("A");
        break;
    case score >= 80:
        console.log("B");
        break;
    case score >= 70:
        console.log("C");
        break;
    default:
        console.log("F");
} // "B"

true를 사용하면 범위 조건도 처리할 수 있다.


3. 복잡한 로직 처리

각 케이스 안에 복잡한 로직을 넣을 수 있다:

const command = "start";
switch (command) {
    case "start": {
        const time = Date.now();
        console.log(`시작 시간: ${time}`);
        break;
    }
    case "stop": {
        let status = "정지";
        console.log(status);
        break;
    }
    default:
        console.log("명령어 없음");
}

블록 스코프({})를 사용하면 변수 충돌을 방지한다.


4. 동적 케이스 활용

케이스를 동적으로 계산하거나 조합할 수 있다:

const input = "A";
const key = input.toLowerCase();
switch (key) {
    case "a":
        console.log("소문자 a");
        break;
    case "b":
        console.log("소문자 b");
        break;
    default:
        console.log("다른 문자");
} // "소문자 a"

입력값을 가공해 동적으로 매핑할 수 있다.


5. 중첩 스위치 문

스위치 문 안에 또 다른 스위치 문을 사용할 수 있다:

const category = "fruit";
const item = "apple";
switch (category) {
    case "fruit":
        switch (item) {
            case "apple":
                console.log("사과");
                break;
            case "banana":
                console.log("바나나");
                break;
        }
        break;
    case "vegetable":
        console.log("채소");
        break;
} // "사과"

중첩을 통해 계층적인 조건을 처리할 수 있다.


6. 함수와 결합

스위치 문을 함수 호출과 결합하면 로직을 모듈화할 수 있다:

function handleAction(type) {
    switch (type) {
        case "log":
            console.log("로그 출력");
            break;
        case "alert":
            alert("경고");
            break;
        default:
            console.log("기본 동작");
    }
}

handleAction("log"); // "로그 출력"

함수로 분리하면 재사용성이 높아진다.


7. 객체와 비교

스위치 문과 객체 맵을 비교하며 활용 방법을 보자:

// 스위치 문
function getColor(code) {
    switch (code) {
        case 1:
            return "빨강";
        case 2:
            return "파랑";
        default:
            return "검정";
    }
}

// 객체 맵
const colors = {
    1: "빨강",
    2: "파랑"
};
function getColorMap(code) {
    return colors[code] || "검정";
}

console.log(getColor(1)); // "빨강"
console.log(getColorMap(2)); // "파랑"

스위치 문은 복잡한 로직에 적합하고, 객체는 간단한 매핑에 유리하다.


8. 다양한 상황에서의 활용

스위치 문을 여러 상황에 적용한 코드를 살펴보자.


1. 상태 처리

const status = "loading";
switch (status) {
    case "loading":
        console.log("로딩 중...");
        break;
    case "success":
        console.log("완료");
        break;
    case "error":
        console.log("오류 발생");
        break;
} // "로딩 중..."

2. 사용자 입력 처리

const choice = 2;
switch (choice) {
    case 1:
        console.log("옵션 1 선택");
        break;
    case 2:
        console.log("옵션 2 선택");
        break;
    default:
        console.log("잘못된 선택");
} // "옵션 2 선택"

성능과 가독성에 미치는 영향

스위치 문은 성능과 가독성에 다음과 같은 영향을 준다:

- 성능: 다중 조건을 빠르게 비교하며, V8 엔진은 이를 효율적으로 최적화한다.

- 가독성: 조건이 명확히 분리되어 긴 if-else보다 읽기 쉬울 수 있다.

Fall-Through표현식 사용은 스위치 문의 강력한 활용법으로 주목할 만하다.


마무리

스위치 문은 자바스크립트에서 다중 조건을 다룰 때 강력한 도구다. Fall-Through, 표현식, 중첩 등 다양한 방법을 코드와 함께 다뤘으니, 이를 활용해 깔끔하고 효율적인 코드를 작성해보자.


루프 최적화 (Loop Optimization)

루프 최적화 (Loop Optimization)

자바스크립트에서 루프는 데이터를 반복적으로 처리할 때 필수적인 도구다. 하지만 루프가 비효율적이면 성능 저하로 이어질 수 있다. 루프 최적화의 개념과 다양한 기법을 코드와 함께 알아보며, 성능을 높이는 방법을 단계별로 다뤄보자.


루프를 잘 조정하면 실행 속도를 개선하고 메모리 사용을 줄일 수 있다. 기본부터 심화된 접근까지 살펴보자.


루프의 기본 동작

자바스크립트에서 루프는 for, while, forEach 등으로 구현된다. 기본적인 for 루프를 보자:

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
} // 1, 2, 3

이 구조는 단순하지만 대규모 데이터나 복잡한 연산에서 비효율적일 수 있다. 최적화 방법을 찾아보자.


1. 길이 캐싱(Length Caching)

루프에서 배열의 length를 매번 계산하면 오버헤드가 생긴다. 이를 변수에 저장하는 방법을 비교해보자:

const arr = new Array(1000000).fill(0);

// 비효율적인 방식
for (let i = 0; i < arr.length; i++) {
    arr[i] += 1; // length를 반복마다 계산
}

// 최적화된 방식
const len = arr.length;
for (let i = 0; i < len; i++) {
    arr[i] += 1; // length 계산 제거
}

length를 캐싱하면 V8 엔진이 더 효율적으로 동작하며 속도가 빨라진다.


2. 역방향 루프(Reverse Loop)

루프를 뒤에서 앞으로 돌리면 조건 검사를 단순화할 수 있다:

// 일반적인 순방향
for (let i = 0; i < arr.length; i++) {
    arr[i] *= 2;
}

// 역방향 루프
for (let i = arr.length - 1; i >= 0; i--) {
    arr[i] *= 2;
}

역방향은 i >= 0만 확인하면 되므로 약간의 성능 향상이 가능하다.


3. 불필요한 연산 제거

루프 안에서 반복적으로 계산되는 값을 밖으로 빼면 효율이 높아진다:

// 비효율적인 연산
for (let i = 0; i < arr.length; i++) {
    arr[i] = i * Math.random(); // 매번 호출
}

// 최적화된 연산
const randomFactor = Math.random();
for (let i = 0, len = arr.length; i < len; i++) {
    arr[i] = i * randomFactor; // 한 번만 호출
}

루프 밖으로 빼낼 수 있는 계산은 미리 처리하는 것이 좋다.


4. 루프 분리(Loop Splitting)

복잡한 루프를 여러 개로 나누면 가독성과 성능을 개선할 수 있다:

// 단일 복잡한 루프
for (let i = 0; i < arr.length; i++) {
    arr[i] += 1;
    if (arr[i] % 2 === 0) {
        arr[i] *= 2;
    }
}

// 분리된 루프
const len = arr.length;
for (let i = 0; i < len; i++) {
    arr[i] += 1;
}
for (let i = 0; i < len; i++) {
    if (arr[i] % 2 === 0) {
        arr[i] *= 2;
    }
}

분리하면 각 루프가 단순해지고 엔진 최적화가 쉬워질 수 있다.


5. 조건문 최소화

루프 내 조건문을 줄이면 실행 속도가 빨라진다:

// 조건문이 많은 경우
for (let i = 0; i < arr.length; i++) {
    if (arr[i] > 0) {
        arr[i] += 10;
    }
}

// 조건문 제거
const positives = arr.filter(n => n > 0);
for (let i = 0, len = positives.length; i < len; i++) {
    positives[i] += 10;
}

필터링 후 루프를 돌리면 조건문 없이 작업을 처리할 수 있다.


6. forEach와 for 비교

forEach는 편리하지만 성능 면에서 for에 비해 느릴 수 있다:

// forEach 사용
arr.forEach((item, i) => {
    arr[i] += 1;
});

// for 사용
for (let i = 0, len = arr.length; i < len; i++) {
    arr[i] += 1;
}

forEach는 콜백 호출로 인해 오버헤드가 발생하므로, 성능이 중요한 경우 for를 고려하자.


7. While 루프 활용

while은 조건이 유동적인 경우 유리하다:

// for 사용
for (let i = 0; i < arr.length && arr[i] < 100; i++) {
    arr[i] += 5;
}

// while 사용
let i = 0;
while (i < arr.length && arr[i] < 100) {
    arr[i] += 5;
    i++;
}

while은 조건을 더 유연하게 조정할 수 있다.


8. 다양한 상황에서의 활용

루프 최적화를 여러 상황에 적용한 코드를 보자.


1. 데이터 변환

const data = new Array(1000000).fill(0);
const len = data.length;
const factor = 2;
for (let i = 0; i < len; i++) {
    data[i] = i * factor;
}

2. 조건부 처리

const numbers = [1, 2, 3, 4, 5];
let i = 0;
while (i < numbers.length && numbers[i] < 4) {
    numbers[i] *= 10;
    i++;
}
console.log(numbers); // [10, 20, 30, 4, 5]

성능 측정과 비교

performance.now()로 최적화 전후를 확인해보자:

const arr = new Array(1000000).fill(0);
let start = performance.now();
for (let i = 0; i < arr.length; i++) {
    arr[i] += 1;
}
console.log("비최적화:", performance.now() - start);

start = performance.now();
const len = arr.length;
for (let i = 0; i < len; i++) {
    arr[i] += 1;
}
console.log("최적화:", performance.now() - start);

최적화된 루프가 일반적으로 더 빠른 결과를 보여준다.


마무리

루프 최적화는 자바스크립트에서 성능을 높이는 데 중요한 기술이다. 길이 캐싱, 역방향 루프, 조건문 최소화 등 다양한 방법을 코드와 함께 다뤘으니, 이를 적용해 효율적인 코드를 작성해보자.


+ Recent posts