[JavaScript] 커스텀 에러 객체 만드는 법 - try catch, throw, exception 처리

 


 이번 포스팅에서는 프론트엔드에서 'try...catch' 문을 통해 처리하는 예외처리 작성 패턴과 사용 처에 따른 Error 객체의 생성에 대해 다룹니다.



1. 예제 구성

 일반적으로 UI에서 저장할 때 발생하는 프로세스 중 발생하는 오류를 에제로 사용 합니다.

아래 그림에서

1) No : 저장하려는 자료에 대한 유효성 검사 실패 (날짜형, 문자형 등)

2) Fail : Ajax 등과 같은 Service로 전달하는 통신과정에서의 오류 (400, 500번대의 기타 오류들)

3) Fail : 정상응답(200)을 회신 받았으나, Service 단의 저장과정에서 발생한 예외처리로 정상처리되지 않은 경우의 실패


2. Custom Error

1) BaseError: Error Class를 상속받아 'catch' 구문으로 발생한 Error 객체를 다룸.

2) FormatError: 입력값의 자료형에 대한 오류를 다루기 위한 클래스

3) LogicalError: 비즈니스 로직상 적합하지 않은 자료 행을 구분하기 위한 에러 클래스

4) APISendError: API 송/수신간의 오류를 구분하기 위한 에러 클래스 

5) SaveError: 상기 상세 구분을 위한 3가지 Error Class를 감싸기 위한 에러 클래스

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class BaseError extends Error {
    constructor(message) {
        super(message);
        this.name = this.constructor.name;
    }
}

class FormatError extends BaseError {
    constructor(message, seq, property) {
        super(message);
        this.seq = seq;
        this.property = property;
    }
}

class LogicalError extends BaseError {
    constructor(message, seq, property, category){
        super(message);
        this.seq = seq;
        this.property = property;
        this.category = category;
    }
}

class APISendError extends BaseError {
    constructor(message, url, code){
        super(message);
        this.url = url;
        this.code = code;
    }
}

class SaveError extends BaseError {
    constructor(message){
        super(message);
    }
}

  • 3 : 상위 클래스인 'Error' 로의 message 값 반영
  • 4 : BaseError를 상속할 객체들의 생성 클래스 구분. (instanceof 구문으로 ErrorClass 구분) 
  • 11 : 예외처리 과정에서 입력값의 식별값(seq)을 Error객체에 포함
  • 12: 예외처리 과정에서 입력값의 속성인자(property)을 Error객체에 포함
  • 21 : 예외처리 과정에서 입력값의 유형인자(category)을 Error객체에 포함
  • 28 : API 전송 URL
  • 29 : API 오류처리 과정에서 식별할 오류코드



3. 호출 구조

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** 유효성 검사 부 */
const Validation = (rowData) => {

    /** Write Validation Logic */
    throw

}

/** 데이터 전송 부 */
const SendData = (sendData) => {

    /** Write API Send Logic */
    throw

}

/** '저장' 로직 */
const SaveData = (sourceData) => {

    /** 1. 유효성 검사 */
    try{
        sourceData.map(row => Validation(row));
    }
    catch(error) {
        if(error instanceof FormatError){ throw new SaveError(); }
        else if (error instanceof LogicalError) { throw new SaveError(); }
        else { throw error; }
    }

    /** 2. API 전송 (Save) */
    try{
        SendData(sourceData);
    }
    catch(error){
        if(error instanceof APISendError){ throw new SaveError(); }
    }
}

