새소식

반응형
언어/자바스크립트(JavaScript)

[JavaScript] JavaScript에서 우아하게 에러 핸들링(Error Handling)하기 (try-catch)

2024.11.18
  • -
반응형

1. 들어가며

JavaScript에서 에러 핸들링(Error Handling) 작업은 굉장히 유연하지만, 양날의 검과도 같습니다. 사실 에러 핸들링을 전혀 사용하지 않고도 굉장히 규모가 있는 서비스를 구현하는 경우도 많이 있습니다. 임의로 거대한 코드 블록을 try-catch 블록으로 감싸서 모든 잠재적 오류를 한 번에 처리할 수 있는 것이죠.

 

하지만 그러면 어떻게 될까요?

 

물론 코드 작성이야 빨라지겠지만, 견고함이 떨어지고 에러의 명확도가 떨어져 디버깅하기도 더 어렵습니다. 그리고 이러한 현상은 코드의 규모가 커지면 커질수록 더 강조되는 문제이죠.

 

이번 포스팅에서는 TypeScript와 ESLint를 활용하여 보다 명시적인 에러 핸들링을 처리하는 방법에 대해 공유하고자 합니다.

 

일단, 그 전에 먼저 자바스크립트의 에러 핸들링이 개선할 수 있는 여지가 있는 이유는 무엇일까요?

 

2. 문제 상황

자바스크립트에서 에러 핸들링이 어떤 경우에 잘못 사용되고 있는 것인지를 몇 가지 예로써 보여드리겠습니다.

 

시작에 앞서, 단순히 작은 코드 조각만을 보고 어떤 문제인지 진단하는 것임을 염두에 두어야 합니다.

 

먼저, try-catch 지옥의 예시입니다.(try-catch hell)

function outerFunction() {
  try {
    function firstLayer() {
      try {
        function secondLayer() {
          try {
            // Some code that might throw an error
          } catch (error) {
            console.error("Error in secondLayer: ", error);
          }
        }
        secondLayer();
      } catch (error) {
        console.error("Error in firstLayer: ", error);
      }
    }
    firstLayer();
  } catch (error) {
    console.error("Error in outerFunction: ", error);
  }
}

 

보기만 해도 토가 나오는 그런 코드입니다...

 

실제로 프로젝트 코드에서, try-catch가 중첩되어 있는 단일 함수를 볼 수 있는 경우는 그렇게 많지 않을 것입니다. 하지만 이 예제는 매우 현실적인 문제를 단순하게 추상화한 것인데요. 일반적으로 try-catch 블록은 중첩된 함수에서 찾을 수 있고 중첩이 많으면 오류를 효과적으로 추적하고 처리하기가 더 어려워질 수 있습니다.

 

다음 예시는 자바스크립트 에러 핸들링 시에 발생할 수 있는 또 다른 문제를 나타낸 것입니다. 여기서는 타입스크립트가 사용되었습니다.

function processData(data: string): number[] {
  try {
    const parsedData = JSON.parse(data);
    const value = parsedData.someProperty;
    return value.map((item) => item * 2);
  } catch (error) {
    console.error("An error occurred:", error);
  }
}

 

이 processData라는 함수는 실제 프로젝트 코드에서 볼 수 있을 정도의 합리적인 코드입니다. 그리고 솔직히 이대로도 그렇게 나쁘다는 생각은 들지 않습니다.

 

하지만 문제는 모든 줄에서 에러가 발생할 수 있지만 모두 동일한 방식으로 처리를 하고 있다는 것입니다. 타입스크립트는 존재하지 않는 값에 접근하고 있다는 경고를 제공하지 않습니다. 컴파일 시에는 parsedData에 일부 프로퍼티가 없거나 해당 값이 배열이 아닐 수 있다는 사실을 알 방법이 없습니다.

 

이러한 문제를 대처하기 위해 각 줄에 try-catch 블록을 둘러싸는 방법도 있겠지만, 이렇게 하면 코드가 매우 지저분해진다는 사실을 아래와 같이 확인할 수 있습니다.

function processData(data: string): number[] {
  let parsedData;
  try {
    parsedData = JSON.parse(data);
  } catch (error) {
    // handle parsing error
  }

  let value;
  try {
    value = parsedData.someProperty;
  } catch (error) {
    // handle property access error
  }

  let mappedValue;
  try {
    mappedValue = value.map((item) => item * 2);
  } catch (error) {
    // handle mapping error
  }

  return mappedValue;
}

 

