이벤트 루프와 태스크 스케줄링 (Event Loop and Task Scheduling)

이벤트 루프와 태스크 스케줄링 (Event Loop and Task Scheduling)

자바스크립트에서 비동기 작업이 어떻게 실행되는지 이해하려면 이벤트 루프태스크 스케줄링을 알아야 한다. 이 두 개념이 자바스크립트 엔진의 핵심 동작을 좌우한다. 이번에는 이벤트 루프의 기본부터 태스크와 마이크로태스크의 스케줄링까지 코드와 함께 깊이 파고들어보려고 한다.


단계별로 하나씩 뜯어보면서 자바스크립트의 비동기 흐름을 명확히 파악해보자.


이벤트 루프 기본

이벤트 루프는 자바스크립트가 단일 스레드 환경에서 비동기 작업을 처리할 수 있게 해준다. 간단한 동작부터 보자:

console.log("시작");
setTimeout(() => {
    console.log("타이머");
}, 1000);
console.log("끝");
// "시작"
// "끝"
// 1초 후 "타이머"

setTimeout은 비동기 작업으로 태스크 큐에 들어가고, 이벤트 루프가 이를 나중에 실행한다. 호출 스택이 비면 태스크 큐를 확인해서 실행한다.


1. 호출 스택과 태스크 큐

호출 스택과 태스크 큐의 관계를 이해해야 한다:

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
// "1"
// "3"
// "2"

여기서 setTimeout의 콜백은 태스크 큐로 이동하고, 호출 스택이 비워진 후에 이벤트 루프가 태스크 큐에서 꺼내 실행한다. 0초라도 바로 실행되지 않는다.


2. 마이크로태스크와의 차이

태스크 외에 마이크로태스크라는 게 있다. Promise는 마이크로태스크 큐로 간다:

console.log("시작");
setTimeout(() => console.log("타이머"), 0);
Promise.resolve("프로미스").then(value => console.log(value));
console.log("끝");
// "시작"
// "끝"
// "프로미스"
// "타이머"

마이크로태스크는 호출 스택이 비면 바로 실행되고, 태스크는 그 다음에 처리된다. 이벤트 루프는 먼저 마이크로태스크 큐를 비우고 나서 태스크 큐로 넘어간다.


3. 복잡한 태스크 스케줄링

여러 비동기 작업이 얽히면 어떻게 될까:

console.log("1");
setTimeout(() => {
    console.log("2");
    Promise.resolve("3").then(value => console.log(value));
}, 0);
setTimeout(() => console.log("4"), 0);
Promise.resolve("5").then(value => console.log(value));
console.log("6");
// "1"
// "6"
// "5"
// "2"
// "3"
// "4"

호출 스택이 비면 마이크로태스크("5")가 먼저 실행되고, 그 다음 태스크 큐에서 순서대로 "2", "4"가 처리된다. "2" 안의 프로미스는 다시 마이크로태스크로 들어가 "3"을 출력한다.


4. 태스크 지연 이해

태스크가 지연되는 상황을 보자:

function block() {
    let start = Date.now();
    while (Date.now() - start < 2000);
}

console.log("시작");
setTimeout(() => console.log("타이머"), 0);
block();
console.log("끝");
// "시작"
// 약 2초 후 "끝"
// 바로 "타이머"

호출 스택이 block으로 막히면 태스크 큐의 "타이머"는 스택이 비워질 때까지 기다린다. 이벤트 루프는 스택이 비어야 동작한다.


5. 마이크로태스크 과부하

마이크로태스크가 쌓이면 어떻게 될까:

Promise.resolve()
    .then(() => console.log("1"))
    .then(() => console.log("2"))
    .then(() => console.log("3"));
console.log("4");
// "4"
// "1"
// "2"
// "3"

마이크로태스크는 한 번에 모두 처리된다. 호출 스택이 비면 마이크로태스크 큐를 끝까지 비운다.


6. 브라우저 렌더링과의 관계

이벤트 루프는 렌더링과도 연결된다:

function heavyTask() {
    let i = 0;
    while (i < 10000000) i++;
    console.log("무거운 작업 끝");
}

setTimeout(() => console.log("타이머"), 0);
Promise.resolve().then(() => console.log("프로미스"));
heavyTask();
// 약간의 지연 후 "무거운 작업 끝"
// "프로미스"
// "타이머"

무거운 작업이 스택을 막으면 렌더링도 지연된다. 브라우저는 이벤트 루프 한 사이클이 끝나야 화면을 갱신한다.


