Nodejs 백엔드 개발 환경 재정비하기

tslint를 eslint로 마이그레이션, vscode에서 pretter 사용, lerna 모노레포에 husky 적용

Kay Hwang
직방 기술 블로그
13 min readSep 20, 2022

--

안녕하세요? 직방부동산팀에서 원룸, 오피스텔, 빌라 백엔드를 주로 담당하고 있는 Kay입니다. 저희 직방에서는 원룸, 오피스텔, 빌라를 줄여서 원오빌이라고 부르고 있답니다.

직방에서 원오빌은 역사가 꽤 오래 되었기 때문에 원오빌 서비스의 백엔드 소스 코드도 꽤 오래된 편입니다. 원오빌 백엔드 소스 코드는 대략 70%가 TypeScript 프로젝트로, 28%는 c# 프로젝트로 구성된 것 같습니다. 이 둘의 first commit 된 날짜를 보니 TypeScript는 2018년 5월, c#는 2014년 5월이니 프로젝트가 시작된 지 각 4년, 8년 정도 된 것 같습니다.

저는 둘 중 주로 TypeScript 쪽을 다루고 있기 때문에 TypeScript에 대해서만 조금 더 자세히 말씀드려볼까 합니다. 대략 TypeScript 프로젝트의 50%는 Nestjs로 40%는 express, typedi 등을 조합으로 구성될 것 같습니다. Nodejs를 비교적 오래전부터 사용해 왔기 때문에 이런 혼용은 당연한 결과라 여겨집니다. 

JavaScript가 크게 주목받기 시작한 시점은 ES6나 나온 시점인 2015 이후라고 생각합니다. 이후 많은 변화가 일어났고 현재는 Nodejs가 프론트엔드 개발에서 거의 표준이 되다시피 한 것 같습니다.

백엔드의 경우, 웹 개발자에게 익숙한 스크립트 언어인 JavaScript로 개발이 가능하다는 점에서 Nodejs가 인기를 끌기 시작한 것 같습니다. 최근 Nestjs가 백엔드 프레임워크로 자리를 잡는 것처럼 보이는데 Spring boot를 사용하는 것과 비슷한 경험을 제공하지만 러닝커브가 낮아 Nodejs가 백엔드에서도 인기를 조금씩 얻고 있는 듯합니다.

아시다시피 Nodejs는 상용 제품의 백엔드에 사용되기 시작한 역사가 매우 짧습니다. 그래서 지금도 일부 주요한 기능에 breaking change가 발생하고 있습니다. 즉, 아직 일부 주요 기능들이 안정화되지 못한 채 상용 제품에 사용되고 있습니다. 실제로 최근 팀 내에서 Nodejs 런타임을 v14에서 v16으로 업데이트하는 과정에서 unhandledRejection이 warn에서 uncaught exception으로 변경되는 부분을 발견하여 하위 코드와 호환되도록 처리한 적도 있었습니다.

Nodejs 라이브러리 중에서도 breaking change가 빈번히 발생하는데요. 그중 하나가tslint입니다. tslint는 무려 3년 전인 2019년부터 업데이트가 중단되었습니다. eslint가 JavaScript뿐만 아니라 TypeScript의 표준 linter가 되도록 돕기 위해서라고 합니다. 그래서 만약 TypeScript와 tslint를 사용하고 있던 팀이라면 eslint로 마이그레이션 하는 것을 고려해야 할 수도 있을 것 같습니다. linter를 앞으로 원활히 사용하기 위해서 말이에요.

원오빌 백엔드는 아직도 tslint를 사용하고 있습니다. linter를 당장 업데이트 하지 않아도 기존에 동작하던 코드에 크게 문제가 생기지 않기 때문에, linter에 신경 쓰기보다는 당장 새롭게 구현해야 기능들이 쌓여있기 때문에, 매일매일 버그 수정에 시간을 쏟기 때문에 등 여러 이유에 의해 linter 같은 것에 크게 신경을 쓰지 못하고 있었던 것 같습니다.