이것보다 더 좋은 방법은 각 줄에 대해 처리하는 부분을 분리하도록 자체 helper 함수를 제작하여 적용하는 것일 겁니다. 하지만 에러를 더 세밀하게 처리하기 위해 들어가는 코드가 오히려 더 많아지기에 배보다 배꼽이 더 큰 상황이라고 할 수 있죠.

 

예를 들어 JSON.parse()에서 에러가 발생하면 이를 값으로 반환하지 않기 때문에 우리는 단순히 TypeScript를 사용하여 각 줄에 type을 세세히 추가해줄 수는 없는 노릇이기도 하고 말이죠.

  • 어찌됐건, 한 마디로 모든 잠재 에러를 식별하는 것을 전부 개발자에게 위임하고 싶지는 않은 것입니다.

 

다시 정리해보면 자바스크립트(그리고 타입스크립트)에서의 에러 핸들링 시의 문제는 아래 세 가지정도로 추려볼 수 있는 것 같습니다.

  • 존재하지 않는 값에 액세스하려고 할 때 TypeScript 컴파일러에서 경고를 표시하지 않기 때문에 이에 대해 개발자가 신경써서 오류를 잘 처리해야 할 책임이 있습니다.
  • try-catch 블록이 두 개 이상 있으면 코드를 읽기 어렵고, try-catch의 장황함 때문에 개발자가 오류가 발생한 지점에 대해 보다 세부적인 방식으로 오류를 해결하지 못하는 것 같습니다.
  • 예외(throw exception)를 던지면 에러를 일반적인 값으로 처리할 수 있는 능력을 잃게 됩니다. 이게 무슨 말이냐 하면, 에러 데이터 자체에 일반적인 값을 사용하면 로직의 흐름을 개선하는 데 도움이 될 뿐만 아니라 비용이 많이 드는 '스택 풀기'와 제어 흐름 변경을 피할 수 있기 때문에 성능도 향상됩니다. 또한 여러가지 에러를 한 번에 일괄 처리하는 것처럼 예외를 throw 할 때는 허용되지 않는 작업을 수행할 수도 있습니다.

 

간단한 예제를 볼 때는 좋은 코드를 작성하기 쉽지만, 코드의 규모가 큰 실제 프로젝트에서도 일관되게 작성하는 것은 훨씬 더 어려운 일입니다. 자바스크립트에서 Rust나 Go의 모든 이점을 누릴 수는 없지만, 해당 프레임워크에서 사용하는 몇 가지 원칙을 도입하여 자바스크립트 코드에 적용해 볼 수 있고 이를 ESLint로 강제할 수도 있습니다.

 

3. 주의 사항

앞으로 나올 예제에서 다루는 접근 방식이 모든 프로젝트에서 적합하지 않은 것은 다 아실 거라고 생각합니다!

 

예를 들어, 오픈소스 자바스크립트 라이브러리에서 작업하는 경우엔 개발자들이 으레 하는 전통적인 자바스크립트 에러 핸들링 패턴을 고수하고 싶을 수도 있습니다.

 

혹은 export한 함수에서 기존 방식으로 에러를 던지는 것만 기억하고 있다면 이러한 접근 방식이 여전히 먹힐 수 있습니다.

 

자 그럼 Rust와 Go에서는 어떻게 사용하는지 봐봅시다.

 

4. Go 에서의 에러 핸들링

Go는 일반적으로 튜플을 반환하여 에러를 핸들링합니다. 공식 Go 튜토리얼의 코드를 예시로 보여드리겠습니다.

 

import (
    "errors"
    "fmt"
)

// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
    // If no name was given, return an error with a message.
    if name == "" {
        return "", errors.New("empty name")
    }

    // If a name was received, return a value that embeds the name
    // in a greeting message.
    message := fmt.Sprintf("Hi, %v. Welcome!", name)
    return message, nil
}

 

여기서 Hello라는 함수는 '(string, error)'와 같은 튜플을 반환하고 있습니다. 함수가 성공하면 메시지와 에러 쪽에는 nil을 반환합니다. 반대로 실패하면 에러와 빈 문자열을 반환하게 되죠. 즉, 함수를 호출한 주체 쪽에서 에러를 확인하고 적절하게 처리하도록 권장하고 있는 모습입니다.

 

func main() {
    greeting, err := greetings.Hello("Gopher")

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(greeting)
}

 

에러를 반환값으로 사용하면 함수가 실패할 수 있음을 명시적으로 알 수 있습니다. 이것은 Go에서 필수적인 패턴은 아니지만 일반적인 규칙입니다.

 