/** main */
try{
    const sourceData = [
        { seq: '001', product: 'apple', price: '1000', expireDt: '2023-12-31', soldOut: false},
        { seq: '002', product: 'banaa', price: '2000', expireDt: '2024-01-00', soldOut: false},
        { seq: '003', product: 'melon123', price: '4000', expireDt: '2033-10-01', soldOut: true},
        { seq: '004', product: 'grape', price: '1500', expireDt: '2023-11-15', soldOut: false},
        { seq: '005', product: 'cherry', price: '4400', expireDt: '2023-11-15', soldOut: true},
    ];

    SaveData(sourceData);

}catch(e){
    if(e instanceof SaveError) { alert(e.message); }
    else { throw e; }
}

  • 41 : 샘플 자료형. 행 단위로 과일상품에 대한 정보를 관리함.
  • 49 : 저장 함수 호출
  • 22 : "1) 유효성 검사" 수행.
  • 25 : 자료형 검사 (FormatError)에 대한 예외처리
  • 26 : 논리형 검사 (LogicalError)에 대한 예외처리
  • 27 : 정의된 두 가지 검사 외의 예외처리
  • 32 : "2) 데이터 전송" 수행.
  • 35 : 데이터 전송 간의 오류 (APISendError)에 대한 예외처리
  • 52 : "25, 26, 35" 라인에서 감싸놓은 저장에 대한 오류 (SaveError)에 대한 포괄적인 예외처리



4. 유효성 검사 정의 및 예외처리

 정규식을 통해 아래와 같이 유효성검사 여부에 따라, 예외를 발생시킨다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const DEFAULT_REGEXP_ALPHABET = /^[a-zA-Z]+$/;        // 영문자(알파벳)
