프론트엔드 짧은 간단 지식 정리 - 예외처리

이상범·2024년 10월 13일

1. 예외 처리란? 🛡️

프로그램 실행 중 발생하는 오류를 적절히 처리하여 프로그램이 중단되지 않도록 하는 방법입니다.

🔍 예외 처리의 정의

예외 처리(Exception Handling)는 프로그래밍 언어가 정해놓은 규칙에서 벗어난 오류가 발생했을 때의 행동을 정의하는 것입니다.

💡 왜 예외 처리가 필요한가?

// ❌ 예외 처리 없는 코드
function divide(a, b) {
    return a / b;
}

console.log(divide(10, 0)); // Infinity (수학적으로는 문제)
console.log(divide(10, "hello")); // NaN (예상치 못한 동작)

// ❌ 오류로 프로그램 중단
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
// 이후 코드는 실행되지 않음!

// ✅ 예외 처리가 있는 코드
function safeDivide(a, b) {
    try {
        if (b === 0) {
            throw new Error("0으로 나눌 수 없습니다");
        }
        if (typeof a !== "number" || typeof b !== "number") {
            throw new Error("숫자만 입력 가능합니다");
        }
        return a / b;
    } catch (error) {
        console.error("오류 발생:", error.message);
        return null;
    }
}

console.log(safeDivide(10, 2));      // 5
console.log(safeDivide(10, 0));      // 오류 발생: 0으로 나눌 수 없습니다
console.log(safeDivide(10, "hello")); // 오류 발생: 숫자만 입력 가능합니다

🌟 예외 처리의 장점

  • 프로그램 안정성 향상: 예상치 못한 오류로 인한 프로그램 중단 방지
  • 사용자 경험 개선: 오류 발생 시 적절한 메시지 제공
  • 디버깅 용이: 오류 원인과 위치를 쉽게 파악
  • 코드 가독성: 정상 로직과 오류 처리 로직 분리

2. try-catch 기본 구조 📝

🎯 기본 문법

try {
    // 오류가 발생할 가능성이 있는 코드
    const result = riskyOperation();
    console.log(result);
} catch (error) {
    // 오류가 발생했을 때 실행될 코드
    console.error("오류가 발생했습니다:", error.message);
}

💡 실제 사용 예시

// 1. JSON 파싱
function parseJSON(jsonString) {
    try {
        const data = JSON.parse(jsonString);
        return { success: true, data };
    } catch (error) {
        return { 
            success: false, 
            error: "유효하지 않은 JSON 형식입니다" 
        };
    }
}

console.log(parseJSON('{"name": "Alice"}')); 
// { success: true, data: { name: "Alice" } }

console.log(parseJSON('{invalid json}'));
// { success: false, error: "유효하지 않은 JSON 형식입니다" }

// 2. 배열 접근
function getArrayElement(arr, index) {
    try {
        if (!Array.isArray(arr)) {
            throw new Error("배열이 아닙니다");
        }
        if (index < 0 || index >= arr.length) {
            throw new Error("인덱스가 범위를 벗어났습니다");
        }
        return arr[index];
    } catch (error) {
        console.error("오류:", error.message);
        return undefined;
    }
}

const numbers = [1, 2, 3, 4, 5];
console.log(getArrayElement(numbers, 2));  // 3
console.log(getArrayElement(numbers, 10)); // 오류: 인덱스가 범위를 벗어났습니다
console.log(getArrayElement("not array", 0)); // 오류: 배열이 아닙니다

3. try-catch-finally 구조 🔄

🔍 finally의 역할

finally 블록은 try 블록의 성공/실패 여부와 상관없이 항상 실행됩니다.

📊 실행 순서

function demonstrateFinally() {
    console.log("1. 시작");
    
    try {
        console.log("2. try 블록 실행");
        // throw new Error("의도적 오류"); // 주석 해제하면 catch 실행
        console.log("3. try 블록 완료");
        return "success";
    } catch (error) {
        console.log("4. catch 블록 실행");
        return "error";
    } finally {
        console.log("5. finally 블록 실행 (항상 실행!)");
    }
    
    console.log("6. 함수 끝"); // 도달하지 않음 (return 때문)
}

demonstrateFinally();
/*
출력:
1. 시작
2. try 블록 실행
3. try 블록 완료
4. finally 블록 실행 (항상 실행!)
*/

💡 finally의 주요 사용 사례

1️⃣ 리소스 해제