5. Rust에서의 에러 핸들링

Rust에서 실패할 수 있는 모든 함수는 Ok 또는 Err 중 하나가 될 수 있는 열거형인 Result 타입을 반환해야 합니다. Go 예제를 Rust에서 작동하도록 수정해 보겠습니다:

 

// Hello returns a greeting for the named person.
fn hello(name: &str) -> Result<String, &'static str> {
    // If no name was given, return an error with a message.
    if name.is_empty() {
        return Err("empty name");
    }

    // If a name was received, return a value that embeds the name
    // in a greeting message.
    Ok(format!("Hi, {}. Welcome!", name))
}

 

저는 Rust의 오류 처리가 Go보다 훨씬 더 명시적이라고 주장하고 싶습니다. Go에서는 튜플을 사용하는 것이 관례에 불과하지만, Rust에서는 결과 유형이 개발자가 오류를 처리하도록 강제하기 때문입니다. 그런 다음 함수를 호출할 때 일치 문을 사용하여 오류를 처리합니다.

fn main() {
    match hello("") {
        Ok(greeting) => println!("{}", greeting),
        Err(e) => println!("Error: {}", e),
    }
}

 

또는 closure에서 오류를 처리하고 성공 사례와 관련된 로직을 함수의 본문에 유지하려는 경우 unwrap_or_else 메서드를 사용할 수 있습니다.

fn main() {
    let greeting = hello("John").unwrap_or_else(|e| {
        println!("Error: {}", e);
    });

    println!("{}", greeting);
}

 

Result 유형은 Rust 언어에 내장되어 있으며 표준 Rust 라이브러리 전체에서 사용되는 패턴이므로 거의 피할 수 없는 유형입니다. 또한 Result에는 map 및 flatMap과 같은 유용한 메서드가 많이 포함되어 있는데, 여기선 다루지 않겠습니다.

 

7. 자바스크립트에서의 전형적인 에러 핸들링

마지막으로 자바스크립트에서 오류를 처리하는 일반적인 접근 방식의 예시를 보여드리겠습니다. 다시 한 번 위에서 사용한 hello 함수의 버전을 만들어 보겠습니다.

function hello(name: string): string {
  if (name === "") {
    throw new Error("empty name");
  }

  return `Hi, ${name}. Welcome!`;
}

 

그런 다음 다음과 같이 함수를 호출할 수 있습니다:

try {
  const greeting = hello("");
  console.log(greeting);
} catch (error) {
  console.error("Error:", error);
}

 

Hello 함수를 호출하는 사람이 오류가 발생할 수 있다는 것을 알고 적절하게 처리해야 한다는 점을 제외하면 이 접근 방식에 문제가 있는 것은 아닙니다. 그렇지만 규모가 크거나 복잡한 코드베이스에서는 문제가 발생할 수 있습니다!

 

8. 더 나은 방법

TypeScript와 ESLint의 강력한 기능을 사용하면 Go나 Rust와 유사한 접근 방식을 채택할 수 있습니다.

 

JavaScript 함수는 [string, Error] 튜플 유형을 반환하거나 Result 유형을 생성하여 사용할 수 있습니다. 저는 후자가 좀 더 명시적이라고 생각하기 때문에 후자를 택할 것이며, 명시적은 JavaScript/TypeScript 개발자에게 익숙하지 않을 수 있는 패턴에 가장 적합한 접근 방식이라고 생각합니다.

 

그렇다면 함수에 대한 결과 유형을 만드는 것부터 시작해 보겠습니다:

type Result<T> = 
  | { success: true; value: T }
  | { success: false; error: Error };

 

이것은 차별적 결합(discriminated union)을 사용하여 성공 또는 실패가 될 수 있는 유형을 만듭니다.

  • 성공인 경우 성공 속성이 true로 설정되고 값을 포함하는 값 속성이 있고,
  • 실패인 경우 성공 속성이 false로 설정되고 오류를 포함하는 오류 속성이 있습니다.

 

두 개의 유틸리티 함수를 만들어서 결과 유형을 쉽게 만들 수 있도록 하겠습니다.

function ok<T>(value: T): Result<T> {
  return { success: true, value };
}

function err(error: Error): Result<never> {
  return { success: false, error };
}

 

에러 type으로 never를 사용하면 오류가 발생했을 때 잘못 값을 반환할 수 없습니다. JavaScript는 매우 유연하고 catch 블록은 모든 값을 캐치할 수 있으므로(TypeScript에서는 catch 블록의 인수가 `any`이거나 `unkown`이어야 함), 다음과 같이 덜 일반적인 경우를 추가적으로 처리하도록 오류 함수를 변경할 수도 있습니다:

 