하지만 성장하는 조직이라면 장기적인 관점에서 linter 잘 관리하는 것도 중요하다고 생각합니다. linter의 규칙을 잘 정의해 둔다면 구성원이 누구든지 간에 어느 정도 이상의 코드 품질이 보장되어 버그 발생을 줄일 수 있다고 여기기 때문입니다.

그래서 오늘은 최근 작업했던 것 중 tslint를 제거하면서 eslint를 사용하도록 마이그레이션 했던 내용을 소개해드리려고 합니다. 더불어 코드 포맷을 쉽게 정리해주는 prettier를 새롭게 추가했던 것, 기존에 있던 husky를 lerna 기반 모노 레포에 알맞게 동작하도록수정했던 것도 같이 다루어 보겠습니다.

linter가 필요한 이유

linter는 코드를 정적으로 분석해 컴파일 시간에는 전혀 문제가 되지 않지만, 실행시간에 문제를 발생시킬 수 있는 것들을 미리 알려줍니다. eslint로 예를 들어보겠습니다. if 문을 사용할 때 괄호({ })를 반드시 사용해야 하는 규칙(curly), 비교문에서 === 처럼 등호 세 개를 사용하도록 하는 규칙(eqeqeq) 등이 있습니다.

tslint를 eslint로 마이그레이션

*tslint에서 eslint로 옮기는 방법은 이 문서를 참고하였습니다.

원오빌 TypeScript 백엔드는 lerna 기반 모노 레포로 구성되어 있습니다.

각 패키지(packageA, packageB, …) 마다 tslint.json이 있고 그것들은 프로젝트 루트에 있는 tslint.json을 확장해서 사용하고 있었습니다.

마이그레이션 후에도 이 구조를 크게 벗어나지 않게 하려고 했습니다.

packageA부터 마이그레이션을 해보겠습니다.

eslint 사용을 위해 devDependencies에 eslint , @typescript-eslint/parser,@typescript-eslint/eslint-plugin을 설치합니다.

그다음 packageA 디렉터리로 옮겨가서 eslint로 마이그레이션 하기 위해 다음 코드를 실행합니다.

작업을 시작하기 전 tslint 설정 하나하나를 어떻게 한 땀 한 땀 eslint 설정으로 변환시켜야 할까 걱정이 많았습니다. 하지만 tslint-to-eslint-config 덕분에 비교적 쉽게 eslint 설정 파일을 생성할 수 있었습니다. 👍

실행해보니 다음과 같이 출력됩니다.

출력된 것처럼 tslint-to-eslint-config가 tslint에서 eslint로 마이그레이션 할 때 만능은 아니었습니다. 몇몇 규칙들은 마이그레이션 할 수 없었습니다. 관련해서 자세한 내용은 ./tslint-to-eslint-config.log 파일에서 찾아볼 수 있었습니다.

eslint 설정을 위해 설치해야 하는 plugin도 안내해주고 있습니다. 실제로 생성된 config 파일인 .eslintrc.js 중 plugin 부분입니다. plugin을 통해 기본적으로 제공하지 않는 규칙 외에 다른 규칙을 적용하고 싶을 때 모듈로 쉽게 가져다 쓸 수 있도록 해줍니다.

설레는 맘으로 lint를 실행시켜봅니다. 하지만 매우 많은 lint 에러가 발생합니다.

아마 위 출력된 결과에서 알려주는 것처럼 기존 규칙과 다르거나 아예 번역할 수 없었던 규칙들 때문에 에러가 발생하는 것 같습니다.

이 에러들을 어떻게 처리할까 고민해봤습니다.

1. lint 에러(error) 규칙에 맞게 소스 코드를 모두 수정한다.

2. 에러를 발생시키는 lint 규칙들의 단계를 에러(error)에서 경고(warn)로 변환한다.