// 파일 작업
function readFile(filePath) {
    let file = null;
    
    try {
        file = openFile(filePath); // 파일 열기
        const content = file.read();
        console.log("파일 내용:", content);
        return content;
    } catch (error) {
        console.error("파일 읽기 오류:", error.message);
        return null;
    } finally {
        // 오류 발생 여부와 관계없이 파일 닫기
        if (file) {
            file.close();
            console.log("파일이 닫혔습니다.");
        }
    }
}

// 데이터베이스 연결
async function queryDatabase(query) {
    let connection = null;
    
    try {
        connection = await connectToDatabase();
        const result = await connection.execute(query);
        return result;
    } catch (error) {
        console.error("쿼리 실행 오류:", error);
        throw error;
    } finally {
        // 연결 종료 (항상 실행)
        if (connection) {
            await connection.close();
            console.log("데이터베이스 연결이 종료되었습니다.");
        }
    }
}

2️⃣ 로딩 상태 관리

// React 예시
async function fetchData() {
    let isLoading = true;
    
    try {
        console.log("로딩 시작...");
        const response = await fetch("/api/data");
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("데이터 가져오기 실패:", error);
        return null;
    } finally {
        // 성공/실패 여부와 관계없이 로딩 종료
        isLoading = false;
        console.log("로딩 완료");
    }
}

3️⃣ 타이머 정리

function processWithTimeout(task, timeout = 5000) {
    let timerId = null;
    
    try {
        // 타임아웃 설정
        timerId = setTimeout(() => {
            throw new Error("작업 시간 초과");
        }, timeout);
        
        // 작업 실행
        const result = task();
        console.log("작업 완료:", result);
        return result;
    } catch (error) {
        console.error("오류 발생:", error.message);
        return null;
    } finally {
        // 타이머 정리 (항상 실행)
        if (timerId) {
            clearTimeout(timerId);
            console.log("타이머 정리 완료");
        }
    }
}

🌟 실전 예시: 숫자 계산기

// 사용자 입력을 숫자로 변환하는 함수
function parseNumber(input) {
    const number = parseFloat(input);
    if (isNaN(number)) {
        throw new Error("입력 값이 숫자가 아닙니다.");
    }
    return number;
}

// 두 숫자를 더하는 함수
function addNumbers(a, b) {
    return a + b;
}

// 메인 함수
function calculator() {
    console.log("=== 계산기 시작 ===");
    
    try {
        const input1 = "10";
        const input2 = "20";
        
        console.log(`입력값: ${input1}, ${input2}`);
        
        const number1 = parseNumber(input1);
        const number2 = parseNumber(input2);
        
        const result = addNumbers(number1, number2);
        console.log(`결과: ${result}`);
        return result;
    } catch (error) {
        console.error(`오류가 발생했습니다: ${error.message}`);
        return null;
    } finally {
        console.log("=== 계산기 종료 ===");
    }
}

calculator();
/*
출력:
=== 계산기 시작 ===
입력값: 10, 20
결과: 30
=== 계산기 종료 ===
*/

// 오류 발생 케이스
function calculatorWithError() {
    console.log("=== 계산기 시작 ===");
    
    try {
        const input1 = "10";
        const input2 = "abc"; // 잘못된 입력
        
        console.log(`입력값: ${input1}, ${input2}`);
        
        const number1 = parseNumber(input1);
        const number2 = parseNumber(input2); // 여기서 오류 발생
        
        const result = addNumbers(number1, number2);
        console.log(`결과: ${result}`);
    } catch (error) {
        console.error(`오류가 발생했습니다: ${error.message}`);
    } finally {
        console.log("=== 계산기 종료 ===");
    }
}

calculatorWithError();
/*
출력:
=== 계산기 시작 ===
입력값: 10, abc
오류가 발생했습니다: 입력 값이 숫자가 아닙니다.
=== 계산기 종료 ===
*/

4. 예외 전달 (throw) 🎯

🔍 throw의 역할

throw 문은 사용자 정의 예외를 발생시켜 상위 호출 스택으로 전달합니다.

💡 기본 사용법

function validateAge(age) {
    if (age < 0) {
        throw new Error("나이는 음수일 수 없습니다");
    }
    if (age > 150) {
        throw new Error("나이가 너무 큽니다");
    }
    return true;
}

function createUser(name, age) {
    try {
        validateAge(age); // 예외가 발생하면 catch로 전달
        return { name, age, createdAt: new Date() };
    } catch (error) {
        console.error("사용자 생성 실패:", error.message);
        return null;
    }
}

console.log(createUser("Alice", 25));  // { name: "Alice", age: 25, ... }
console.log(createUser("Bob", -5));    // 사용자 생성 실패: 나이는 음수일 수 없습니다
console.log(createUser("Charlie", 200)); // 사용자 생성 실패: 나이가 너무 큽니다

