프론트엔드/Javascript

에러 처리 (Error handling)

syleemomo 2021. 10. 9. 13:18
728x90

* 프로그램 에러 (error)

프로그램 에러란 말 그대로 프로그램을 실행하는 도중에 발생되는 오류를 의미한다. 에러가 발생하는 이유는 수십만가지다. 크게 문법에러와 논리에러가 있다. 문법에러는 코드를 작성하면서 자바스크립트 문법에 맞게 작성하지 않았거나, 오타가 난 경우이다. 논리에러는 문법에는 전혀 이상이 없고, 콘솔창에도 에러가 뜨지 않지만 논리적인 절차의 오류로 인하여 프로그램이 정상적으로 동작하지 않는 경우이다. 

 

* 에러를 처리하는 이유 (디버깅 목적, 신뢰성 있는 코드, 유지보수 & 디버깅)

에러가 발생하면 스크립트는 동작을 멈추고 (프로그램이 중단되고), 콘솔창에 에러가 출력된다. 프로그램이 실행되다가 도중에 멈추면 위험한 경우가 있다. 예를 들어 자율주행 차량이 스스로 운전하다가 프로그램 에러가 발생하여 차량이 도로에서 멈추면 사고가 발생한다.

또한, 개발자가 아무리 천재라고 하더라도 코드를 작성할때 한번도 에러가 발생하지 않도록 코드를 작성하는 것은 불가능하다. 그러므로 에러가 발생된 위치와 이유를 알고, 제대로 동작하도록 코드를 수정하기 위해서는 에러를 처리하는 것이 좋다. 

 

* try, catch 와 에러 핸들링 

try {

  // 코드...

} catch (err) {

  // 에러 핸들링

}

try ~ catch 문법은 try 와 catch 라는 두개의 블럭으로 구성된다. try ~ catch 문법의 동작 알고리즘은 다음과 같다.

1. try {...} 안의 코드를 실행한다.
2. try {...} 코드블럭에 에러가 없으면 try 의 전체 코드블럭이 실행되고, catch 블럭은 건너띈다.
3. try {...} 코드블럭에 에러가 있으면 try 의 코드블럭 실행은 중단되고, catch 블럭이 실행된다.

catch 블럭 안의 err 는 try 코드블럭에서 발생한 에러가 무엇이고, 어느 위치에서 에러가 발생했는지 알려주는 에러 객체이다. 이렇게 try 의 코드블럭에서 에러가 발생해도 catch 블럭에서 에러를 처리하기 때문에 프로그램은 죽지 않고 계속 실행된다. 

try { // 에러없음
    console.log('try 블록 시작')  
    console.log('try 블록 끝')   
} catch(err) { 
    console.log('에러가 없으므로, catch는 무시됩니다.') 
}

해당 코드는 에러가 없는 경우이다. 

try {
    console.log('try 블록 시작')
    lalala // 에러, 변수가 정의되지 않음!
    console.log('try 블록 끝(절대 도달하지 않음)')
} catch(err) {
    console.log(`에러가 발생했습니다!`)
}

해당 코드는 에러가 발생한 경우이다. 