고민 후 저는 2번을 택했습니다. 이유는 에러가 발생하는 규칙들에 대해 아직 팀원들과 어떠한 합의를 본 적이 없고, lint 에러를 수정하다가 몇 년 전 커밋 된 소스 코드를 잘 못 수정해 치명적인 버그를 만들 수도 있을 것 같았기 때문입니다. 앞으로는 팀원들과 lint 규칙에 대해 논의, 결정, 리뷰 후 lint 규칙에 맞게 소스 코드를 수정해 나가면 될 것 같습니다. 수정이 다 되면 lint 경고(warn)를 에러(error) 단계로 변경하면 되겠습니다.

인내심있게 소스 코드에서 lint error가 발생하는 부분들을 하나하나 확인해가면서 일단은 경고(warn)로 바꿔도 될지 검토를 한 후 .eslintrc.js에 해당 규칙을 에러(error)에서 경고(warn)로 모두 변경하였습니다.

VSCode를 사용 중이라면 eslint 플러그인을 설치하는 것을 추천합니다. IDE에서 실시간으로 lint 정적 분석을 해주기 때문에 커맨드라인으로 매번 실행하는 것보다 반응이 빠르기 때문입니다. 그리고 IDE가 추천해주는 대로 쉽게 수정도 가능하답니다.

당장 해결하지 못하는 lint 규칙은

eslint-disable-next-line {lint 규칙}

처럼 명시해 linter가 해당 라인을 분석하는 것을 무시하게 할 수도 있습니다.

마지막으로 eslint 설정 파일을 root에 있는 설정 파일로 옮기고 packageA에 있는 설정 파일은 root 설정을 확장(extends)하게 합니다. 다른 레퍼지토리에서도 공통된 lint 규칙을 사용하게 하고 싶어서입니다. 만약 레퍼지터리마다 다른 lint 규칙을 적용할 예정이라면 확장(extends)하지 않아도 될 것입니다.

eslint가 잘 적용된 것 같으니 그간 고생했던 tslint는 보내주겠습니다. 👋

prettier가 필요한 이유

prettier가 필수는 아니지만 왜 필요한지 고민해볼 필요가 있습니다.

공식 문서에는 다음과 같이 6가지 장점이 있다고 소개하고 있습니다.

1. Building and enforcing a style guide

2. Helping Newcomers

3. Writing code

4. Easy to adopt

5. Clean up an existing codebase

6. Ride the hype train

문서에서 말하고 있는 것처럼 저도 prettier가 코드를 예쁘게 해주는 것 이상의 역할을 한다고 생각해왔습니다. 개인적으로 prettier의 장점은 코딩 스타일을 통일, 강제 시켜 상대적으로 덜 중요한 부분인 코드 포맷, 스타일보다 더 중요한 부분인 해결해야할 문제에 더 집중할 수 있게 해준다는 것이라고 생각합니다. 때문에 prettier를 도입한다면 작업자와 리뷰하는 동료 모두에게 업무시간, 효율과 스트레스를 덜어주는 효과가 있다고 봅니다.

이쯤에서 prettier와 eslint가 비슷한 역할을 하는 것이 아니냐고 의문을 가질 수도 있겠습니다. 개인적으로도 비슷하지만 조금은 다르다고 생각합니다. eslint는 정적분석을 통해 의도치 않게 발생할 수 있는 버그를 방지하는 역할에 집중하고 있지만, prettier는 코드 포맷을 강제하는 역할에 집중한다고 생각합니다. 하지만 둘의 경계가 모호한 부분이 분명히 있긴 합니다.

vscode에서 prettier 사용

원하는 때에 cli를 직접 실행시키거나 코드를 git 커밋, 푸시 전에 prettier가 실행되도록 자동화할 수 있습니다. 하지만 소스 코드 파일을 저장할 때 prettier 규칙에 맞게 포맷이 자동으로 변경되도록 적용했습니다. 작업했던 코드의 변경을 바로 보면서 곧바로 확인하는 것이 더 나은 선택인 것 같아서입니다.