🌟 예외 전달의 실전 활용

1️⃣ 다단계 함수 호출

// 최하위 함수
function readConfig() {
    throw new Error("설정 파일을 찾을 수 없습니다");
}

// 중간 함수
function initializeApp() {
    try {
        const config = readConfig(); // 예외 발생
        return config;
    } catch (error) {
        // 예외를 상위로 다시 전달 (재throw)
        throw new Error(`앱 초기화 실패: ${error.message}`);
    }
}

// 최상위 함수
function startApp() {
    try {
        initializeApp();
        console.log("앱이 시작되었습니다");
    } catch (error) {
        console.error("치명적 오류:", error.message);
        // 치명적 오류: 앱 초기화 실패: 설정 파일을 찾을 수 없습니다
    }
}

startApp();

2️⃣ 유효성 검증 체인

function validateEmail(email) {
    if (!email) {
        throw new Error("이메일을 입력해주세요");
    }
    if (!email.includes("@")) {
        throw new Error("올바른 이메일 형식이 아닙니다");
    }
    return true;
}

function validatePassword(password) {
    if (!password) {
        throw new Error("비밀번호를 입력해주세요");
    }
    if (password.length < 8) {
        throw new Error("비밀번호는 8자 이상이어야 합니다");
    }
    return true;
}

function register(email, password) {
    try {
        console.log("회원가입 시도...");
        validateEmail(email);
        validatePassword(password);
        console.log("회원가입 성공! ✅");
        return { success: true, email };
    } catch (error) {
        console.error("회원가입 실패:", error.message);
        return { success: false, error: error.message };
    }
}

// 테스트
register("alice@example.com", "securepass123"); // 성공
register("", "password"); // 실패: 이메일을 입력해주세요
register("invalid-email", "password"); // 실패: 올바른 이메일 형식이 아닙니다
register("bob@example.com", "short"); // 실패: 비밀번호는 8자 이상이어야 합니다

3️⃣ Error 객체의 스택 추적

function innerFunction() {
    throw new Error("내부 함수에서 오류 발생");
}

function middleFunction() {
    innerFunction(); // 예외를 상위로 전달
}

function outerFunction() {
    try {
        middleFunction();
    } catch (error) {
        console.error("오류 처리:", error.message);
        console.error("스택 추적:");
        console.error(error.stack); // 어디서 오류가 발생했는지 확인
        /*
        출력:
        오류 처리: 내부 함수에서 오류 발생
        스택 추적:
        Error: 내부 함수에서 오류 발생
            at innerFunction (file.js:2:11)
            at middleFunction (file.js:6:5)
            at outerFunction (file.js:10:9)
        */
    }
}

outerFunction();

5. 커스텀 에러 클래스 🎨

🔍 왜 커스텀 에러가 필요한가?

에러 타입을 구분하여 각각 다르게 처리할 수 있습니다.

💡 커스텀 에러 정의

// 1. 기본 커스텀 에러
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

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

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

// 2. 에러 타입별 처리
function processData(data) {
    try {
        if (!data) {
            throw new ValidationError("데이터가 없습니다");
        }
        if (!data.id) {
            throw new ValidationError("ID가 필요합니다");
        }
        
        // 네트워크 요청 시뮬레이션
        if (Math.random() < 0.3) {
            throw new NetworkError("서버 연결 실패", 500);
        }
        
        // 데이터베이스 작업 시뮬레이션
        if (Math.random() < 0.2) {
            throw new DatabaseError(
                "쿼리 실행 실패", 
                "SELECT * FROM users"
            );
        }
        
        return { success: true, data };
    } catch (error) {
        // 에러 타입별로 다른 처리
        if (error instanceof ValidationError) {
            console.error("❌ 검증 오류:", error.message);
            return { success: false, type: "validation" };
        } else if (error instanceof NetworkError) {
            console.error("🌐 네트워크 오류:", error.message);
            console.error("상태 코드:", error.statusCode);
            return { success: false, type: "network" };
        } else if (error instanceof DatabaseError) {
            console.error("💾 데이터베이스 오류:", error.message);
            console.error("쿼리:", error.query);
            return { success: false, type: "database" };
        } else {
            console.error("⚠️ 알 수 없는 오류:", error);
            return { success: false, type: "unknown" };
        }
    }
}

// 테스트
processData({ id: 1, name: "Alice" });
processData(null);
processData({ name: "Bob" });