7. 태스크 분할로 부하 줄이기

긴 작업을 쪼개서 태스크로 나눌 수 있다:

function splitTask(total, chunkSize) {
    let count = 0;
    function step() {
        let start = count;
        while (count - start < chunkSize && count < total) {
            count++;
        }
        console.log(`진행: ${count}/${total}`);
        if (count < total) {
            setTimeout(step, 0);
        }
    }
    step();
}

splitTask(100, 20);
// "진행: 20/100"
// "진행: 40/100"
// "진행: 60/100"
// "진행: 80/100"
// "진행: 100/100"

작업을 태스크로 나눠 이벤트 루프가 다른 일을 처리할 틈을 준다.


8. 실제 API 호출 스케줄링

API 호출을 스케줄링해보자:

async function fetchData(id) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
    const data = await response.json();
    return data.title;
}

console.log("시작");
setTimeout(() => console.log("타이머"), 0);
fetchData(1).then(title => console.log("API: " + title));
console.log("끝");
// "시작"
// "끝"
// "API: sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
// "타이머"

API 호출은 마이크로태스크로 처리되고, setTimeout은 태스크로 나중에 실행된다.


9. 이벤트 루프의 한계

이벤트 루프에도 한계가 있다:

function infiniteMicrotasks() {
    Promise.resolve()
        .then(() => {
            console.log("마이크로태스크");
            infiniteMicrotasks();
        });
}

infiniteMicrotasks();
setTimeout(() => console.log("타이머"), 0);
// "마이크로태스크" 무한 출력
// "타이머"는 절대 실행되지 않음

마이크로태스크가 끝없이 쌓이면 태스크 큐로 넘어갈 기회가 없다. 이벤트 루프가 멈춘다.


10. 성능과 흐름 제어

이벤트 루프가 코드에 미치는 영향을 보자:

- 성능: 태스크와 마이크로태스크를 잘 분배하면 부하를 줄일 수 있다.

- 흐름: 실행 순서를 예측하고 제어할 수 있다.

태스크마이크로태스크의 우선순위를 이해하는 게 핵심이다.


마무리

이벤트 루프와 태스크 스케줄링은 자바스크립트의 비동기 동작을 이해하는 열쇠다. 호출 스택, 태스크 큐, 마이크로태스크 큐가 어떻게 조화를 이루는지 알면 코드 흐름을 더 잘 다룰 수 있다.


비동기 패턴 심화 (Advanced Async Patterns)

비동기 패턴 심화 (Advanced Async Patterns)

비동기 프로그래밍을 다루다 보면 단순한 Promiseasync/await로는 해결하기 까다로운 상황이 생긴다. 이때 비동기 패턴을 활용하면 복잡한 흐름을 깔끔하게 정리할 수 있다. 이번엔 비동기 작업을 다루는 다양한 패턴을 코드와 함께 단계적으로 풀어보려고 한다.


기본적인 사용법부터 시작해서 점점 복잡한 상황까지 다뤄보며, 비동기 코드를 더 유연하고 강력하게 만드는 법을 알아보자.


비동기 패턴의 기본 흐름

비동기 패턴은 작업의 순서나 의존성을 조정하는 데 유용하다. 먼저 간단한 순차 실행부터 보자:

function delay(ms, value) {
    return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

async function sequentialFlow() {
    const a = await delay(1000, "A 완료");
    console.log(a);
    const b = await delay(1000, "B 완료");
    console.log(b);
    const c = await delay(1000, "C 완료");
    console.log(c);
}

sequentialFlow();
// 1초 후 "A 완료"
// 2초 후 "B 완료"
// 3초 후 "C 완료"

각 작업이 순차적으로 끝날 때까지 기다리며 진행했다. 단순하지만 의존성이 있을 때 유용하다.


1. 병렬 실행 패턴

순차 실행이 필요 없는 경우엔 병렬로 처리해서 시간을 줄일 수 있다:

async function parallelFlow() {
    const promises = [
        delay(1000, "A 완료"),
        delay(1500, "B 완료"),
        delay(500, "C 완료")
    ];
    const results = await Promise.all(promises);
    results.forEach(result => console.log(result));
}

parallelFlow();
// 1.5초 후:
// "A 완료"
// "B 완료"
// "C 완료"

Promise.all로 모든 작업을 동시에 시작하고, 가장 오래 걸리는 작업이 끝날 때까지 기다렸다. 순서가 중요하지 않을 때 효율적이다.


2. 경쟁 조건 패턴 (Race)

여러 작업 중 가장 먼저 끝나는 걸 사용하고 싶다면 Promise.race를 활용할 수 있다:

async function raceFlow() {
    const promises = [
        delay(2000, "느린 서버"),
        delay(1000, "빠른 서버"),
        delay(1500, "중간 서버")
    ];
    const winner = await Promise.race(promises);
    console.log("가장 빠른 결과: " + winner);
}

raceFlow();
// 1초 후 "가장 빠른 결과: 빠른 서버"

가장 빠른 응답을 받아서 바로 처리했다. 서버 요청처럼 속도가 중요한 경우에 유용하다.


3. 순차 병렬 혼합 패턴

일부는 순차로, 일부는 병렬로 처리해야 할 때도 있다:

async function mixedFlow() {
    const step1 = await delay(1000, "첫 단계");
    console.log(step1);

    const parallelSteps = await Promise.all([
        delay(1000, "병렬 A"),
        delay(1000, "병렬 B")
    ]);
    parallelSteps.forEach(step => console.log(step));

    const step3 = await delay(1000, "마지막 단계");
    console.log(step3);
}

mixedFlow();
// 1초 후 "첫 단계"
// 2초 후 "병렬 A"
// "병렬 B"
// 3초 후 "마지막 단계"

첫 단계가 끝난 뒤 병렬로 두 작업을 처리하고, 그 다음 마지막 단계를 기다렸다. 흐름을 세밀하게 조정할 수 있다.


4. 재시도 패턴 (Retry)

작업이 실패하면 자동으로 재시도하는 패턴도 자주 쓰인다:

async function unstableTask() {
    if (Math.random() > 0.3) {
        throw new Error("실패");
    }
    return "성공";
}

async function retry(task, maxAttempts) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return await task();
        } catch (error) {
            console.log(`시도 ${attempt} 실패: ` + error.message);
            if (attempt === maxAttempts) throw error;
            await delay(1000, "재시도 대기");
        }
    }
}

async function run() {
    const result = await retry(unstableTask, 3);
    console.log(result);
}

run();
// "시도 1 실패: 실패" (랜덤)
// 1초 후 "시도 2 실패: 실패" (랜덤)
// 1초 후 "성공" (랜덤)

최대 3번까지 재시도하며 실패하면 에러를 던지고, 성공하면 바로 결과를 반환했다. 불안정한 네트워크 상황에서 빛을 발한다.


5. 타임아웃 패턴

작업이 너무 오래 걸리면 강제로 끝낼 수도 있다:

async function timeout(promise, ms) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("시간 초과")), ms);
    });
    return Promise.race([promise, timeoutPromise]);
}

async function run() {
    try {
        const result = await timeout(delay(2000, "느림"), 1000);
        console.log(result);
    } catch (error) {
        console.log(error.message);
    }
}

run();
// "시간 초과"

Promise.race로 타임아웃과 원래 작업을 경쟁하게 만들어 제한 시간을 넘기면 중단했다.


6. 파이프라인 패턴

비동기 작업을 연속적으로 연결해서 처리할 수도 있다:

async function step1(input) {
    return await delay(1000, input + " -> 1단계");
}

async function step2(input) {
    return await delay(1000, input + " -> 2단계");
}

async function step3(input) {
    return await delay(1000, input + " -> 3단계");
}

async function pipeline(input) {
    const result1 = await step1(input);
    const result2 = await step2(result1);
    const result3 = await step3(result2);
    return result3;
}

async function run() {
    const result = await pipeline("시작");
    console.log(result);
}

run();
// 3초 후 "시작 -> 1단계 -> 2단계 -> 3단계"

각 단계를 연결해서 데이터를 순차적으로 변환했다. 데이터 흐름을 체계적으로 관리할 때 좋다.


7. 큐 기반 패턴

작업을 큐에 쌓아서 순차적으로 처리하는 방식도 유용하다:

class AsyncQueue {
    constructor() {
        this.queue = [];
        this.running = false;
    }

    add(task) {
        this.queue.push(task);
        this.process();
    }

    async process() {
        if (this.running || !this.queue.length) return;
        this.running = true;
        while (this.queue.length) {
            const task = this.queue.shift();
            await task();
        }
        this.running = false;
    }
}

const queue = new AsyncQueue();

queue.add(async () => {
    const result = await delay(1000, "작업 1");
    console.log(result);
});
queue.add(async () => {
    const result = await delay(1000, "작업 2");
    console.log(result);
});
queue.add(async () => {
    const result = await delay(1000, "작업 3");
    console.log(result);
});
// 1초 후 "작업 1"
// 2초 후 "작업 2"
// 3초 후 "작업 3"