function err(error: unknown): Result<never> {
  if (error instanceof Error) {
    return { success: false, error };
  }

  if (typeof error === "string") {
    return { success: false, error: new Error(error) };
  }

  try {
    const stringified = JSON.stringify(error);
    return { success: false, error: new Error(stringified) };
  } catch {
    return { success: false, error: new Error("An error occurred") };
  }
}

 

다음으로, hello 함수를 다시 작성하여 `Result` 타입을 반환하고 ok 및 err 함수를 사용할 수 있습니다.

 

function hello(name: string): Result<string> {
  if (name === "") {
    return err(new Error("empty name"));
  }

  return ok(`Hi, ${name}. Welcome!`);
}

 

마지막으로 다음과 같이 함수를 호출할 수 있습니다:

function main(): void {
  const result = hello(userInput);

  if (result.success) {
    // handle success
  } else {
    // handle error
  }
}

 

try-catch 블록이 보이지 않습니다! result.success가 false일 때 result.value에 액세스하려고 하면 TypeScript가 에러를 표시하는데, 이는 존재하지 않는 값에 액세스할 수 없다는 것을 의미하기 때문에 굉장히 개발자 입장에서 고마운 에러입니다.

 

function main(): void {
  const result = hello(userInput);

  // "Property 'value' does not exist on type '{ success: false; error: Error; }'."
  console.log(result.value);
}

 

저는 이 접근 방식이 훨씬 더 명시적이므로 코드를 읽고 디버깅하기가 더 쉽다고 생각합니다. 실패를 처리하는 데 주의를 기울이고 존재하지 않는 값에 액세스하는 것을 막아주기 때문에 도움이 됩니다.

 

9. Resulify

하지만 우리가 사용하는 라이브러리 함수는 기존의 자바스크립트 에러 핸들링 패턴을 따른다는 점에 유의해야 합니다. 이러한 경우 try-catch 블록을 사용하여 오류를 'Result' 타입으로 변환할 수 있습니다.

function readFile(filePath: string): Result<string> {
  try {
    const file = fs.readFileSync(filePath, "utf-8");
    return ok(file);
  } catch (error) {
    return err(error);
  }
}

 

이제 readFile 함수를 호출하는 것은 hello 함수를 호출하는 것과 동일합니다:

function main(): void {
  const result = readFile(filePath);

  if (result.success) {
    // handle success
  } else {
    // handle error
  }
}

 

이 프로세스를 더 쉽게 수행하기 위해 일반 'resulify' 헬퍼 함수를 생성하여 기존 JavaScript 에러 핸들링 패턴을 사용하는 함수를 `Result` 유형을 반환하는 함수로 변환할 수 있습니다.

function resultify<T, U>(fn: (arg: T) => U): (arg: T) => Result<U> {
  return (arg: T) => {
    try {
      return ok(fn(arg));
    } catch (error) {
      return err(error);
    }
  };
}

 

그런 다음 'Result' 유형을 반환하도록 함수를 변환하는 것은 다음과 같이 간단합니다!

const readFile = resultify(fs.readFileSync);

 

10. 더 나아가서

Rust에서 영감을 얻은 이 접근법을 한 단계 더 발전시킬 수 있습니다. Rust에서 결과 유형은 monad 디자인 패턴을 따르므로 연산을 함께 연결할 수 있습니다. JavaScript에서도 연산을 함께 연결할 수 있는 map 및 flatMap 메서드가 포함된 결과 클래스를 생성하여 동일한 작업을 수행할 수 있습니다.

 

TypeScript에서 이를 달성하려면 기존 Result 유형의 이름을 ResultType으로 변경하는 것부터 시작하면 됩니다:

 

type ResultType<T> =
  | { success: true; value: T }
  | { success: false; error: Error };

 

그런 다음 Result 클래스를 만듭니다:

class Result<T> {
  private constructor(private readonly result: ResultType<T>) {}

  static ok<T>(value: T): Result<T> {
    return new Result<T>({ success: true, value });
  }

  static err<T>(error: Error): Result<T> {
    return new Result<T>({ success: false, error });
  }
}

 

지금까지는 동일한 ok 및 err 함수가 있었지만 이제는 Result 클래스의 인스턴스를 반환하는 정적 메서드입니다. 새 클래스에 몇 가지 getter 메서드를 추가하여 값 및 에러 속성에 액세스할 수 있도록 해 보겠습니다:

 