🌟 실전 예시: API 에러 처리

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

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new APIError(
                "사용자 정보를 가져올 수 없습니다",
                response.status,
                `/api/users/${userId}`
            );
        }
        
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        if (error instanceof APIError) {
            console.error("API 오류:", {
                message: error.message,
                status: error.statusCode,
                endpoint: error.endpoint,
                time: error.timestamp
            });
            
            // 상태 코드별 처리
            if (error.statusCode === 404) {
                return { success: false, error: "사용자를 찾을 수 없습니다" };
            } else if (error.statusCode === 401) {
                return { success: false, error: "인증이 필요합니다" };
            } else if (error.statusCode >= 500) {
                return { success: false, error: "서버 오류가 발생했습니다" };
            }
        }
        
        return { success: false, error: "알 수 없는 오류" };
    }
}

6. 비동기 코드의 예외 처리 ⚡

🔍 async/await과 try-catch

// ✅ async/await과 try-catch
async function fetchData(url) {
    try {
        console.log("데이터 요청 중...");
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP 오류! 상태: ${response.status}`);
        }
        
        const data = await response.json();
        console.log("데이터 받기 성공:", data);
        return data;
    } catch (error) {
        console.error("데이터 가져오기 실패:", error.message);
        return null;
    } finally {
        console.log("요청 완료");
    }
}

// 사용
async function loadUserProfile(userId) {
    const userData = await fetchData(`/api/users/${userId}`);
    if (userData) {
        console.log("사용자 프로필:", userData);
    } else {
        console.log("기본 프로필 사용");
    }
}

💡 Promise의 catch

// Promise 체인에서의 에러 처리
fetch("/api/data")
    .then(response => {
        if (!response.ok) {
            throw new Error("네트워크 응답이 올바르지 않습니다");
        }
        return response.json();
    })
    .then(data => {
        console.log("데이터:", data);
        return processData(data);
    })
    .catch(error => {
        console.error("오류 발생:", error.message);
    })
    .finally(() => {
        console.log("작업 완료");
    });

🌟 실전 예시: CSV 다운로드

async function downloadCSV(queryParams) {
    try {
        console.log("CSV 다운로드 시작...");
        
        // API 호출
        const response = await fetch(
            `/api/common/openData/getExmnStatExcelByFile?${makeUrlQuery(queryParams)}`
        );
        
        // 응답 상태 확인
        if (!response.ok) {
            throw new Error(`서버 오류: ${response.status}`);
        }
        
        // JSON 데이터 파싱
        const data = await response.json();
        
        // 데이터 검증
        if (!data || !data.length) {
            throw new Error("다운로드할 데이터가 없습니다");
        }
        
        // CSV 헤더 생성
        const headerString = "조사통계종류,기준연도,자료명,조회수,작성일,수정일";
        
        // CSV 데이터 변환
        const csvRows = data.map(item => {
            return [
                JSON.stringify(item.exmnStatCtgrNm),
                JSON.stringify(item.crtrYr),
                JSON.stringify(item.title),
                JSON.stringify(item.inqCnt),
                item.regDt ? JSON.stringify(item.regDt) : "-",
                item.mdfcnDt ? JSON.stringify(item.mdfcnDt) : "-"
            ].join(",");
        });
        
        // CSV 다운로드 실행
        clientCSVDownload(
            headerString,
            csvRows,
            `우리플랫폼_조사통계.csv`
        );
        
        console.log("CSV 다운로드 완료! ✅");
        return { success: true };
        
    } catch (error) {
        console.error("CSV 다운로드 실패:", error.message);
        
        // 사용자에게 알림
        alert(`다운로드 실패: ${error.message}`);
        
        return { success: false, error: error.message };
    } finally {
        console.log("다운로드 프로세스 종료");
    }
}

// 버튼 클릭 핸들러
const handleDownload = async () => {
    const { page, size, orderBy, sort, ...rest } = queryInstance;
    await downloadCSV(rest);
};

🎯 여러 비동기 작업 처리

async function fetchMultipleData() {
    try {
        console.log("여러 데이터 요청 시작...");
        
        // Promise.all: 모든 요청이 성공해야 함
        const [users, posts, comments] = await Promise.all([
            fetch("/api/users").then(r => r.json()),
            fetch("/api/posts").then(r => r.json()),
            fetch("/api/comments").then(r => r.json())
        ]);
        
        console.log("모든 데이터 로드 성공!");
        return { users, posts, comments };
        
    } catch (error) {
        console.error("데이터 로드 실패:", error);
        return null;
    }
}

async function fetchMultipleDataSafely() {
    try {
        console.log("여러 데이터 요청 시작 (안전 모드)...");
        
        // Promise.allSettled: 일부 실패해도 계속 진행
        const results = await Promise.allSettled([
            fetch("/api/users").then(r => r.json()),
            fetch("/api/posts").then(r => r.json()),
            fetch("/api/comments").then(r => r.json())
        ]);
        
        // 결과 처리
        const [usersResult, postsResult, commentsResult] = results;
        
        return {
            users: usersResult.status === "fulfilled" ? usersResult.value : [],
            posts: postsResult.status === "fulfilled" ? postsResult.value : [],
            comments: commentsResult.status === "fulfilled" ? commentsResult.value : []
        };
        
    } catch (error) {
        console.error("예상치 못한 오류:", error);
        return { users: [], posts: [], comments: [] };
    }
}

7. 예외 처리 베스트 프랙티스 ✨

📌 1. 구체적인 에러 메시지

// ❌ 나쁜 예
throw new Error("오류");

// ✅ 좋은 예
throw new Error("사용자 ID가 유효하지 않습니다: " + userId);

📌 2. 적절한 에러 타입 사용

// ❌ 모든 에러를 동일하게 처리
try {
    // ...
} catch (error) {
    console.error("오류 발생");
}

// ✅ 에러 타입별로 다르게 처리
try {
    // ...
} catch (error) {
    if (error instanceof ValidationError) {
        handleValidationError(error);
    } else if (error instanceof NetworkError) {
        handleNetworkError(error);
    } else {
        handleUnknownError(error);
    }
}

📌 3. 필요한 곳에만 try-catch 사용

// ❌ 과도한 try-catch
function processUser(user) {
    try {
        const name = user.name;
    } catch (error) {
        // 불필요
    }
    
    try {
        const age = user.age;
    } catch (error) {
        // 불필요
    }
}

// ✅ 적절한 범위
function processUser(user) {
    try {
        validateUser(user);
        const result = transformUser(user);
        saveUser(result);
    } catch (error) {
        handleUserProcessingError(error);
    }
}

📌 4. 에러 로깅

// ✅ 에러 정보를 상세히 기록
function logError(error, context = {}) {
    console.error({
        message: error.message,
        name: error.name,
        stack: error.stack,
        timestamp: new Date().toISOString(),
        ...context
    });
}

try {
    // 위험한 작업
    riskyOperation();
} catch (error) {
    logError(error, {
        operation: "riskyOperation",
        userId: currentUser.id,
        params: { /* ... */ }
    });
}

📌 5. 에러 복구 전략

async function fetchWithRetry(url, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error("Request failed");
            return await response.json();
        } catch (error) {
            console.log(`시도 ${i + 1}/${maxRetries} 실패`);
            
            if (i === maxRetries - 1) {
                // 마지막 재시도도 실패
                throw new Error(`${maxRetries}번 시도 후 실패: ${error.message}`);
            }
            
            // 재시도 전 대기 (Exponential Backoff)
            await new Promise(resolve => 
                setTimeout(resolve, Math.pow(2, i) * 1000)
            );
        }
    }
}

// 사용
fetchWithRetry("/api/data")
    .then(data => console.log("데이터:", data))
    .catch(error => console.error("최종 실패:", error));

8. 실전 종합 예제 🚀

🎯 사용자 등록 시스템

// 커스텀 에러 정의
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "ValidationError";
        this.field = field;
    }
}

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

// 유효성 검증 함수들
function validateEmail(email) {
    if (!email) {
        throw new ValidationError("이메일을 입력해주세요", "email");
    }
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
        throw new ValidationError("올바른 이메일 형식이 아닙니다", "email");
    }
    return true;
}

function validatePassword(password) {
    if (!password) {
        throw new ValidationError("비밀번호를 입력해주세요", "password");
    }
    if (password.length < 8) {
        throw new ValidationError(
            "비밀번호는 8자 이상이어야 합니다", 
            "password"
        );
    }
    if (!/[A-Z]/.test(password)) {
        throw new ValidationError(
            "비밀번호에 대문자가 포함되어야 합니다",
            "password"
        );
    }
    if (!/[0-9]/.test(password)) {
        throw new ValidationError(
            "비밀번호에 숫자가 포함되어야 합니다",
            "password"
        );
    }
    return true;
}

function validateAge(age) {
    if (age === undefined || age === null) {
        throw new ValidationError("나이를 입력해주세요", "age");
    }
    if (typeof age !== "number" || isNaN(age)) {
        throw new ValidationError("나이는 숫자여야 합니다", "age");
    }
    if (age < 0) {
        throw new ValidationError("나이는 0 이상이어야 합니다", "age");
    }
    if (age > 150) {
        throw new ValidationError("올바른 나이를 입력해주세요", "age");
    }
    return true;
}

// 중복 체크 (시뮬레이션)
async function checkDuplicateEmail(email) {
    // 실제로는 데이터베이스 조회
    const existingEmails = ["test@example.com", "admin@example.com"];
    
    if (existingEmails.includes(email)) {
        throw new DuplicateError(
            "이미 사용 중인 이메일입니다",
            "email",
            email
        );
    }
    return true;
}

// 사용자 등록 함수
async function registerUser(userData) {
    console.log("=".repeat(50));
    console.log("회원가입 시작");
    console.log("=".repeat(50));
    
    try {
        const { email, password, age, name } = userData;
        
        // 1단계: 기본 유효성 검증
        console.log("1️⃣ 유효성 검증 중...");
        validateEmail(email);
        validatePassword(password);
        validateAge(age);
        console.log("✅ 유효성 검증 통과");
        
        // 2단계: 중복 체크
        console.log("2️⃣ 중복 체크 중...");
        await checkDuplicateEmail(email);
        console.log("✅ 중복 체크 통과");
        
        // 3단계: 사용자 생성 (시뮬레이션)
        console.log("3️⃣ 사용자 생성 중...");
        const newUser = {
            id: Date.now(),
            email,
            name,
            age,
            createdAt: new Date().toISOString()
        };
        console.log("✅ 사용자 생성 완료");
        
        // 4단계: 환영 이메일 발송 (시뮬레이션)
        console.log("4️⃣ 환영 이메일 발송 중...");
        await sendWelcomeEmail(email, name);
        console.log("✅ 환영 이메일 발송 완료");
        
        console.log("=".repeat(50));
        console.log("🎉 회원가입 성공!");
        console.log("=".repeat(50));
        
        return {
            success: true,
            user: newUser,
            message: "회원가입이 완료되었습니다"
        };
        
    } catch (error) {
        console.log("=".repeat(50));
        
        if (error instanceof ValidationError) {
            console.error("❌ 유효성 검증 실패");
            console.error(`필드: ${error.field}`);
            console.error(`메시지: ${error.message}`);
            
            return {
                success: false,
                type: "validation",
                field: error.field,
                message: error.message
            };
            
        } else if (error instanceof DuplicateError) {
            console.error("❌ 중복 데이터 발견");
            console.error(`필드: ${error.field}`);
            console.error(`값: ${error.value}`);
            console.error(`메시지: ${error.message}`);
            
            return {
                success: false,
                type: "duplicate",
                field: error.field,
                message: error.message
            };
            
        } else {
            console.error("⚠️ 예상치 못한 오류");
            console.error(error);
            
            return {
                success: false,
                type: "unknown",
                message: "회원가입 중 오류가 발생했습니다"
            };
        }
    } finally {
        console.log("=".repeat(50));
        console.log("회원가입 프로세스 종료");
        console.log("=".repeat(50));
    }
}

// 환영 이메일 발송 (시뮬레이션)
async function sendWelcomeEmail(email, name) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`📧 ${name}님께 환영 이메일 발송: ${email}`);
            resolve();
        }, 1000);
    });
}

// 테스트 케이스
async function runTests() {
    // ✅ 성공 케이스
    console.log("\n📝 테스트 1: 정상 회원가입");
    await registerUser({
        email: "alice@example.com",
        password: "SecurePass123",
        age: 25,
        name: "Alice"
    });
    
    // ❌ 이메일 유효성 실패
    console.log("\n📝 테스트 2: 잘못된 이메일");
    await registerUser({
        email: "invalid-email",
        password: "SecurePass123",
        age: 25,
        name: "Bob"
    });
    
    // ❌ 비밀번호 유효성 실패
    console.log("\n📝 테스트 3: 약한 비밀번호");
    await registerUser({
        email: "bob@example.com",
        password: "weak",
        age: 30,
        name: "Bob"
    });
    
    // ❌ 나이 유효성 실패
    console.log("\n📝 테스트 4: 잘못된 나이");
    await registerUser({
        email: "charlie@example.com",
        password: "SecurePass123",
        age: -5,
        name: "Charlie"
    });
    
    // ❌ 중복 이메일
    console.log("\n📝 테스트 5: 중복 이메일");
    await registerUser({
        email: "test@example.com",
        password: "SecurePass123",
        age: 28,
        name: "Test"
    });
}

// 테스트 실행
runTests();

🛒 쇼핑몰 주문 시스템

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

class Order {
    constructor() {
        this.items = [];
        this.status = "pending";
    }
    
    // 상품 추가
    addItem(productId, quantity, price) {
        try {
            // 유효성 검증
            if (!productId) {
                throw new OrderError(
                    "상품 ID가 필요합니다",
                    "INVALID_PRODUCT_ID"
                );
            }
            
            if (!quantity || quantity <= 0) {
                throw new OrderError(
                    "수량은 1개 이상이어야 합니다",
                    "INVALID_QUANTITY",
                    { quantity }
                );
            }
            
            if (!price || price < 0) {
                throw new OrderError(
                    "올바른 가격이 아닙니다",
                    "INVALID_PRICE",
                    { price }
                );
            }
            
            // 재고 확인 (시뮬레이션)
            const stock = this.checkStock(productId);
            if (stock < quantity) {
                throw new OrderError(
                    "재고가 부족합니다",
                    "OUT_OF_STOCK",
                    { productId, requested: quantity, available: stock }
                );
            }
            
            // 상품 추가
            this.items.push({ productId, quantity, price });
            console.log(`✅ 상품 추가: ${productId} x ${quantity}`);
            
            return { success: true };
            
        } catch (error) {
            if (error instanceof OrderError) {
                console.error(`❌ 상품 추가 실패 [${error.code}]:`, error.message);
                if (Object.keys(error.details).length > 0) {
                    console.error("상세 정보:", error.details);
                }
            } else {
                console.error("❌ 예상치 못한 오류:", error);
            }
            
            return { success: false, error: error.message };
        }
    }
    
    // 재고 확인 (시뮬레이션)
    checkStock(productId) {
        const stocks = {
            "P001": 10,
            "P002": 5,
            "P003": 0,
            "P004": 100
        };
        return stocks[productId] ?? 0;
    }
    
    // 총 금액 계산
    getTotal() {
        return this.items.reduce((sum, item) => {
            return sum + (item.price * item.quantity);
        }, 0);
    }
    
    // 주문 완료
    async checkout(paymentInfo) {
        console.log("\n" + "=".repeat(50));
        console.log("주문 처리 시작");
        console.log("=".repeat(50));
        
        try {
            // 1. 장바구니 검증
            console.log("1️⃣ 장바구니 검증 중...");
            if (this.items.length === 0) {
                throw new OrderError(
                    "장바구니가 비어있습니다",
                    "EMPTY_CART"
                );
            }
            console.log("✅ 장바구니 검증 완료");
            
            // 2. 결제 정보 검증
            console.log("2️⃣ 결제 정보 검증 중...");
            this.validatePaymentInfo(paymentInfo);
            console.log("✅ 결제 정보 검증 완료");
            
            // 3. 재고 재확인
            console.log("3️⃣ 재고 재확인 중...");
            for (const item of this.items) {
                const stock = this.checkStock(item.productId);
                if (stock < item.quantity) {
                    throw new OrderError(
                        "주문 처리 중 재고가 소진되었습니다",
                        "STOCK_DEPLETED",
                        { productId: item.productId }
                    );
                }
            }
            console.log("✅ 재고 확인 완료");
            
            // 4. 결제 처리
            console.log("4️⃣ 결제 처리 중...");
            await this.processPayment(paymentInfo, this.getTotal());
            console.log("✅ 결제 완료");
            
            // 5. 재고 차감
            console.log("5️⃣ 재고 차감 중...");
            this.reduceStock();
            console.log("✅ 재고 차감 완료");
            
            // 6. 주문 확정
            this.status = "completed";
            const orderId = `ORD-${Date.now()}`;
            
            console.log("=".repeat(50));
            console.log("🎉 주문 완료!");
            console.log(`주문 번호: ${orderId}`);
            console.log(`총 금액: ${this.getTotal().toLocaleString()}`);
            console.log("=".repeat(50));
            
            return {
                success: true,
                orderId,
                total: this.getTotal(),
                items: this.items
            };
            
        } catch (error) {
            this.status = "failed";
            
            console.log("=".repeat(50));
            console.error("❌ 주문 실패");
            
            if (error instanceof OrderError) {
                console.error(`오류 코드: ${error.code}`);
                console.error(`메시지: ${error.message}`);
                if (Object.keys(error.details).length > 0) {
                    console.error("상세:", error.details);
                }
            } else {
                console.error("예상치 못한 오류:", error);
            }
            
            console.log("=".repeat(50));
            
            return {
                success: false,
                error: error.message,
                code: error.code
            };
            
        } finally {
            console.log("주문 처리 프로세스 종료\n");
        }
    }
    
    // 결제 정보 검증
    validatePaymentInfo(paymentInfo) {
        if (!paymentInfo) {
            throw new OrderError(
                "결제 정보가 필요합니다",
                "MISSING_PAYMENT_INFO"
            );
        }
        
        const { cardNumber, cvv, expiry } = paymentInfo;
        
        if (!cardNumber || cardNumber.length !== 16) {
            throw new OrderError(
                "올바른 카드 번호를 입력해주세요",
                "INVALID_CARD_NUMBER"
            );
        }
        
        if (!cvv || cvv.length !== 3) {
            throw new OrderError(
                "올바른 CVV를 입력해주세요",
                "INVALID_CVV"
            );
        }
        
        if (!expiry) {
            throw new OrderError(
                "카드 만료일을 입력해주세요",
                "MISSING_EXPIRY"
            );
        }
        
        return true;
    }
    
    // 결제 처리 (시뮬레이션)
    async processPayment(paymentInfo, amount) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                // 10% 확률로 결제 실패 시뮬레이션
                if (Math.random() < 0.1) {
                    reject(new OrderError(
                        "결제 처리 실패",
                        "PAYMENT_FAILED",
                        { amount }
                    ));
                } else {
                    resolve();
                }
            }, 1000);
        });
    }
    
    // 재고 차감 (시뮬레이션)
    reduceStock() {
        this.items.forEach(item => {
            console.log(`  📦 ${item.productId} 재고 차감: ${item.quantity}`);
        });
    }
}

// 테스트
async function testOrderSystem() {
    const order = new Order();
    
    // 상품 추가
    order.addItem("P001", 2, 10000);
    order.addItem("P002", 1, 20000);
    order.addItem("P003", 1, 15000); // 재고 없음
    order.addItem("P004", 3, 5000);
    
    // 주문 완료 시도
    const result = await order.checkout({
        cardNumber: "1234567812345678",
        cvv: "123",
        expiry: "12/25"
    });
    
    console.log("최종 결과:", result);
}

testOrderSystem();

9. 마무리 🎉

🌟 핵심 정리

구문역할사용 시점
try오류 가능성 있는 코드 실행예외 발생 가능 영역
catch오류 발생 시 처리예외 처리 로직
finally항상 실행리소스 정리, 로깅
throw예외 발생사용자 정의 오류

🚀 예외 처리 체크리스트

✅ 해야 할 것

  1. 구체적인 에러 메시지 작성

    throw new Error(`사용자 ID ${userId}를 찾을 수 없습니다`);
  2. 적절한 에러 타입 사용

    class ValidationError extends Error { }
    class NetworkError extends Error { }
  3. finally로 리소스 정리

    finally {
        connection.close();
        clearTimeout(timer);
    }
  4. 에러 로깅

    console.error({
        message: error.message,
        stack: error.stack,
        context: { userId, action }
    });
  5. 재시도 로직 구현

    async function fetchWithRetry(url, maxRetries = 3) {
        // 재시도 로직
    }

❌ 하지 말아야 할 것

  1. 빈 catch 블록

    // ❌ 절대 하지 마세요!
    try {
        riskyOperation();
    } catch (error) {
        // 아무것도 안 함
    }
  2. 모든 에러를 똑같이 처리

    // ❌ 나쁜 예
    catch (error) {
        alert("오류 발생");
    }
  3. 과도한 try-catch

    // ❌ 불필요한 중첩
    try {
        try {
            try {
                // ...
            } catch (e3) { }
        } catch (e2) { }
    } catch (e1) { }

💡 실무 적용 가이드

1️⃣ API 호출 패턴

async function apiCall(endpoint, options = {}) {
    try {
        const response = await fetch(endpoint, options);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return await response.json();
    } catch (error) {
        console.error(`API 호출 실패 [${endpoint}]:`, error);
        throw error;
    }
}

2️⃣ 폼 유효성 검증 패턴

function validateForm(formData) {
    const errors = {};
    
    try {
        validateEmail(formData.email);
    } catch (error) {
        errors.email = error.message;
    }
    
    try {
        validatePassword(formData.password);
    } catch (error) {
        errors.password = error.message;
    }
    
    return {
        isValid: Object.keys(errors).length === 0,
        errors
    };
}

3️⃣ 비동기 작업 패턴

async function performAsyncTask() {
    let resource = null;
    
    try {
        resource = await acquireResource();
        await processResource(resource);
    } catch (error) {
        await handleError(error);
    } finally {
        if (resource) {
            await releaseResource(resource);
        }
    }
}
profile
프론트엔드 입문 개발자입니다.

0개의 댓글