큐에 작업을 쌓아서 순서대로 처리했다. 동시성을 제어하면서 순서를 보장할 수 있다.


8. 동시성 제한 패턴

병렬 작업 개수를 제한하고 싶을 때 사용할 수 있다:

async function limitedParallel(tasks, limit) {
    const results = [];
    const executing = [];

    for (const task of tasks) {
        const promise = task().then(result => {
            results.push(result);
            executing.splice(executing.indexOf(promise), 1);
        });
        executing.push(promise);
        if (executing.length >= limit) {
            await Promise.race(executing);
        }
    }
    await Promise.all(executing);
    return results;
}

const tasks = [
    () => delay(1000, "작업 1"),
    () => delay(1500, "작업 2"),
    () => delay(500, "작업 3"),
    () => delay(2000, "작업 4")
];

async function run() {
    const results = await limitedParallel(tasks, 2);
    results.forEach(r => console.log(r));
}

run();
// 1초 후 "작업 1"
// 1.5초 후 "작업 2"
// 1.5초 후 "작업 3"
// 2초 후 "작업 4"

최대 2개 작업만 동시에 실행되도록 제한했다. 리소스 사용을 조절하면서 병렬성을 유지할 수 있다.


9. 이벤트 기반 비동기 패턴

이벤트에 반응하며 비동기 작업을 처리할 수도 있다:

const { EventEmitter } = require("events");
const emitter = new EventEmitter();

async function handleEvent(eventName) {
    return new Promise(resolve => {
        emitter.once(eventName, data => resolve(delay(1000, data)));
    });
}

async function run() {
    const task = handleEvent("data");
    setTimeout(() => emitter.emit("data", "이벤트 발생"), 500);
    const result = await task;
    console.log(result);
}

run();
// 1.5초 후 "이벤트 발생"

이벤트 발생을 기다리며 비동기 작업을 연결했다. 외부 트리거에 반응할 때 유연하다.


10. 에러 복구 패턴

에러가 발생해도 흐름을 이어가게 만들 수 있다:

async function riskyTask(id) {
    if (id === 2) throw new Error("문제 발생");
    return await delay(1000, `작업 ${id} 완료`);
}

async function runWithRecovery() {
    const tasks = [1, 2, 3].map(id => async () => {
        try {
            return await riskyTask(id);
        } catch (error) {
            console.log(`에러: ${id} - ` + error.message);
            return `작업 ${id} 복구됨`;
        }
    });
    const results = await Promise.all(tasks.map(t => t()));
    results.forEach(r => console.log(r));
}

runWithRecovery();
// 1초 후:
// "작업 1 완료"
// "에러: 2 - 문제 발생"
// "작업 2 복구됨"
// "작업 3 완료"

에러가 발생한 작업은 복구 값을 반환하며 전체 흐름을 유지했다.


비동기 패턴의 장단점

이런 패턴들이 코드에 어떤 영향을 주는지 보자:

- 유연성: 병렬, 순차, 제한 등 상황에 맞게 조정할 수 있다.

- 복잡성: 패턴이 늘어날수록 코드가 복잡해질 수 있으니 적절히 선택해야 한다.

상황에 맞는 패턴을 골라서 쓰는 게 핵심이다. 필요 이상으로 복잡하게 만들지 않도록 주의하자.


마무리

비동기 패턴은 단순한 순차 실행부터 병렬, 재시도, 큐, 동시성 제한까지 다양한 흐름을 다룰 수 있게 해준다. 각 패턴을 상황에 맞춰 적용하면 비동기 코드를 더 효율적이고 깔끔하게 관리할 수 있다.


비동기 제너레이터 (Async Generators)

비동기 제너레이터 (Async Generators)

자바스크립트에서 비동기 작업을 순차적으로 생성하며 처리하려면 비동기 제너레이터가 유용하다. 이는 비동기 이터레이터를 더 간결하게 구현할 수 있게 해준다. 이번에는 비동기 제너레이터의 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


비동기 제너레이터를 잘 다루면 비동기 데이터 흐름을 깔끔하고 효율적으로 관리할 수 있다. 단계별로 하나씩 살펴보자.


비동기 제너레이터 기본

async function*로 정의하며, yield가 비동기 값을 생성한다:

async function* asyncGenerator() {
    yield Promise.resolve(1);
    yield Promise.resolve(2);
    yield Promise.resolve(3);
}

