좋은 코드의 의미에는 코드의 통일성, 일관성도 포함된다. 동료의 코드를 pull 받았는데 나와 포멧이 다르다면 저장을 누르는 순간 내 포멧으로 수정되고 불필요한 흔적이 커밋에 남는다. 따라서, 핵심 로직의 추적이 어려워진다. 단순히 포멧을 통일하는 것이 린트는 아니다. 사용하지 않은 변수 등 코드 퀄리티적인 요소를 찾아주기도 한다.
하지만, 린트를 적용만 하는 것으로는 한계가 있음을 채감했다. 배포 시간이 다가오며 급하게 warning을 확인하지 못하고 커밋을 할 때가 생기고, 다른 작업자 입장에서는 이거를 고치기도 애매하고, 두기도 애매한 그런 불편한 상황을 겪게 되었다.
그래서 우리는 더 강력한 자동화 제제를 적용시켜서, 레포지토리의 코드 퀄리티를 보호하기로 했다.
Prettier는 일정한 코드 포맷을 강제함으로서 코드의 일관성을 지켜주는 툴이다. 탭 수, 세미클론 여부 등 다양한 옵션을 지정할 수 있으며, 자동 보정도 해주기 때문에, VSCode Extension을 설치하고 기본 포매터로 지정한 뒤, Format on Save 옵션을 켜두면 저장할때마다 Prettier를 적용시킬 수 있다.
이렇게 해둔다면 같은 .prettierrc 파일을 공유함으로서, 팀원들과 똑같은 코드 포맷을 유지할 수 있다.
//.prettierrc
{
"singleQuote": true,
"printWidth": 140,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "auto",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve"
}
ESLint는 빠르게 분석해주고 구문에 대한 규칙에 어긋났을 경우 경고를 해주거나 빠르게 고쳐준다. Prettier와의 차이점은 코드 포멧팅에 관련된 어긋난 부분을 잡아준다면, ESLint 코드 퀄리티(미사용 변수, 글로벌 스코프 변수 등) 부분에서 어긋난 부분을 잡아준다. 다양한 플러그인 등을 통해 이러한 규칙들을 추가할 수 있다.
타입스크립트에서 ESLint를 적용하려면 추가적인 플러그인 필요하다.
@typescript-eslint/eslint-plugin
@typescript-eslint/parser
import를 사용하기 위해서는 추가적인 플러그인이 필요하다.
eslint-import-resolver-typescript 를 통해 tsconfig에 설정된 baseurl 등을 인식시킬 수 있다.
eslint-import-resolver-typescript
eslint-plugin-import
대표적으로 가장 많이 사용되는 airbnb 플러그인 적용하였다.
eslint-config-airbnb-typescript
eslint-config-airbnb-typescript
Prettier와 함께 사용하려면, 충돌방지를 위해 추가적인 플러그인이 필요하다.
eslint-config-prettier
eslint-plugin-prettier
.eslintrc 를 통해서 다양한 설정을 할 수 있다. .env에 nodeJs 환경을 추가하였다. 리액트라면 Document 같은 전역 변수를 위해 "browser"를 추가해줄 수 있다. rules를 통해 직접 규칙을 지정할 수 있다. 리액트 환경이라면 "airbnb-typescript"를 그렇지 않다면 "airbnb-typescript/base"를 적용시킬 수 있다.
리액트를 사용한다면 추가적으로 eslint-plugin-react를 설치고하고 setting에 react 버전 설정을 해줘야한다.
각 config에 대한 설명은 이 글에 잘 나와있다.
//NodeJs 서버의 .eslintrc.json
{
"env": {
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["node_modules/"],
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": ["airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"prettier"],
"settings": {
"import/ignore": ["node_modules"],
"import/resolver": {
"typescript": {}
}
},
"rules": {
"no-use-before-define": [
"error",
{ "functions": false, "classes": true, "variables": true }
],
"@typescript-eslint/no-use-before-define": [
"error", { "functions": false, "classes": true, "variables": true, "typedefs": true }
]
}
}
//리액트의 .eslintrc.json
{
"env": {
"browser": true,
"es6": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react"],
"ignorePatterns": ["node_modules/"],
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": [
"airbnb-typescript",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier"
],
"settings": {
"import/ignore": ["node_modules"],
"import/resolver": {
"typescript": {}
},
"react": {
"createClass": "createReactClass",
"pragma": "React",
"fragment": "Fragment",
"version": "detect",
"flowVersion": "0.53"
},
"propWrapperFunctions": [
"forbidExtraProps",
{ "property": "freeze", "object": "Object" },
{ "property": "myFavoriteWrapper" },
{ "property": "forbidExtraProps", "exact": true }
],
"componentWrapperFunctions": [
"observer",
{ "property": "styled" },
{ "property": "observer", "object": "Mobx" },
{ "property": "observer", "object": "<pragma>" }
],
"formComponents": ["CustomForm", { "name": "Form", "formAttribute": "endpoint" }],
"linkComponents": ["Hyperlink", { "name": "Link", "linkAttribute": "to" }]
},
"rules": {
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"react/prop-types": "off",
"import/no-extraneous-dependencies": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off" // todo: any 제거하기
}
}
설정에 대한 좋은 예시도 남긴다.
https://github.com/iamturns/create-exposed-app/blob/master/.eslintrc.js
추가적으로 tsconfig의 baseURL을 eslint 인식하지 못해 에러가 나는 경우가 있는데, vscode에 setting.json에 설정을 추가해주거나, eslint에 tsconfigRootDir를 설정해줘야한다.
"eslint.workingDirectories": [
{"directory": "./client", "changeProcessCWD": true}
],
허스키는 로컬에서 작동하는 Git hook으로 말그대로 Git의 작동을 인식해서 원하는 명령을 실행시킬 수 있다. 깃헙 액션은 특정 브랜치에 푸쉬후에 동작하지만, husky push 이전의 로컬에서 동작시킬 수 있다는 것이 차이점이다.
커밋전에 linter를 동작시켜서 통과하지 않을시 커밋을 중지 시켜 레포지토리에 올라오는 것을 방지할 수 있다. 단순히, linter뿐만 아니라 테스트 또한 자동화시키는 등 다양하게 활용할 수 있다.
.git 폴더위치를 인식해야함으로 하위 폴더구조로 나뉘어 있다면, 커스텀 디렉토리 에 설치할 수 있다.
lint-staged 말 그대로 stage단계에 있는 파일들만 linter를 돌려준다. 커밋할때마다 모든 파일에 linter를 돌린다면 매우 느리고 불필요한 작업이다. lint-staged를 통해, stage 단계에 올라간 파일만 검사를 시킬 수 있다.
// package.json
"scripts": {
"prepare": "cd .. && husky install", //루트 디렉토리에 husky 설치
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint --fix"
}
참고할만한 링크
https://blog.pumpkin-raccoon.com/85
Code spell checker는 VSCode extetnsion으로 코드의 스펠링 검사를 해준다.
영어 스펠링이 헷갈리 때도 있고, 아주 긴 변수명을 짓다보면 실수하기 할 때가 있는데, 이 extension은 camel, snake 케이스의 변수명까지 다 체크를 해줌으로 매우 매우 편리하다.
또한, 가끔 사전에 등록되지 않은 단어가 있을 수도 있는데, 팀적으로 이 단어를 사용한다면 워크스페이스에 단어를 등록해 놓을 수도 있다.
이러한 설정을 해둔다면, 코드 형식, 오타에 대한 실수들을 방지할 수 있고, 이로 인해 커밋을 다시 날리는 일을 방지할 수 있다. 레포지토리의 일관성 있는 코드 관리는 린터에게 맡겨두고, 개발자는 로직에 더 집중하자!!