내 프로젝트에 ESLint Airbnb 적용하기
Airbnb ESlint with TypeScript and NestJS
  • NodeJS

시작하기

이번에 개발중인 프로젝트에 ESlint 를 적용할일이 있었다.

ESLint 에서 많이 사용하는 plugin 으로는 기본인 plugin:@typescript-eslint/recommended 가 있다.
그리고 Airbnb, Google 등에서 본인들의 ESLint style 을 작성하여 npm 모듈로 제공하고 있다.

그 중에 많은 회사에서 Airbnb 를 채택하여 사용하고있다. 모든 항목에 대해 만족스러운것은 아니어서 기본으로 Airbnb 를 채택하되 회사만의 rules를 추가해주었다.
어떻게 Airbnb 를 설정하고, 예외사항으로 추가한 rules 에는 어떤것들이 있는지 살펴본다.

ESLint Airbnb 설정

우선 Airbnb style 을 사용하기 위해서는 npm 모듈을 설치해줘야 한다

npm i -D eslint-config-airbnb eslint-config-airbnb-typescript

그리고 eslintrc 를 설정한다.

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: [
    'airbnb-base',
    'airbnb-typescript/base',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  ignorePatterns: ['.eslintrc.js'],
};

airbnb를 extends 로 설정해준다.

뒤에 -base 가 붙은것은 내가 설정한 프로젝트가 프론트용이 아닌 nodejs 프로젝트이기 때문이다.
자세한건 Airbnb ESLint github 저장소에서 확인할 수 있다.

Rules

Airbnb 의 스타일을 모두 따라하기에는 기존 개발자들과의 성향 차이가 있어서 일부 rules를 추가했다.
추가한 rules에 대해 하나씩 알아보도록 하자.

1. Prefer default export on a file with single export.(import/prefer-default-export)

export class 를 사용하지 말자.
export 하는게 하나만 있는 경우 export default 를 사용할것을 권장한다.

class가 하나이더라도 export class 를 사용하는것이 일반적이라 예외사항을 추가한다.

'import/prefer-default-export': 'off'

2. Missing radix parameter.(radix)

  parseInt(...); // Missing radix parameter.(radix)
  • radix 를 제공하지 않으면 ‘0x’로 시작되는 문자열은 16진수로 간주된다.
    그 외 다른 모든 문자열은 10진수로 간주된다.

  • 예측 가능한 실행을 보장하고, 가독성을 위해 항상 이 파라메터를 명시하도록 할 것을 권장하고있다.
    [MSDN - parseInt 함수(JavaScript)]

  • 명시적으로 10진수임을 표시하도록 한다.

parseInt(...);  

->  parseInt(..., 10);

3. Unexpected dangling '_' in '_validateOrderStatusInOpen'.(no-underscore-dangle)

함수명에 underscore로 시작하지 않는다.
private 함수를 _foo 와같이 작성하지 않는다.

과거 private 이 없었던 JavaScript에서 private 함수임을 표기하기위해 함수의 앞에 _를 붙이는 습관이 있었다. 이제는 private을 사용하자.

_foo () {} 

-> private foo () {}

4. class-methods-use-this

클래스 내부 데이터를 함수를 통해 전달받지 말고, this를 통해 사용하자.

// bad
class Test {
    foo(bar: string) {
      console.log(bar);
    }
}
    
// good
class Test {
  bar;

    foo() {
      console.log(this.bar);
    }
}

this 를 통해 접근할 수 있는 데이터에 대해 함수의 매개견수로 전달하지 말라는 경고이다.
이미 이렇게 사용하는 코드가 너무 많아 예외사항을 추가했다.

'class-methods-use-this': 'off',

5. ESLint: Unexpected await inside a loop.(no-await-in-loop)

반복문 안에 await 를 사용하지 말자.
https://eslint.org/docs/latest/rules/no-await-in-loop

하지만 이 역시 너무 많이 사용되고있는 패턴이다.
⇒ rules를 추가하여 허용하도록 수정한다.

'no-await-in-loop': 'off',

async function foo(things) {
  const results = [];
  for (const thing of things) {
    // Bad: each loop iteration is delayed until the entire asynchronous operation completes
    results.push(await bar(thing));
  }
  return baz(results);
}

6. iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.(no-restricted-syntax)

for 반복문을 사용하지 말자.
Array.forEach,Array.map 또는 Object.keys 및 Object.values와 같은 다른 루프 메서드로 대체할것을 권장한다.
https://runebook.dev/ko/docs/eslint/rules/no-restricted-syntax

for문보다 함수형을 사용할것을 권장하는 이유는, 함수형은 기존 객체는 그대로 둔 상태에서 복사된 새로운 객체가 반환된다.
기존 객체를 수정하지 않음으로써 의도치 않은 오류를 방지한다.