class Result<T> {
  // existing code

  get value(): T | undefined {
    return this.result.success ? this.result.value : undefined;
  }

  get error(): Error | undefined {
    return this.result.success ? undefined : this.result.error;
  }
}

 

TypeScript가 Result 인스턴스의 유형을 올바르게 추론할 수 있도록 하기 위해 맵과 flatMap 메서드에서 사용하지만 Result 클래스를 사용하는 개발자에게도 유용한 Type Guard 메서드인 isSuccessisError도 추가합니다.

 

class Result<T> {
  // existing code

  isSuccess(): this is { result: { success: true; value: T } } {
    return this.result.success;
  }

  isError(): this is { result: { success: false; error: Error } } {
    return !this.result.success;
  }
}

 

마지막으로 Result 클래스에 map과 flatMap 메서드를 추가할 수 있습니다. 이러한 메서드를 사용하면 연산을 서로 연결할 수 있으며 monad 디자인 패턴의 핵심 부분입니다:

  • map은 Result 인스턴스 내부의 값에 함수를 적용합니다.
  • flatMap은 자체적으로  Result 인스턴스를 반환하는 함수를 Result 인스턴스 내부의 값에 적용합니다.
class Result<T> {
  // existing code

  map<U>(fn: (value: T) => U): Result<U> {
    if (this.result.success) {
      return Result.ok(fn(this.result.value));
    } else {
      return Result.err<U>(this.result.error);
    }
  }

  flatMap<U>(fn: (value: T) => Result<U>): Result<U> {
    if (this.result.success) {
      return fn(this.result.value);
    } else {
      return Result.err<U>(this.result.error);
    }
  }
}

 

그리고 이를 통해 다음과 같이 작업을 서로 연결할 수 있습니다!

function main(): void {
  const result = hello(userInput)
    .map((greeting) => greeting.toUpperCase())
    .flatMap((greeting) => {
      if (greeting.length > 10) {
        return Result.ok(greeting);
      } else {
        return Result.err(new Error("Greeting is too short"));
      }
    });

  if (result.isSuccess()) {
    console.log(result.value);
  } else {
    console.error(result.error);
  }
}

 

다음은 새로운 결과 클래스를 사용하는 방법에 대한 몇 가지 예시입니다:

const { ok, err } = Result;

const successResult = ok(42);
const mappedResult = successResult.map((value) => value * 2); // ok(84)
const flatMappedResult = successResult.flatMap((value) => ok(value * 2)); // ok(84)

const errorResult = err<number>(new Error("Something went wrong"));
const mappedErrorResult = errorResult.map((value) => value * 2); // err(Error("Something went wrong"))
const flatMappedErrorResult = errorResult.flatMap((value) => ok(value * 2)); // err(Error("Something went wrong"))const { ok, err } = Result;

 

11. 더 많은 메서드들

더 배우기 원한다면, Rust의 Result 타입에서 차용할 수 있는 메서드가 더 많이 있습니다. 예를 들어, unwrap, unrwapOr, unwrapOrElse 메서드를 추가할 수 있습니다:

  • unwrap은 성공하면 값을 반환하고 실패하면 오류를 반환합니다.
  • unwrapOr는 성공하면 값을 반환하고 실패하면 기본값을 반환합니다.
  • unwrapOrElse는 성공하면 값을 반환하고 실패하면 오류를 인수로 취하는 함수의 결과를 반환합니다.

 

이러한 방법을 구현할 수 있는 한 가지 방법은 다음과 같습니다:
class Result<T> {
  unwrap(): T {
    if (this.result.success) {
      return this.result.value;
    } else {
      throw new Error(
        "Called unwrap on an error result: " + this.result.error.message
      );
    }
  }

  unwrapOr(defaultValue: T): T {
    return this.result.success ? this.result.value : defaultValue;
  }

  unwrapOrElse(fn: (error: Error) => T): T {
    return this.result.success ? this.result.value : fn(this.result.error);
  }
}

 

그리고 사용 방법에 대한 예시입니다:

const { ok, err } = Result;

const successResult = ok(42);
const errorResult = err<number>(new Error("Something went wrong"));

try {
  console.log(successResult.unwrap()); // 42
  console.log(errorResult.unwrap()); // Throws an error
} catch (e) {
  console.error(e);
}

console.log(successResult.unwrapOr(100)); // 42
console.log(errorResult.unwrapOr(100)); // 100

 