async function process() {
    const gen = asyncGenerator();
    for await (const num of gen) {
        console.log(num);
    }
}

process();
// 1
// 2
// 3

for await...of로 비동기 제너레이터를 순회하며 값을 받았다. Promise가 자동으로 해결된다.


1. 비동기 작업과 yield 결합

시간 지연을 포함한 비동기 작업을 생성해보자:

function delay(ms, value) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), ms);
    });
}

async function* asyncGenerator() {
    yield delay(1000, "첫 번째");
    yield delay(1000, "두 번째");
    yield delay(1000, "세 번째");
}

async function process() {
    for await (const value of asyncGenerator()) {
        console.log(value);
    }
}

process();
// 1초 후 "첫 번째"
// 2초 후 "두 번째"
// 3초 후 "세 번째"

yield가 비동기 작업을 기다리며 순차적으로 값을 생성했다.


2. 외부 API 데이터 생성

API 호출을 비동기 제너레이터로 처리해보자:

async function* fetchPosts() {
    for (let id = 1; id <= 3; id++) {
        const response = await fetch(
            `https://jsonplaceholder.typicode.com/posts/${id}`
        );
        const data = await response.json();
        yield data.title;
    }
}

async function process() {
    for await (const title of fetchPosts()) {
        console.log(title);
    }
}

process();
// "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
// "qui est esse"
// "ea molestias quasi exercitationem repellat qui ipsa sit aut"

API에서 가져온 데이터를 하나씩 yield로 생성하며 순회했다.


3. 에러 처리

비동기 제너레이터에서도 에러를 관리할 수 있다:

async function* asyncGenerator() {
    yield delay(1000, 1);
    throw new Error("에러 발생");
    yield delay(1000, 3);
}

async function process() {
    try {
        for await (const num of asyncGenerator()) {
            console.log(num);
        }
    } catch (error) {
        console.log("에러: " + error.message);
    }
}

process();
// 1초 후 1
// "에러: 에러 발생"

throw로 에러를 발생시키고, try-catch로 잡아서 처리했다.


4. 입력값 기반 생성

외부 입력을 받아 동적으로 값을 생성할 수 있다:

async function* countGenerator(max) {
    for (let i = 1; i <= max; i++) {
        yield delay(1000, i);
    }
}

async function process() {
    for await (const num of countGenerator(3)) {
        console.log(num);
    }
}

process();
// 1초 후 1
// 2초 후 2
// 3초 후 3

매개변수로 받은 최대값까지 비동기적으로 생성했다.


5. 스트림처럼 활용

비동기 제너레이터를 스트림처럼 사용할 수 있다:

async function* streamGenerator() {
    let count = 0;
    while (count < 5) {
        count++;
        yield delay(500, `스트림 ${count}`);
    }
}

async function processStream() {
    for await (const chunk of streamGenerator()) {
        console.log(chunk);
    }
}

processStream();
// 0.5초 후 "스트림 1"
// 1초 후 "스트림 2"
// 1.5초 후 "스트림 3"
// 2초 후 "스트림 4"
// 2.5초 후 "스트림 5"

지속적인 데이터 흐름을 시뮬레이션하며 순차적으로 값을 생성했다.


6. 조기 종료와 return

제너레이터를 조기에 종료하고 정리할 수 있다:

async function* asyncGenerator() {
    try {
        yield delay(1000, 1);
        yield delay(1000, 2);
        yield delay(1000, 3);
    } finally {
        console.log("제너레이터 종료");
    }
}

async function process() {
    const gen = asyncGenerator();
    for await (const num of gen) {
        console.log(num);
        if (num === 2) {
            await gen.return();
            break;
        }
    }
}

process();
// 1초 후 1
// 2초 후 2
// "제너레이터 종료"

return 메서드로 종료하고, finally로 정리 작업을 실행했다.


7. next()로 수동 제어

for await...of 대신 next()로 수동으로 제어할 수 있다:

async function* asyncGenerator() {
    yield delay(1000, 1);
    yield delay(1000, 2);
    yield delay(1000, 3);
}

async function processManual() {
    const gen = asyncGenerator();
    let result = await gen.next();
    while (!result.done) {
        console.log(result.value);
        result = await gen.next();
    }
}

processManual();
// 1초 후 1
// 2초 후 2
// 3초 후 3

수동으로 next()를 호출해서 값을 하나씩 처리했다.


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

비동기 제너레이터가 코드에 어떤 영향을 주는지 보자:

- 성능: 비동기 작업을 순차적으로 처리하니 대기 시간이 늘어날 수 있지만, 필요한 만큼만 생성해서 효율적이다.

- 가독성: yieldasync 조합으로 비동기 흐름이 명확해진다.

yield로 비동기 값을 간결하게 생성하고, for await로 쉽게 순회하는 점이 핵심이다.


마무리

비동기 제너레이터는 비동기 데이터 생성과 순회를 단순화한다. 기본 사용부터 API 처리, 스트림, 에러 관리까지 다양한 상황에서 유연하게 활용할 수 있다.


비동기 이터레이터 (Async Iterators)

비동기 이터레이터 (Async Iterators)

자바스크립트에서 비동기 작업과 반복을 결합하면 강력한 흐름 제어가 가능하다. 비동기 이터레이터는 비동기 데이터 소스를 순차적으로 처리할 수 있게 해준다. 이번에는 비동기 이터레이터의 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


이를 잘 활용하면 비동기 데이터를 더 유연하고 직관적으로 다룰 수 있다. 단계별로 하나씩 살펴보자.


비동기 이터레이터 기본

비동기 이터레이터는 [Symbol.asyncIterator]를 구현한 객체로, next()가 Promise를 반환한다:

const asyncIterable = {
    [Symbol.asyncIterator]() {
        let count = 0;
        return {
            async next() {
                count++;
                if (count <= 3) {
                    return Promise.resolve({ value: count, done: false });
                }
                return Promise.resolve({ done: true });
            }
        };
    }
};

async function process() {
    for await (const num of asyncIterable) {
        console.log(num);
    }
}

process();
// 1
// 2
// 3

for await...of로 비동기 이터레이터를 순회하며 값을 출력했다. 각 next()가 Promise를 반환한다.


1. 비동기 데이터 소스 처리

비동기적으로 데이터를 가져오는 상황을 다뤄보자:

function delay(ms, value) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), ms);
    });
}

const asyncIterable = {
    [Symbol.asyncIterator]() {
        let id = 0;
        return {
            async next() {
                id++;
                if (id <= 3) {
                    return delay(1000, `데이터 ${id}`)
                        .then(value => ({ value, done: false }));
                }
                return Promise.resolve({ done: true });
            }
        };
    }
};

async function process() {
    for await (const data of asyncIterable) {
        console.log(data);
    }
}

process();
// 1초 후 "데이터 1"
// 2초 후 "데이터 2"
// 3초 후 "데이터 3"

시간 지연을 시뮬레이션해서 비동기 데이터를 순차적으로 처리했다.


2. 외부 API와 비동기 이터레이터

실제 API 호출을 이터레이터로 처리해보자:

const apiIterable = {
    [Symbol.asyncIterator]() {
        let page = 1;
        return {
            async next() {
                if (page <= 3) {
                    const response = await fetch(
                        `https://jsonplaceholder.typicode.com/posts/${page}`
                    );
                    const data = await response.json();
                    page++;
                    return { value: data.title, done: false };
                }
                return { done: true };
            }
        };
    }
};

async function process() {
    for await (const title of apiIterable) {
        console.log(title);
    }
}

process();
// "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
// "qui est esse"
// "ea molestias quasi exercitationem repellat qui ipsa sit aut"

API에서 데이터를 페이지 단위로 가져와 순차적으로 출력했다.


3. 에러 처리 추가

비동기 이터레이터에서도 에러를 관리할 수 있다:

const asyncIterable = {
    [Symbol.asyncIterator]() {
        let count = 0;
        return {
            async next() {
                count++;
                if (count === 2) {
                    return Promise.reject(new Error("2에서 실패"));
                }
                if (count <= 3) {
                    return delay(1000, count)
                        .then(value => ({ value, done: false }));
                }
                return Promise.resolve({ done: true });
            }
        };
    }
};

async function process() {
    try {
        for await (const num of asyncIterable) {
            console.log(num);
        }
    } catch (error) {
        console.log("에러: " + error.message);
    }
}

process();
// 1초 후 1
// "에러: 2에서 실패"

try-catch로 에러를 잡아서 순회 중단 시 처리했다.


4. 커스텀 비동기 이터레이터 클래스

클래스로 더 구조화된 이터레이터를 만들어보자:

class AsyncCounter {
    constructor(max) {
        this.max = max;
    }

