비동기 에러 처리 (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);

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


마무리

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


고급 조건문 패턴 (Advanced Conditional Patterns)

고급 조건문 패턴 (Advanced Conditional Patterns)

자바스크립트에서 조건문은 프로그램 흐름을 제어하는 데 핵심적인 역할을 한다. 단순한 if-else 구조를 넘어 고급 패턴을 사용하면 코드의 가독성과 유지보수성을 높이고 성능을 개선할 수 있다. 고급 조건문 패턴의 개념과 다양한 접근법을 코드와 함께 단계적으로 알아보자.


조건문을 잘 활용하면 복잡한 로직도 간결하고 명확하게 정리된다. 기본부터 심화된 기법까지 차근차근 다뤄보자.


조건문의 기본 구조

자바스크립트에서 조건문은 주로 if, else if, else로 구성된다. 다음 코드를 보자:

const score = 85;
if (score >= 90) {
 console.log("A");
} else if (score >= 80) {
 console.log("B");
} else {
 console.log("C");
} // "B"

이 방식은 직관적이지만 조건이 늘어나면 복잡해질 수 있다. 더 나은 방법을 찾아보자.


1. Early Return 패턴

Early Return은 조건이 충족되면 즉시 함수를 끝내며 불필요한 중첩을 줄이는 방식이다. 두 가지 접근을 비교해보자:

// 중첩된 구조
function checkUser(user) {
 if (user) {
 if (user.age >= 18) {
 console.log("성인입니다.");
 } else {
 console.log("미성년자입니다.");
 }
 } else {
 console.log("사용자가 없습니다.");
 }
}

// Early Return 사용
function checkUserOptimized(user) {
 if (!user) return console.log("사용자가 없습니다.");
 if (user.age < 18) return console.log("미성년자입니다.");
 console.log("성인입니다.");
}

checkUserOptimized({ "age": 20 }); // "성인입니다."

Early Return은 코드의 깊이를 줄여 이해하기 쉽게 만들고 불필요한 검사를 피한다.


2. 객체 맵(Object Map) 패턴

조건이 많을 때 if-else 대신 객체를 사용하면 로직이 간단해진다. 다음 코드를 보자:

// 긴 조건 분기
function getGrade(score) {
 if (score >= 90) return "A";
 else if (score >= 80) return "B";
 else if (score >= 70) return "C";
 else if (score >= 60) return "D";
 else return "F";
}

// 객체 맵 적용
const gradeMap = {
 90: "A",
 80: "B",
 70: "C",
 60: "D",
 0: "F"
};
function getGradeOptimized(score) {
 for (const threshold in gradeMap) {
 if (score >= threshold) return gradeMap[threshold];
 }
}

console.log(getGradeOptimized(85)); // "B"

객체 맵은 조건을 데이터로 바꿔 관리하기 편리하게 만든다.


3. 삼항 연산자와 간소화

간단한 조건은 삼항 연산자(?:)로 줄일 수 있다. 다음 코드를 보자:

// 일반적인 분기
let message;
if (age >= 18) {
 message = "성인";
} else {
 message = "미성년자";
}

// 삼항 연산자 사용
const age = 20;
const messageOptimized = age >= 18 ? "성인" : "미성년자";
console.log(messageOptimized); // "성인"

삼항 연산자는 코드 길이를 줄이지만, 지나치게 중첩하면 읽기 어려울 수 있으니 적절히 사용하자.


4. 논리 연산자 활용

&&||를 사용하면 조건문을 더 간결하게 표현할 수 있다. 다음 코드를 보자:

// 일반적인 조건
function greet(name) {
 if (name) {
 console.log(`안녕, ${name}!`);
 } else {
 console.log("안녕, 손님!");
 }
}

// 논리 연산자 적용
function greetOptimized(name) {
 console.log(`안녕, ${name || "손님"}!`);
}

greetOptimized("철수"); // "안녕, 철수!"
greetOptimized(); // "안녕, 손님!"

||는 기본값을 설정하는 데 유용하고, &&는 조건에 따른 동작을 간소화한다:

const user = { "active": true };
user.active && console.log("활성 사용자"); // "활성 사용자"

5. Switch 대체 패턴

switch 대신 객체 또는 함수 맵 으로 로직을 구성할 수 있다. 다음 코드를 보자:

// switch 사용
function getAction(command) {
 switch (command) {
 case "start":
 return "시작";
 case "stop":
 return "정지";
 default:
 return "알 수 없음";
 }
}

// 함수 맵 사용
const actions = {
 "start": () => "시작",
 "stop": () => "정지",
 default: () => "알 수 없음"
};
function getActionOptimized(command) {
 return (actions[command] || actions.default)();
}

console.log(getActionOptimized("start")); // "시작"

함수 맵은 동적인 로직을 추가하기 쉬워 유연성이 뛰어나다.


6. 패턴 매칭 스타일

자바스크립트는 공식 패턴 매칭을 지원하지 않지만, 객체 분해로 비슷한 효과를 낼 수 있다:

function describeUser({ age, name } = {}) {
 return age >= 18 && name
 ? `성인 ${name}`
 : age < 18 && name
 ? `미성년자 ${name}`
 : "정보 없음";
}

console.log(describeUser({ age: 20, name: "영희" })); // "성인 영희"

구조 분해를 활용하면 조건과 데이터를 깔끔하게 연결할 수 있다.


7. 상태 기반 조건문

상태 객체를 사용해 조건문을 체계적으로 관리할 수 있다:

const state = {
 "isLoggedIn": true,
 "isAdmin": false,
 "hasPermission": true
};

function checkAccess(state) {
 if (!state.isLoggedIn) return "로그인 필요";
 if (state.isAdmin) return "관리자 접근 허용";
 if (state.hasPermission) return "일반 접근 허용";
 return "접근 불가";
}

console.log(checkAccess(state)); // "일반 접근 허용"

상태 기반 접근은 복잡한 조건을 구조적으로 정리한다.


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

고급 조건문 패턴을 여러 상황에 적용한 코드를 살펴보자.


1. 사용자 권한 확인

const roles = {
 "admin": () => "모든 권한",
 "editor": () => "편집 권한",
 "viewer": () => "보기 권한",
 default: () => "권한 없음"
};

function getPermission(user) {
 if (!user || !user.role) return roles.default();
 return (roles[user.role] || roles.default)();
}

console.log(getPermission({ role: "editor" })); // "편집 권한"

2. 상태에 따른 출력

const renderContent = (status) => {
 const contentMap = {
 "loading": "로딩 중...",
 "success": "데이터 로드 완료",
 "error": "오류 발생"
 };
 return contentMap[status] || "알 수 없는 상태";
};

console.log(renderContent("success")); // "데이터 로드 완료"

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

고급 패턴은 코드의 성능과 가독성에 영향을 준다:

- 성능: 객체 맵은 O(1) 조회 속도를 제공하며, 긴 if-elseswitch보다 효율적일 수 있다.

- 가독성: 중첩을 줄이고 로직을 데이터로 변환하면 코드를 더 쉽게 파악할 수 있다.

Early Return객체 맵은 특히 자주 활용되는 패턴으로 주목할 만하다.


마무리

고급 조건문 패턴은 자바스크립트에서 코드 품질을 높이는 데 중요한 도구다. Early Return, 객체 맵, 논리 연산자 등 다양한 방법을 코드와 함께 다뤘으니, 이를 활용해 더 나은 코드를 작성해보자.


+ Recent posts