.Catch()의 Return - 자바스크립트 고수만 쓴다는 비동기 에러 처리 기술

JavaScript에서 비동기 함수 에러 처리에는 여러가지 방법이 있다. 이 중 Promise의 .catch 에서 return을 값을 잘 준다면 코드의 가독성을 높일 수 있다.


약간의 수정 만으로 가독성을 높이는 방법, Early Return도 알아보세요!

Early Return - 빠른 탈출을 통한 코드 가독성 상승
Early return 은 적은 비용으로 코드 가독성을 올릴 수 있는 가장 쉬운 방법 중에 하나이다. 가독성은 ”개발자가 이 코드를 읽기 쉬운가?”를 따지는 지표이며 높은 가독성은 나중에 기능을 추가해야 하거나 코드를 수정해야 할때 쉽고 빠르고 정확하게 할 수 있도록 해준다. 가독성이 좋지 못할경우 ”처음부터 다시 만들어야겠다”라는 결론을 만들어 내기에

    .then과 .catch만을 쓰기

    const data = await fetch('/api/something')
      .then((res) => res.json())
      .catch((err) => {
        console.log("Error: " + err.message)
      })
    
    console.log(data)

    자 이 코드에서 fetch()res.json() 함수가 에러를 반환한다면 어떻게 될까?

    물론 .catch()가 오류들을 잡아 console.log를 할 것이다. 하지만 그 다음 console.log(data)도 호출되게 된다.

    우리는 오류가 발생하면 Error만 출력되고 data는 뜨지 않도록 하고 싶다. 어떻게 할 수 있을까?

    const data = await fetch('/api/something')
      .then((res) => res.json())
      .then((data) => console.log(data))
      .catch((err) => {
        console.log("Error: " + err.message)
      })

    물론 이렇게도 할 수 있다. 하지만 가독성이 썩 좋아보이지는 않는다.

    게다가 이제 악조건을 하나 더 걸어보자. fetch()res.json()의 오류 말고도 /api/something API의 오류도 잡아야 한다고 가정해보자:

    {
      "success": false,
      "message": "Oh no!"
    }
    

    이제는 API가 보내준 success가 true, false 인지도 확인해야 한다. 괭장히 야매로 짠다면 다음과 같이 짤 수 있다.

    const data = await fetch('/api/something')
      .then((res) => res.json())
      .then((data) => {
        if (!data.success) {
          console.log("Error: " + data.message)
          return
        }
    
        console.log(data)
      })
      .catch((err) => {
        console.log("Error: " + err.message)
      })

    if문을 추가하였기 때문에 상당히 가독성이 떨어진 것을 볼 수 있다. 어떻게 하면 개선할 수 있을까?

    Await+Try Catch를 쓰기

    이런 상황에서 많은 개발자들은 다음과 같이 Try Catch 문법을 사용하려고 시도한다:

    try {
      const data = await fetch('/api/something')
        .then((res) => res.json())
    
      if (!data.success) {
        console.log("Error: " + data.error)
        return
      }
    
      console.log(data)
    } catch (err) {
      console.log("Error: " + err.message)
    }

    전 보다 코드가 깔끔해졌고 코드 순환 방향이 명확해 졌다. 하지만 여전히 코드 흐름을 깨는 try catch문은 가독성에 부정적인 영향을 준다.

    .catch에게 Return을 주기

    const data = await fetch('/api/something')
      .then((res) => res.json())
      .catch((err) => {
        return {
          success: false,
          message: err.message
        }
      })
    
    if (!data.success) {
      console.log("Error: " + data.message)
      return
    }
    
    console.log(data)

    이런 식으로 .catch 문에 return을 준다면 만약 위에 fetch(), res.json()에 문제가 발생할때 data를 { success: false, message: err.message } 로 변경하도록 만들 수 있다.

    화살표 함수는 return을 생략할 수 있으므로 더 줄이면 다음과 같다:

    const data = await fetch('/api/something')
      .then((res) => res.json())
      .catch((err) => ({
        success: false,
        message: err.message
      }))
    
    if (!data.success) {
      console.log("Error: " + data.message)
      return
    }
    
    console.log(data)

    한눈에 읽기 쉽고 흐름이 명확한 가독성이 높은 코드를 작성할 수 있다.

    Promise의 Chaining

    위 원리는 Promise의 Chaining 기능으로 인해 발생한다.

    이해를 쉽게 하기 위해 다음과 같이 Promise를 생성하는 코드가 있다고 가정하자:

    function someAsyncJob (willSuccess) {
      return new Promise((success, fail) => {
        if (willSuccess) {
          return success()
        }
    
        return fail()
      })
    }

    자바스크립트의 Promise는 비동기 작업이 성공할 경우 then의 첫번째 인자 함수를 실행한다.

    const returns = await someAsyncJob(true)
      .then(() => console.log('Success!')) // Prints: Success!
    
    console.log(returns) // Prints: undefined

    만약 then이 무언가를 return한다면 그 값은 다른 Promise 컨테이너에 담겨 보여지게 된다.

    const returns = await someAsyncJob(true)
      .then(() => 'Success!')
    
    console.log(returns) // Prints: Success!

    그래서 다음과 같은 작업도 가능하다.

    const returns = await someAsyncJob(true)
      .then(() => 'Success!')
      .then((prev) => prev + ' yay!')
    
    console.log(returns) // Prints: Success! yay!

    catch도 마찬가지다.

    const returns = await someAsyncJob(false)
      .then(() => 'Success!')
      .then((prev) => prev + ' yay!')
      .catch(() => 'Failed...')
    
    console.log(returns) // Prints: Failed...

    즉, 오류가 발생했을때는 'Failed'로 대체하도록 할 수 있다는 것이다.

    then의 두번째 인자도 비슷하게 작동한다.

    참고로 then의 두번째 인자는 바로 앞 Promise가 Fail 했을때만 작동하고 catch는 then의 두번째 인자로 처리하지 못한 Fail들을 모두 잡는다.

    const returns = await someAsyncJob(true)
      .then(() => someAsyncJob(false), () => 'Failed First job...')
      .then(() => ' Success All!', () => 'Failed Second job...')
      .catch(() => 'Failed Something...')
    
    console.log(returns) // Prints: Failed Second job...

    마치며...

    평상시 async/await의 습관으로 놓치기 쉬운 Promise의 숨은 기능들을 알아보았다. 쓸일이 있을까 생각할 수 있지만 적절한 부분에서 사용한다면 고급 JavaScript 문법의 편리함을 얻을 수 있다.

    만약 틀린 내용이 있거나 Promise에 숨은 다른 사실이 있다면 댓글로 알려주세요!