    [Symbol.asyncIterator]() {
        let count = 0;
        return {
            async next() {
                count++;
                if (count <= this.max) {
                    return delay(1000, count)
                        .then(value => ({ value, done: false }));
                }
                return Promise.resolve({ done: true });
            }
        };
    }
}

async function process() {
    const counter = new AsyncCounter(3);
    for await (const num of counter) {
        console.log(num);
    }
}

process();
// 1초 후 1
// 2초 후 2
// 3초 후 3

클래스로 이터레이터를 만들어 재사용성을 높였다.


5. 스트림처럼 사용

비동기 이터레이터를 스트림처럼 활용할 수 있다:

const streamIterable = {
    [Symbol.asyncIterator]() {
        let count = 0;
        return {
            async next() {
                if (count < 5) {
                    const value = await delay(500, `스트림 ${count}`);
                    count++;
                    return { value, done: false };
                }
                return { done: true };
            }
        };
    }
};

async function processStream() {
    for await (const chunk of streamIterable) {
        console.log(chunk);
    }
}

processStream();
// 0.5초 후 "스트림 0"
// 1초 후 "스트림 1"
// 1.5초 후 "스트림 2"
// 2초 후 "스트림 3"
// 2.5초 후 "스트림 4"

데이터가 지속적으로 도착하는 상황을 시뮬레이션했다.


6. 조기 종료와 cleanup

이터레이터에 종료 로직을 추가할 수 있다:

const asyncIterable = {
    [Symbol.asyncIterator]() {
        let count = 0;
        let timer;
        return {
            async next() {
                count++;
                if (count <= 5) {
                    return delay(1000, count)
                        .then(value => ({ value, done: false }));
                }
                return Promise.resolve({ done: true });
            },
            async return() {
                clearTimeout(timer);
                console.log("이터레이터 종료");
                return { done: true };
            }
        };
    }
};

async function process() {
    for await (const num of asyncIterable) {
        console.log(num);
        if (num === 2) break;
    }
}

process();
// 1초 후 1
// 2초 후 2
// "이터레이터 종료"

return 메서드로 조기 종료 시 정리 작업을 추가했다.


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

비동기 이터레이터가 코드에 어떤 영향을 주는지 보자:

- 성능: 비동기 작업을 순차적으로 처리하니 대기 시간이 늘어날 수 있지만, 스트림 처리에 적합하다.

- 가독성: for await...of로 비동기 순회가 직관적이 된다.

for await...of로 간결함을 유지하고, 에러 처리로 안정성을 더하는 점이 핵심이다.


마무리

비동기 이터레이터는 비동기 데이터 소스를 순회하며 처리하는 데 유용하다. 기본 구현부터 API 호출, 스트림 처리까지 확장 가능하며, 에러 관리와 종료 로직으로 더 튼튼해진다.


에러 로깅과 모니터링 (Error Logging and Monitoring)

에러 로깅과 모니터링 (Error Logging and Monitoring)

자바스크립트 애플리케이션에서 에러는 언제든 발생할 수 있다. 에러 로깅과 모니터링을 잘하면 문제를 빠르게 파악하고, 사용자 경험을 개선할 수 있다. 이번에는 에러를 기록하고 추적하는 방법을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


에러를 체계적으로 관리하면 코드 안정성이 높아지고, 문제 해결이 쉬워진다. 단계별로 하나씩 살펴보자.


기본적인 에러 로깅

가장 간단한 방법은 console.error로 에러를 기록하는 것이다:

try {
    let data = JSON.parse("잘못된 JSON");
    console.log(data);
} catch (error) {
    console.error("에러 발생: ", error);
}
// "에러 발생: " SyntaxError: Unexpected token � in JSON at position 0

console.error로 에러 메시지와 객체를 출력했다. 개발 중에는 유용하지만, 운영 환경에서는 더 체계적인 로깅이 필요하다.


1. 에러 객체 상세 기록

에러의 세부 정보를 기록하면 디버깅이 쉬워진다:

function logError(error) {
    const errorDetails = {
        message: error.message,
        stack: error.stack,
        time: new Date().toISOString()
    };
    console.error("에러 상세: ", errorDetails);
}

try {
    throw new Error("테스트 에러");
} catch (error) {
    logError(error);
}
// "에러 상세: " { message: "테스트 에러", stack: "...", time: "2025-03-19T..." }

메시지, 스택 트레이스, 발생 시간을 객체로 만들어 기록했다. 문제의 원인을 추적하기 좋아진다.


2. 비동기 에러 로깅

비동기 작업에서도 에러를 기록할 수 있다:

async function fetchData() {
    const response = await fetch("https://invalid.url");
    return response.json();
}