try {
    {{{{{{{{{{{{
} catch(e) {
    console.log("유효하지 않은 코드이기 때문에, 자바스크립트 엔진은 이 코드를 이해할 수 없습니다.")
}

try ~ catch 는 오직 실행가능한(runnable) 코드에서만 동작한다. 실행가능하다는 의미는 적어도 문법에러는 없는 코드이다. 위와 같이 중괄호 짝이 맞지 않는다면 try ~ catch 는 실행되지 않는다. 자바스크립트 엔진은 코드를 읽고 난 이후 코드를 실행한다. 코드를 읽는 중에 발생되는 에러를 "parse-tiem" 에러라고 하는데, 엔진은 해당 에러를 코드 실행중에 복구하지 못한다. 즉, 문법에러는 코드실행 전에 수정되어야 한다. 코드 실행중에 발생하는 에러를 런타임 에러(runtime error)라고 한다. 

try {
    setTimeout(function() {
      noSuchVariable // 스크립트는 여기서 죽습니다.
    }, 1000)
  } catch (e) {
    alert( "작동 멈춤" )
  }

try ~ catch 는 동기적으로 동작한다. 다음과 같이 비동기적으로 동작하는 setTimeout 안의 에러는 잡아내지 못한다. 왜냐하면 try ~ catch 가 실행되고 나서 1초 후에 에러가 발생하기 때문이다. 

setTimeout(function() {
    try {
      noSuchVariable // 이제 try..catch에서 에러를 핸들링 할 수 있습니다!
    } catch {
      alert( "에러를 잡았습니다!" )
    }
  }, 1000)

비동기적으로 동작하는 코드 내부의 에러를 try ~ catch 로 처리하려면 비동기 코드 내부에서 작성해야 한다. 

 

* 에러객체 

try {
  // ...
} catch(err) { // <-- '에러 객체', err 대신 다른 이름으로도 쓸 수 있음
  // ...
}

에러가 발생되면 자바스크립트는 에러에 대한 상세정보가 담긴 에러객체를 생성하고, catch 블럭에 인자로 전달한다. 

name 에러명 (ex. 정의되지 않은 에러의 에러명은 "ReferenceError")
message 에러 상세내용을 담은 메시지
stack 에러가 발생하기까지 실행된 함수의 호출순서(스택)

에러 객체에는 위와 같은 주요 프로퍼티가 있다. 

try {
    lalala // 에러, 변수가 정의되지 않음!
} catch(err) {
    console.log(err.name) // ReferenceError
    console.log(err.message) // lalala is not defined
    console.log(err.stack) // ReferenceError: lalala is not defined at ... (호출 스택)

    // 에러 전체를 보여줄 수도 있습니다.
    // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다.
    console.log(err) // ReferenceError: lalala is not defined
}

해당 코드의 에러 객체를 출력하면 아래와 같다. 

에러 객체의 주요내용

try {
  // ...
} catch { // <-- (err) 없이 쓸 수 있음
  // ...
}

에러에 대한 상세정보가 필요하지 않으면 위와 같이 작성해도 되지만, 구식 브라우저에서는 동작하지 않는다. 



  function add(){
    return a + b
  }
  function getInfo(){
    add()
  }

  try{
    getInfo()
  }catch(err){
    console.log(err.stack)
  }

에러객체의 stack 프로퍼티는 에러가 발생한 위치를 프로그램 흐름에 따라 역추적한다.

 

* try ~ catch 활용하기 

let json = '{"name":"John", "age": 30}' // 서버로부터 전달받은 데이터

let user = JSON.parse(json) // 전달받은 문자열을 자바스크립트 객체로 변환

// 문자열 형태로 전달받은 user가 프로퍼티를 가진 객체가 됨
console.log( user.name ) // John
console.log( user.age )  // 30

앞서 JSON 문자열로 인코딩된 값을 자바스크립트에서 사용할 수 있게 해주는 JSON.parse 메서드를 배운적이 있다. 해당 메서드는 주로 서버로부터 네트워크를 통해 전달된 데이터를 디코딩하기 위한 용도로 사용된다. 

let json = "{ bad json }"

try {
  let user = JSON.parse(json) // <-- 여기서 에러가 발생하므로
  console.log( user.name ) // 이 코드는 동작하지 않습니다.
} catch (e) {
  // 에러가 발생하면 제어 흐름이 catch 문으로 넘어옵니다.
  console.log( "데이터에 에러가 있어 재요청을 시도합니다." )
  console.log( e.name )
  console.log( e.message )
}

JSON 문법에러

해당 코드와 같이 서버에서 전송된 JSON 형식이 잘못된 경우 JSON.parse 를 사용하면 에러가 발생하여 프로그램이 중단된다. 프로그램이 중단되면 웹사이트의 경우 사용자는 개발자 콘솔을 확인하지 않는 이상 절대 원인을 알지 못한다. 하지만 사용자는 에러의 원인을 알지 못하면 답답해한다. 이를 해결하기 위하여 위의 코드와 같이 try ~ catch 를 사용하여 발생한 에러를 적절히 처리할 수 있다. 

해당 코드에서는 단순히 서버에 데이터를 재요청한다는 메세지만 출력하였지만, 실제로 JSON 형식이 잘못된 데이터가 브라우저로 전송된 경우 try ~ catch 를 이용하여 서버에 다시 데이터를 요청할 수 있다. 또는 사용자에게 에러의 원인을 팝업창으로 알려주거나, 다른 대안을 제시할 수도 있다. 아니면 서버에 해당 에러에 대해 기록할 수 있다. 

 

* 직접 에러 만들어서 던지기 - throw 

const json = '{ "age": 30 }' // 불완전한 데이터

try {
  const user = JSON.parse(json) // <-- 에러 없음
  console.log( user.name ) // 이름이 없습니다!
} catch (e) {
  console.log( "실행되지 않습니다." )
}

 해당 코드에서 JSON 데이터는 문법적으로 오류가 없다. 그러므로 JSON.parse 는 정상적으로 실행이 된다. 하지만 user 객체에는 name 프로퍼티가 존재하지 않으므로 콘솔창에 undefined 를 출력한다. 이러한 경우 throw 키워드를 사용하면 개발자가 직접 에러를 발생시키고, 이를 처리할 수 있다. 

throw [에러객체]
throw 1
throw "잘못된 JSON 형식입니다."

throw 키워드 다음에는 이론적으로 숫자, 문자열 등의 원시형 타입도 에러객체로 사용할 수 있다. 

var error = new Error(message)
var error = new SyntaxError(message)
var error = new ReferenceError(message)

하지만 내장 에러객체와의 호환성을 위하여 에러 객체에 name, message 프로퍼티를 넣어주는 것을 권장한다. 자바스크립트에는 Error, SyntaxError, ReferenceError, TypeError 등의 표준 에러객체를 생성할 수 있는 생성자를 지원한다. 이를 활용하여 에러객체를 만들수 있다. 

let error = new Error("이상한 일이 발생했습니다. o_O")

console.log(error.name) // Error
console.log(error.message) // 이상한 일이 발생했습니다. o_O

표준 에러객체를 생성하는 생성자 함수를 이용하여 만든 에러객체는 위와 같다. 에러객체의 name 프로퍼티는 생성자 함수의 이름과 동일하고, message 프로퍼티는 생성자 함수로 전달된 인자값이 출력된다.  

try {
    JSON.parse("{ 잘못된 형식의 json o_O }")
} catch(e) {
    console.log(e.name) // SyntaxError
    console.log(e.message) // Unexpected token b in JSON at position 2
}

잘못된 형식의 JSON 데이터를 인코딩하면 SyntaxError 가 발생한다. 

const json = '{ "age": 30 }' // 불완전한 데이터

try {
  const user = JSON.parse(json) // <-- 에러 없음
  if (!user.name) {
    throw new SyntaxError("불완전한 데이터: 이름 없음") 
  }
  console.log( user.name )
} catch(e) {
  console.log( "JSON Error: " + e.message ) // JSON Error: 불완전한 데이터: 이름 없음
}

해당 코드는 회원(user) 정보에 이름이 존재하지 않을시 SyntaxError 를 던진다. 그러므로 user.name 은 절대 콘솔창에 출력되지 않고, catch 블럭에서 에러를 출력한다.  

const json = '{ 잘못된 형식 }' // 불완전한 데이터

try {
  const user = JSON.parse(json) // <-- 에러 발생
  if (!user.name) {
    throw new SyntaxError("불완전한 데이터: 이름 없음") 
  }
  console.log( user.name )
} catch(e) {
  console.log( "JSON Error: " + e.message ) // JSON Error: 불완전한 데이터: 이름 없음
}

물론 위와 같이 JSON 형식이 잘못되더라도 에러가 발생된다. 해당 에러는 자바스크립트에서 자동으로 던진 에러이다. 

 

* 에러 다시 던지기 

"use strict"

const json = '{ "age": 30 }' // 불완전한 데이터

try {
  user = JSON.parse(json) // <-- user 앞에 let을 붙이는 걸 잊었네요.
  // ...
} catch(err) {
  console.log("JSON Error: " + err) // JSON Error: ReferenceError: user is not defined
  // (실제론 JSON Error가 아닙니다.)
}

catch 블럭은 원래 try 블록에서 발생한 모든 에러를 잡으려는 목적으로 만들어졌다. 그런데 해당 코드에서는 에러를 잡아주기는 하지만, 에러종류와 상관없이 JSON Eerror 메시지를 보여준다. 이런 식으로 에러종류와 관계없이 동일한 에러 메세지를 보여주는 것은 디버깅을 어렵게 만들기 때문에 좋지 않다. 참고로 "use strict" 을 설정하면 let, const, var 키워드가 빠진 경우 에러를 발생시킨다.

1. catch 는 모든 에러를 받는다.
2. catch(err){...} 블럭 안에서 예상되는 에러에 대한 처리를 한다.
3. 에러 처리방법을 모르면 throw err 를 해서 해당 에러를 처리한다.

이러한 문제를 해결하기 위하여 다시 던지기(rethrowing) 을 사용한다. catch 는 예상되는 에러만 처리하고 나머지는 다시 던진다. 

"use strict"

const json = '{ "age": 30 }' // 불완전한 데이터

try {
  user = JSON.parse(json) // <-- user 앞에 let을 붙이는 걸 잊었네요.
  // ...
} catch(err) {
  if(err instanceof ReferenceError){
    console.log('참조에러가 발생하였습니다.', err)
  }else if(err instanceof SyntaxError){
    console.log("JSON 형식이 올바르지 않습니다.", err) 
  }
}

해당 코드는 아래와 같이 ReferenceError 를 발생시킨다. 

ReferenceError 발생

"use strict"

const json = '{ 잘못된 형식 }' // 불완전한 데이터

try {
  user = JSON.parse(json) // <-- user 앞에 let을 붙이는 걸 잊었네요.
  // ...
} catch(err) {
  if(err instanceof ReferenceError){
    console.log('참조에러가 발생하였습니다.', err)
  }else if(err instanceof SyntaxError){
    console.log("JSON 형식이 올바르지 않습니다.", err) 
  }
}

해당 코드는 아래와 같이 SyntaxError 를 발생시킨다. 

SyntaxError 발생

"use strict"

const json = '{ "age": 30 }' // name 속성이 빠진 회원정보

try {
  const user = JSON.parse(json) 
  if(!user.name){
    throw new TypeError("회원 정보에 이름이 없습니다.")
  }
} catch(err) {
  if(err instanceof ReferenceError){
    console.log('참조에러가 발생하였습니다.', err)
  }else if(err instanceof SyntaxError){
    console.log("JSON 형식이 올바르지 않습니다.", err) 
  }else if(err instanceof TypeError){
    console.log(err.message)
  }
}

에러처리 방법을 모르면 위와 같이 throw 키워드를 사용해서 에러를 처리한다.

TypeError 발생

"use strict"

const json = '{ "age": 30 }' 

try {
  const user = JSON.parse(json) 
  if(!user.name){
    throw new TypeError("회원 정보에 이름이 없습니다.")
  }
} catch(err) {
  if(err instanceof ReferenceError){
    console.log('참조에러가 발생하였습니다.', err)
  }else if(err instanceof SyntaxError){
    console.log("JSON 형식이 올바르지 않습니다.", err) 
  }else if(err instanceof TypeError){
    console.log(err.message)
  }else{
    throw err // 정체 불명의 에러 다시 던지기 
  }
}

catch 블럭에서 다시 던진 에러는 try ~ catch 바깥으로 전달된다. 

"use strict"

const json = '{ "age": 200, "name": "sunrise" }' // 나이 범위가 비정상적인 회원

try {
  const user = JSON.parse(json) 
  if(!user.name){
    throw new TypeError("회원 정보에 이름이 없습니다.")
  }
  if(user.age > 130){
    throw new RangeError("회원의 나이가 범위를 벗어났습니다.")
  }
} catch(err) {
  if(err instanceof ReferenceError){
    console.log('참조에러가 발생하였습니다.', err)
  }else if(err instanceof SyntaxError){
    console.log("JSON 형식이 올바르지 않습니다.", err) 
  }else if(err instanceof TypeError){
    console.log(err.message)
  }else{
    throw err // 정체 불명의 에러 다시 던지기 
  }
}

이때 바깥에 try ~ catch 가 없으면 프로그램은 아래와 같이 중단된다. 

function getUserInfo(){
  "use strict"

  const json = '{ "age": 200, "name": "sunrise" }' // 나이 범위가 비정상적인 회원

  try {
    const user = JSON.parse(json) 
    if(!user.name){
      throw new TypeError("회원 정보에 이름이 없습니다.")
    }
    if(user.age > 130){
      throw new RangeError("회원의 나이가 범위를 벗어났습니다.")
    }
  } catch(err) {
    if(err instanceof ReferenceError){
      console.log('참조에러가 발생하였습니다.', err)
    }else if(err instanceof SyntaxError){
      console.log("JSON 형식이 올바르지 않습니다.", err) 
    }else if(err instanceof TypeError){
      console.log(err.message)
    }else{
      throw err // 정체 불명의 에러 다시 던지기 
    }
  }
}

getUserInfo()

동일한 코드를 getUserInfo 함수 내부로 감싸주었다. 

function getUserInfo(){
  "use strict"

  const json = '{ "age": 200, "name": "sunrise" }' // 나이 범위가 비정상적인 회원

  try {
    const user = JSON.parse(json) 
    if(!user.name){
      throw new TypeError("회원 정보에 이름이 없습니다.")
    }
    if(user.age > 130){
      throw new RangeError("회원의 나이가 범위를 벗어났습니다.")
    }
  } catch(err) {
    if(err instanceof ReferenceError){
      console.log('참조에러가 발생하였습니다.', err)
    }else if(err instanceof SyntaxError){
      console.log("JSON 형식이 올바르지 않습니다.", err) 
    }else if(err instanceof TypeError){
      console.log(err.message)
    }else{
      throw err // 정체 불명의 에러 다시 던지기 
    }
  }
}

try {
  getUserInfo()
} catch (err) {
  console.log( "External catch got: " + err) // 다시 던진 에러를 잡음
}

이때 바깥에 try ~ catch 블럭이 있다면 여기서 던져진 에러를 잡을 수 있다. 이렇게 하면 내부의 try ~ catch 블럭은 예상되는 에러만 처리하고, 알수 없는 정체불명의 에러들은 바깥에 있는 try ~ catch 가 처리한다. 

다시 던진 에러 잡기

 

* try ~ catch ... finally 

try ~ catch 는 마지막에 finally 블럭을 추가할 수 있다.  finally 는 작업을 종료하고, 프로그램이 정상적이든 에러가 나든 마지막에 항상 실행하고 싶을때 사용된다. 

try {
   ... 코드를 실행 ...
} catch(e) {
   ... 에러 핸들링 ...
} finally {
   ... 항상 실행 ...
}

finally 는 try 블럭의 실행이 끝난후 에러가 없는 경우 실행된다. 또한, 에러가 있는 경우 catch 블럭의 실행이 끝난후에도 실행될 수 있다. 

try {
  alert( 'try 블록 시작' )
  if (confirm('에러를 만드시겠습니까?')) 이상한_코드()
} catch (e) {
  alert( 'catch' )
} finally {
  alert( 'finally' )
}

해당 코드는 두가지 경우로 실행된다. 컨펌창에서 [확인] 버튼을 클릭하면 try -> catch -> finally 순으로 실행된다. 반대로 [취소]하면 try -> finally 순으로 실행된다. 

const num = +prompt("양의 정수를 입력해주세요.", 35)

let diff, result

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("음수나 정수가 아닌 값은 처리할 수 없습니다.")
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}

const start = Date.now()

try {
  result = fib(num)
} catch (e) {
  result = 0
} finally {
  diff = Date.now() - start
}

console.log(result || "에러 발생")

console.log( `연산 시간: ${diff}ms` )

피보나치 함수의 연산시간을 측정하려고 하는 경우 함수 실행전에 측정을 시작하고, 함수 실행이 완료된후 측정을 종료하면 된다. 이때 에러가 발생되면 어떻게 할까? fib(n)에는 음수나 정수가 아닌 수를 입력할 경우 에러가 발생된다. 이러한 경우 함수가 정상적으로 종료되든 함수실행 도중에 에러가 발생하든 finally 블럭을 추가하면 무조건 연산시간은 측정할 수 있다. 

num 이 35인 경우

코드를 실행하고 프롬프트 입력창에 35를 입력하면 try 블럭이 실행된 다음에 finally 블럭이 정상적으로 실행되면서 연산시간이 측정된다. -1 을 입력하면 에러가 발생되고, 연산시간은 아래와 같이 0ms 가 되지만, 어찌되었든 연산시간은 제대로 측정된다. 

num 이 -1인 경우

 

function func() {

  try {
    return 1

  } catch (e) {
    /* ... */
  } finally {
    alert( 'finally' )
  }
}

alert( func() ) // finally 안의 alert가 실행되고 난 후, 실행됨

finally 는 try ~ catch 절을 빠져나가는 어떠한 경우에도 실행된다. 해당 코드처럼 함수 안에서 return 을 사용하여 명시적으로 함수 밖으로 빠져나가려고 할때도 마찬가지로 finally 는 실행된다. 즉, 값이 반환되기 전에 finally 블럭이 먼저 실행되고, func() 함수에서 반환된 값이 얼럿창에 출력된다. 

function func() {
  // 무언가를 측정하는 경우와 같이 끝맺음이 있어야 하는 프로세스
  try {
    // ...
  } finally {
    // 스크립트가 죽더라도 완료됨
  }
}

catch 블럭이 없는 try ~ finally  구문도 상황에 따라 유용하게 활용할 수 있다. try ~ finally 에서는 에러처리는 하고 싶지 않지만, 시작한 프로세스가 마무리 되었는지 확실히 하고 싶을때 사용할 수 있다. 해당 코드는 catch 블럭이 없기 때문에 try 블럭에서 발생된 에러는 항상 밖으로 빠져나온다. finally 는 함수가 종료되기 전에 무조건 실행된다. 

function func() {
  "use strict"

  // 무언가를 측정하는 경우와 같이 끝맺음이 있어야 하는 프로세스
  try {
    user = 1
  } finally {
    // 스크립트가 죽더라도 완료됨
    console.log('프로세스가 종료되었습니다.')
  }
}

func()
console.log('함수종료됨!')

해당 코드는 func 함수가 종료되기 직전에 finally 블럭이 실행된다. 이후 ReferenceError 가 발생하였기 때문에 함수호출 위치로 되돌아왔을때 프로그램은 종료되고, 아래쪽의 콘솔창은 실행되지 않는다. 

함수종료 직전 finally 실행

 

 

* 전역 환경에서 에러 처리하기 - try, catch 없이 

만약 try ~ catch 밖에서 치명적인 에러가 발생하면 어떻게 할까요? 대처방법이 있을까요? 어딘가에 에러내역을 기록해놓거나 사용자에게 에러가 났음을 알려줄 수 있다. 

자바스크립트 명세서에는 이런 치명적인 에러에 대응하는 방법이 명시되어 있지 않지만, try ~ catch 에서 처리하지 못한 에러를 잡는 것은 중요하기 때문에 자바스크립트 호스트 환경 대다수는 자체적인 에러처리 기능을 제공한다. Node.js 의 process.on("uncaughtException") 이나 브라우저 환경의 window.onerror 를 이용하면 프로그램 전체에서 발생되는 에러를 처리할 수 있다. 

window.onerror = function(message, url, line, col, error) {
  // ...
};

window.onerror 프로퍼티에 이벤트핸들러 함수를 등록해두면 브라우저 전체에서 예상치 못한 에러가 발생했을때 해당 핸들러 함수가 에러를 처리한다. message 는 에러 메세지이다. url 은 에러가 발생한 스크립트의 url 이다. line, col 은 에러가 발생한 위치의 줄과 열번호이다. error 는 에러객체이다. 

window.onerror = function(message, url, line, col, error) {
  console.log(`${message}\n At ${line}:${col} of ${url}`)
}

function readData() {
  badFunc() // 에러가 발생한 장소
}

readData()

해당 코드는 readData 함수 내부에서 에러가 발생하였고, window.onerror 에 등록된 이벤트핸들러 함수가 아래와 같이 발생된 에러를 처리하고 있다. 하지만 window.onerror 는 죽어버린 스크립트를 복구하지는 못한다. 다만 개발자에게 브라우저 환경에서 에러가 어딘가 발생했음을 알려주기 위한 용도로 사용된다. 

 

전역 에러처리

 

 

 

 

 

 

 

 

 

 

console.log

console.error

throw

 

에러처리와 콜스택

 

728x90