이 시점에서 만약 프로젝트에 이 패턴을 시도하려면 코드 리뷰를 통해 적용하는 것이 좋습니다. 하지만 한 단계 더 나아가 보다 강력한 접근 방식을 사용하고 싶다면 ESLint를 사용하여 프로젝트에 이 패턴을 적용할 수 있습니다.

 

12.  ESLint로 패턴 강제화하기

커스텀한 ESLint 규칙의 강력한 기능을 사용하면 코드베이스에서 이 패턴을 필수로 지정할 수 있습니다.

 

프로젝트에서 ESLint를 설정하는 가장 쉬운 방법은 다음 명령을 실행하는 것입니다:

npm init @eslint/config@latest

 

이렇게 하면 프로젝트에 ESLint가 자동으로 설정되고 eslint.config.mjs 구성 파일이 생성됩니다. 다음으로 프로젝트의 루트에 규칙 디렉터리를 만들어 보겠습니다. 이 디렉터리 안에 enforce-result-type.js 파일을 생성합니다.

 

mkdir rules
touch rules/enforce-result-type.js

 

 

그리고 enforce-result-type.js 파일 안에 다음 코드를 추가합니다:

// eslint-disable-next-line no-undef
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "enforce functions to return a Result type",
      category: "Best Practices",
      recommended: false,
    },
    schema: [],
  },
  create: function (context) {
    return {
      FunctionDeclaration(node) {
        const typeAnnotation = node.returnType?.typeAnnotation;

        const type = typeAnnotation?.type;

        const noReturnType = !typeAnnotation;

        const functionReturnsVoid = type === "TSVoidKeyword";

        const functionReturnsPromiseVoid =
          type === "TSTypeReference" &&
          typeAnnotation.typeName.name === "Promise" &&
          typeAnnotation.typeParameters.params[0].type === "TSVoidKeyword";

        const functionReturnsReturnType =
          type === "TSTypeReference" &&
          typeAnnotation.typeName.name === "Result";

        if (noReturnType) {
          context.report({
            node,
            message:
              "Please add a return type to your function. If the function does not return anything, use void.",
          });
        } else if (
          !functionReturnsVoid &&
          !functionReturnsPromiseVoid &&
          !functionReturnsReturnType
        ) {
          context.report({
            node,
            message:
              "Functions that return a value should return a Result type",
          });
        }
      },
    };
  },
};

 

이 규칙에 따라 모든 함수는 리턴 타입을 지정해야 하며 리턴 타입은 다음 중 하나여야 합니다:

  • void
  • Result<T>
  • Promise<void>
  • Promise<Result<T>>

규칙을 사용 설정하려면 eslint.config.mjs 파일로 이동하여 plugins.custom 내에 사용자 지정 규칙을 추가한 다음 규칙 개체에 규칙을 추가하시면 됩니다.

 

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import enforceResultType from "./rules/enforce-result-type.js";

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts}"],
    languageOptions: {
      globals: globals.browser,
    },
    plugins: {
      custom: {
        rules: {
          "enforce-result-type": enforceResultType,
        },
      },
    },
    rules: {
      "custom/enforce-result-type": "error",
    },
  },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
];

 

코드 편집기에서 이 기능을 확인하려면 ESLint 또는 편집기를 다시 시작해야 할 수 있습니다.

 

특별한 경우에는 특정 기능에 대해 이 규칙을 비활성화할 수 있습니다. 함수 상단에 주석을 추가하여 이 작업을 수행할 수 있습니다:

// eslint-disable-next-line custom/enforce-result-type
function myFunction() {
  // ...
}

 

개발자가 모든 함수에 리턴 타입을 추가하도록 요구하는 것이 너무 과하다면 암시적 리턴 타입(implicit return type)을 사용하도록 규칙을 수정해 볼 수도 있습니다. 이를 위해 기본 TypeScript 파서보다 한 단계 더 나아가 반환 유형을 유추할 수 있는 @typescript-eslint/parser 패키지를 확인하는 것이 좋습니다.

 

이제 코드베이스에 보다 명시적인 에러 핸들링 패턴을 적용하는 사용자 지정 ESLint 규칙이 생겼습니다!

 

13. 마치며

자바스크립트의 에러 핸들링이 개선될 수 있다는 전제에 이제는 동의하시나요? 아니면 기존의 접근 방식이 괜찮다고 생각하시나요? 프로젝트에서 이와 같은 패턴을 시도하게 된다면 어떨지 의견을 공유해주시면 감사하겠습니다.

 

 

 

 

 

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.