async function process() {
    try {
        await fetchData();
    } catch (error) {
        const log = {
            message: error.message,
            stack: error.stack,
            time: new Date().toISOString()
        };
        console.error("비동기 에러: ", log);
    }
}

process();
// "비동기 에러: " { message: "Failed to fetch", stack: "...", time: "2025-03-19T..." }

try-catch로 비동기 에러를 잡고 상세 로그를 남겼다.


3. 서버로 에러 전송

운영 환경에서는 에러를 서버로 보내 모니터링할 수 있다:

function sendErrorToServer(error) {
    const log = {
        message: error.message,
        stack: error.stack,
        time: new Date().toISOString(),
        userAgent: navigator.userAgent
    };
    fetch("https://your-api.com/error", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(log)
    }).catch(err => console.error("전송 실패: ", err));
}

try {
    throw new Error("운영 에러");
} catch (error) {
    sendErrorToServer(error);
}

fetch로 에러를 서버로 보내고, 사용자 정보도 추가했다. 서버에서 이를 저장하면 실시간 모니터링이 가능하다.


4. window.onerror로 전역 에러 잡기

처리되지 않은 에러를 전역적으로 잡을 수 있다:

window.onerror = function (message, source, lineno, colno, error) {
    const log = {
        message,
        source,
        line: lineno,
        column: colno,
        stack: error ? error.stack : "없음",
        time: new Date().toISOString()
    };
    console.error("전역 에러: ", log);
    return true; // 기본 에러 표시 방지
};

throw new Error("전역 테스트");
// "전역 에러: " { message: "전역 테스트", source: "...", line: ..., column: ..., stack: "...", time: "2025-03-19T..." }

window.onerror로 잡히지 않은 에러를 기록하고, 기본 에러 팝업을 막았다.


5. 모니터링 시스템 연동

Sentry 같은 외부 서비스와 연동하면 더 강력해진다:

// Sentry 초기화 (가정)
Sentry.init({ dsn: "https://your-dsn@sentry.io" });

function logWithSentry(error) {
    Sentry.captureException(error, {
        extra: {
            userAgent: navigator.userAgent,
            time: new Date().toISOString()
        }
    });
}

try {
    throw new Error("Sentry 테스트");
} catch (error) {
    logWithSentry(error);
}

Sentry로 에러를 전송하고 추가 데이터를 붙였다. 대시보드에서 실시간으로 확인할 수 있다.


6. 커스텀 에러 로깅

커스텀 에러 클래스로 구체적인 로깅을 할 수 있다:

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

function logCustomError(error) {
    const log = {
        name: error.name,
        message: error.message,
        field: error.field || "없음",
        stack: error.stack,
        time: new Date().toISOString()
    };
    console.error("커스텀 에러: ", log);
}

try {
    throw new ValidationError("잘못된 입력", "email");
} catch (error) {
    logCustomError(error);
}
// "커스텀 에러: " { name: "ValidationError", message: "잘못된 입력", field: "email", stack: "...", time: "2025-03-19T..." }

커스텀 속성을 추가해서 에러의 맥락을 더 풍부하게 기록했다.


7. 모니터링 알림 설정

중요 에러 발생 시 알림을 받을 수 있다:

async function notifyError(error) {
    const log = {
        message: error.message,
        stack: error.stack,
        time: new Date().toISOString()
    };
    await fetch("https://your-api.com/notify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ log, level: "critical" })
    });
}

try {
    throw new Error("심각한 에러");
} catch (error) {
    notifyError(error);
}

서버로 에러를 보내고, 이를 슬랙이나 이메일로 알림 설정할 수 있다.


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

에러 로깅과 모니터링이 개발에 어떤 영향을 주는지 보자:

- 성능: 로깅과 전송은 약간의 오버헤드를 추가하지만, 문제 발견 속도를 높인다.

- 가독성: 체계적인 로그는 코드 상태를 이해하기 쉽게 한다.

서버 전송으로 실시간 추적을 하고, 커스텀 로그로 세부 정보를 강화하는 점이 핵심이다.


마무리

에러 로깅과 모니터링은 단순히 콘솔 출력에서 시작해서 서버 연동, 전역 관리, 외부 서비스 활용까지 확장된다. 이를 통해 문제를 사전에 감지하고, 안정적인 애플리케이션을 유지할 수 있다.


디버깅 도구 활용 (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. 성능과 가독성에 미치는 영향

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

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

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

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


마무리

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


+ Recent posts