const DEFAULT_REGEXP_ALPHANUM = /^[0-9a-zA-Z]+$/;     // 영문자 + 숫자
const DEFAULT_REGEXP_FIGURE = /^[0-9]+$/;             // 숫자
const DEFAULT_REGEXP_DATE = /^(19[7-9][0-9]|20\d{2})-(0[0-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/;  // 날짜

/** 유효성 검사 부 */
const Validation = (rowData) => {

    if(!(DEFAULT_REGEXP_ALPHABET).test(rowData.product)){
        throw new FormatError(`자료유형 오류 : 영문자(알파벳)`, rowData.seq, rowData.product );
    }

    if(!(DEFAULT_REGEXP_FIGURE).test(rowData.price)){
        throw new FormatError(`자료유형 오류 : 숫자`, rowData.seq, rowData.price );
    }

    if(!(DEFAULT_REGEXP_DATE).test(rowData.expireDt)){
        throw new FormatError(`자료유형 오류 : 날짜`, rowData.seq, rowData.expireDt);
    }

    if(rowData.soldOut){
        const category = `STOCK`;
        throw new LogicalError(`논리유형 오류`, rowData.seq, rowData.soldOut ? '품절' : '재고', category);
    }

}

  • 9 : 제품명은 영문으로만 구성. 
  • 13 : 가격은 숫자로만 구성.
  • 18 : 날짜는 yyyy-MM-dd 유형으로 구성.
  • 21 : 품절여부 검사.


5. 데이터 전송 부 예외처리

 axios 전송에 대한 성공(then)에서의 처리실패에 대한 예외처리와 실패(catch)에서의 기타 통신상의 오류에서 예외를 발생시킨다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/** 데이터 전송 부 */
const SendData = (sendData) => {
    
    const DATA_SAVE_API = 'http://test.com';

    axios.post(`${DATA_SAVE_API}`, {params: sendData})
        .then((response) => {

            /** Success Process */

            if(response.state = 'invalid'){
                throw new APISendError('유효하지 않은 응답', DATA_SAVE_API, response.code);
            }
        })
        .catch((error) => {
            throw new APISendError('전송 실패', DATA_SAVE_API, '');
        })

}



6. 저장 부, 예외처리 감싸기와 메시징 처리

 유효성 검사, 데이터 전송 과정에서 발생한 예외를 감싸고 메시징처리하여 핸들링 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** '저장' 로직 */
const SaveData = (sourceData) => {

    /** 1. 유효성 검사 */
    try{
        sourceData.map(row => Validation(row));
    }
    catch(error) {
        if(error instanceof FormatError){ throw new SaveError(`[${error.message}]\n(SEQ): ${error.seq}\n입력값: ${error.property}`); }
        else if (error instanceof LogicalError) { throw new SaveError(`[${error.message}]\n(SEQ): ${error.seq}\n입력값: ${error.property}\n구분: ${error.category}`); }
        else { throw error; }
    }

    /** 2. API Send (Save) */
    try{
        SendData(sourceData);
    }
    catch(error){
        if(error instanceof APISendError){ throw new SaveError(`[API Error]\nCode: ${error.code}\nMessage: ${error.message}`); }
    }
}



7. 전체 코드

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
const DEFAULT_REGEXP_ALPHABET = /^[a-zA-Z]+$/;        // 영문자(알파벳)
const DEFAULT_REGEXP_ALPHANUM = /^[0-9a-zA-Z]+$/;     // 영문자 + 숫자
const DEFAULT_REGEXP_FIGURE = /^[0-9]+$/;             // 숫자
const DEFAULT_REGEXP_DATE = /^(19[7-9][0-9]|20\d{2})-(0[0-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/;  // 날짜

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

class FormatError extends BaseError {
    constructor(message, seq, property) {
        super(message);
        this.seq = seq;
        this.property = property;
    }
}

class LogicalError extends BaseError {
    constructor(message, seq, property, category){
        super(message);
        this.seq = seq;
        this.property = property;
        this.category = category;
    }
}

class APISendError extends BaseError {
    constructor(message, url, code){
        super(message);
        this.url = url;
        this.code = code;
    }
}

class SaveError extends BaseError {
    constructor(message){
        super(message);
    }
}


(() => {

    /** 유효성 검사 부 */
    const Validation = (rowData) => {

        if(!(DEFAULT_REGEXP_ALPHABET).test(rowData.product)){
            throw new FormatError(`자료유형 오류 : 영문자(알파벳)`, rowData.seq, rowData.product );
        }

        if(!(DEFAULT_REGEXP_FIGURE).test(rowData.price)){
            throw new FormatError(`자료유형 오류 : 숫자`, rowData.seq, rowData.price );
        }

        if(!(DEFAULT_REGEXP_DATE).test(rowData.expireDt)){
            throw new FormatError(`자료유형 오류 : 날짜`, rowData.seq, rowData.expireDt);
        }

        if(rowData.soldOut){
            const category = `STOCK`;
            throw new LogicalError(`논리유형 오류`, rowData.seq, rowData.soldOut ? '품절' : '재고', category);
        }

    }

    /** 데이터 전송 부 */
    const SendData = (sendData) => {
        
        const DATA_SAVE_API = 'http://test.com';

        axios.post(`${DATA_SAVE_API}`, {params: sendData})
            .then((response) => {

                /** Success Process */

                if(response.state = 'invalid'){
                    throw new APISendError('유효하지 않은 응답', DATA_SAVE_API, response.code);
                }
            })
            .catch((error) => {
                throw new APISendError('전송 실패', DATA_SAVE_API, '');
            })

    }

    /** '저장' 로직 */
    const SaveData = (sourceData) => {

        /** 1. 유효성 검사 */
        try{
            sourceData.map(row => Validation(row));
        }
        catch(error) {
            if(error instanceof FormatError){ throw new SaveError(`[${error.message}]\n(SEQ): ${error.seq}\n입력값: ${error.property}`); }
            else if (error instanceof LogicalError) { throw new SaveError(`[${error.message}]\n(SEQ): ${error.seq}\n입력값: ${error.property}\n구분: ${error.category}`); }
            else { throw error; }
        }

        /** 2. API Send (Save) */
        try{
            SendData(sourceData);
        }
        catch(error){
            if(error instanceof APISendError){ throw new SaveError(`[API Error]\nCode: ${error.code}\nMessage: ${error.message}`); }
        }
    }
    

    try{
        const sourceData = [
            { seq: '001', product: 'apple', price: '1000', expireDt: '2023-12-31', soldOut: false},
            { seq: '002', product: 'banaa', price: '2000', expireDt: '2024-01-00', soldOut: false},
            // { seq: '003', product: 'melon123', price: '4000', expireDt: '2033-10-01', soldOut: true},
            { seq: '004', product: 'grape', price: '1500', expireDt: '2023-11-15', soldOut: false},
            { seq: '005', product: 'cherry', price: '4400', expireDt: '2023-11-15', soldOut: true},
        ];

        SaveData(sourceData);

    }catch(e){
        if(e instanceof SaveError) { alert(e.message); }
        else { throw e; }
    }

});

0 댓글