7. was used before it was defined @typescript-eslint/no-use-before-define

호출되는 함수는 호출부위보다 더 상단에서 정의되어야 한다.

// bad
class Test {
    bar() {
      this.foo();
    }

    foo() {
        console.log('hello');
    };
}

// good
class Test {
    foo() {
        console.log('hello');
    };
    
    bar() {
      this.foo();
    }
}

8. Expected '!==' and instead saw '!='

비교는 !=, == 이 아닌 !==, === 을 사용한다.

9. ESLint: Unexpected console statement.(no-console)

console 을 사용하지 않는다.
테스트 후 pr 할 땐 삭제를 권장하며 서버 로그로 출력이 필요하다면 debug 를 사용하자.

10. File has too many classes (2). Maximum allowed is 1

한 파일에 2개 이상의 class를 선언하지 않는다.

// bad
// foo.ts
export class A {}
export calss B {}

// good
// A.ts
export class A {}

// B.ts
export class B {}

11. The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype.

index값 사용을 위해 for..in 으로 사용하지 말자.
object 의 경우 key값이 순회한다.

특별한 이유가 없다면 for..in → forEach((item, idx)) Object.keys 사용

// bad
const foo = ['a','b','c'];
for (const idx in foo) {
  console.log(idx); // 0, 1, 2
}

const bar = { a: 1, b: 2 };
for (const idx in bar) {
  console.log(idx); // a, b
}

12. Unary operator '++' used.

++ 연산자 혹은 — 연산자를 허용하지 않는다. ++, — 대산 +=1, -=1 을 권장한다.
++와 — 역시 너무 많이 사용하는 패턴이기에 rules를 추가한다.

‘no-plusplus’: ‘off’,

13. Assignment to property of function parameter 'filter'.(no-param-reassign)

매개변수로 받은 변수를 수정하면 안된다.

// bad
foo (t: number) {
    t = 2;
}

이는 매개션수를 직접 수정하면, 함수 호출부의 객체 또한 값이 바뀌게 되어 의도치 않은 오류를 발생시킬 수 있다.

14. Unexpected use of continue statement.(no-continue)

continue 는 코드의 테스트 가능성, 가독성, 유지 관리 가능성이 떨어진다.

// bad
var sum = 0,
    i;

for(i = 0; i < 10; i++) {
    if(i > 5) {
       continue;
    }

        a += i;
}

// good
var sum = 0,
    i;

for(i = 0; i < 10; i++) {
    if(i < 5) {
       a += i;
    }
}

개인적으로 continue는 가독성을 올리는 방법중에 하나라고 생각한다.
=> rules를 추가했다.

'no-continue': 'off',

15. Unnecessary try/catch wrapper.

의미없는 try/catch 사용하지 말것

// bad
try {
    ...
} catch (e) {
    throw e;
}

16. Expected a default case.

switch 문에는 default 가 있어야한다.

// bad
switch (..) {
  case A:
        break;
}

// good
switch (..) {
  case A:
        break;
    default:
        ...
}

17. Unexpected lexical declaration in case block

switch 에 로직이 들어간다면 block으로 감싸준다.

// bad
switch (..) {
    case ..:
        foo();
    bar();
    qaz();
        break;
}

// good
switch (..) {
    case ..: {
        foo();
    bar();
    qaz();
        break;
    }
}

18. Unnecessary escape character: \+

불필요한 이스케이프 문자 제거
정규식에서 / , + , . 는 이스케이프 (\) 가 필요하지 않으므로 제거한다.

19. Do not nest ternary expressions

가독성이 떨어지는 중첩된 삼항연산자 사용하지 말자.

// bad
foo === 'a' ? bar === 'b' ? true : false : false

// good
if (foo === 'a') {
  if (bar === 'b' {
        return true;
  }

    return false;
}

return false;

20. Type Alias name T_FOO must match one of the following formats: PascalCase

Interface name dMeta must match one of the following formats: PascalCase

naming 컨벤션에 따라 type, interface 이름은 PascalCase 로 작성되어야 한다.

// bad
T_FOO

// good
TypeFoo

21. Array.prototype.map() expects a return value from arrow function.

map, forEach, find, some, every, filter 등 Array 함수 사용 주의하자.
예를들어, foreach에는 return이 없고, map에는 return이 있어야한다.
상황에 맞는 함수를 사용할 것.

22. Returning an awaited promise is not allowed in this context.

awaited된 프로미스를 return 하지 않는다.

// bad
async foo () {
  return await bar()
}

// good
foo (): Promise<..> {
  return bar();
}