비동기 에러 처리 (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를 통해 점점 더 간결해졌다. 병렬과 순차 처리, 재시도, 로깅까지 결합하면 비동기 작업이 더 안정적이고 관리하기 쉬워진다.


+ Recent posts