prettier의 config 파일인 .prettierrc.js 파일을 프로젝트 root 디렉터리에 만듭니다.

그 후 .prettierrc.js에 코드 포맷 규칙을 명시할 것인데요. 몇몇 규칙은 eslint과 겹치는 부분이 있습니다. 겹치는 부분들은 eslint에 설정했던 것과 호환되도록 알맞게 eslint 규칙을 작성합니다. 그렇지 않으면 서로 충돌이 발생하게 됩니다.

vscode에서 save 될 때마다 코드 포맷이 수정되도록 하려면 prettier plugin을 설치해야 합니다.

설치 후,

Preferences > Settings > “format on save”라고 검색 후 체크

해줍니다.

만약 코드를 save 했을 때 prettier가 잘 동작하지 않는다면 VScode를 껐다 켜봅니다. 그래도 잘 동작하지 않으면

[cmd] + [shift] + [p] > “Format Document With…” 입력 후 선택 > “Prettier — Code Formatter”를 선택해 줍니다.

prettier 설정을 마쳤습니다. 코드를 저장하게 되면 규칙대로 코드 포맷을 정렬해주는 것을 보실 수 있을 것입니다!

eslint와 prettier 간의 호환성

작업을 하다 보니 eslint와 prettier가 100%는 호환될 수 없다는 것을 알게 됐습니다.

eslint에는 @typescript-eslint/member-delimiter-style 라는 규칙이 있습니다. 객체 interface, type의 경우 어떻게 구분을 할지(콤마 또는 세미콜론 또는 빈칸)를 명시하는 규칙입니다.

하지만 prettier에서는 세부적인 옵션을 지정할 수 없기 때문에 eslint를 이렇게 설정해야만 서로 호환이 되는 것을 확인했습니다.

lerna 모노레포에 Husky 적용

기존에 husky를 사용하고 있었지만, 서브 패키지 중에 1개에서만 동작하고 있었습니다. 이번에 모든 패키지에 공통으로 동작하는 eslint, prettier를 적용했으니 코드 변화만 있는 패키지에서만 husky를 통해 커밋 전 lint 검사를 자동으로 할 수 있게 수정해보겠습니다.

기존에 packagesA에만 설치돼있던 husky는 제거하고 root에 husky를 devDependencies에 설치합니다.

그리고 husky 설정을 쉽게 하기 위해 husky-init을 이용합니다. precommit, prepush 스크립트 등을 쉽게 설정할 수 있게 도와줍니다.

명령어를 실행시키면 .husky 디렉터리가 생성되고 하위에 pre-commit 파일이 생성됩니다. 이 pre-commit 파일 안에 작성된 쉘이 커밋 전에 실행시킵니다.

다음과 같이 작성해 커밋 전에 변경된 패키지만 linter가 검사하게 했습니다.

husky 설정도 마쳤습니다.

마무리

이번에는 저희 원오빌 TypeScript 백엔드에 eslint, prettier 그리고 husky를 사용하도록 설정하는 작업을 해보았습니다.

본래 해야 하는 업무를 진행하면서 틈틈이 남는 시간을 쪼개어 개발환경을 다듬는 작업을 진행했는데요. 팀원들이 소스 코드를 작성할 때 덜 고생하고, 더 좋은 소스 코드가 자라났으면 하는 마음에서 즐겁게 작업했습니다.

이제 이 PR이 master에 병합될 수 있도록 팀원들을 잘 설득하는 일만 남은 것 같습니다.

또한 팀원 중 VScode뿐만 아니라 WebStorm을 사용하고 있는 분들도 계셔서 WebStorm에서도 prettier가 잘 동작하도록 하는 방법도 찾아볼 예정입니다.

--

--

Kay Hwang
직방 기술 블로그

직방 부동산팀에서 백엔드 개발을 하고 있습니다.