Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

린팅(linting)이란 소스 코드에서 발생할 수 있는 프로그래밍 오류와 스타일상의 문제를 자동으로 검사하는 작업을 말합니다. 이 검사는 '린터(linter)'라 불리는 정적 코드 분석 도구가 수행합니다. 반면 코드 포매터(code formatter)는 소스 코드를 사전에 설정된 규칙 집합에 엄격하게 맞추도록 서식을 다듬는 데 특화된 도구입니다. 일반적으로 린터는 위반 사항을 보고하기만 하고 수정은 프로그래머의 몫으로 남기지만, 포매터는 규칙을 코드에 직접 적용해 서식 오류를 자동으로 고쳐 줍니다.

프로젝트에서 일관된 코드 스타일을 만들려면 보통 린팅 도구와 포매팅 도구를 따로 도입해야 하지만, 두 가지 역할을 모두 수행할 수 있는 도구도 있습니다. 대표적인 예가 바로 RuboCop입니다. 이 글에서는 RuboCop을 Ruby 프로젝트에 설치하고 설정 옵션을 조정해서 원하는 결과를 얻는 방법을 살펴봅니다. 또한 로컬 개발 환경에 통합하는 것뿐만 아니라 CI(지속적 통합) 워크플로우에 포함시키는 방법까지 함께 다룹니다.

RuboCop 설치하기

RuboCop은 RubyGems를 통해 간단히 설치할 수 있습니다.

$ gem install rubocop

설치된 버전을 확인해 보겠습니다.

$ rubocop --version
1.18.3

Bundler 사용을 선호한다면 아래 코드를 Gemfile에 추가한 뒤 bundle install을 실행하면 됩니다. require: false 부분은 해당 젬이 명령줄에서만 사용되므로 Bundler.require가 코드에서 불러오지 않도록 지시합니다.

gem 'rubocop', require: false

설치된 버전을 확인합니다.

$ bundle exec rubocop --version
1.18.3

RuboCop 실행하기

프로젝트 루트에서 rubocop(Bundler로 설치했다면 bundle exec rubocop)을 입력하면 기본 설정으로 실행됩니다. 인자 없이 실행하면 현재 디렉터리와 모든 하위 디렉터리의 Ruby 소스 파일을 검사하며, 분석할 파일이나 디렉터리 목록을 직접 지정할 수도 있습니다.

$ bundle exec rubocop
$ bundle exec rubocop src/lib

별도의 설정이 없으면 RuboCop은 커뮤니티 주도의 Ruby Style Guide에 담긴 지침들을 대부분 강제합니다. 명령을 실행하면 여러 개의 오류(위반 사항)가 출력될 수 있는데, 각 항목에는 문제 해결에 필요한 정보, 즉 위반 내용 설명과 파일 경로, 줄 번호 등이 함께 표시됩니다.

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

보고서 하단에는 검사한 파일 수, 총 위반 건수, 그리고 자동 수정이 가능한 건수가 요약되어 나타납니다. 여기에 -a 또는 --auto-correct 인자를 붙이면 RuboCop이 소스 파일에서 발견한 문제 중 [Correctable]로 표시된 것들을 자동으로 고치려 시도합니다.

$ bundle exec rubocop -a

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

수정된 항목 앞에는 [Corrected] 표시가 붙고, 보고서 하단에 수정된 건수 요약도 함께 나타납니다. 위 예시에서는 -a 플래그를 붙였음에도 하나의 수정 가능한 위반이 그대로 남아 있습니다. 이는 일부 자동 수정이 코드의 의미를 미세하게 변경할 수 있어 RuboCop이 '안전하지 않다'고 판단하기 때문입니다. 이런 항목까지 자동으로 고치려면 -A 또는 --auto-correct-all 플래그를 사용하세요.

$ bundle exec rubocop -A

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

자동 수정 기능을 사용한 후에는 예상치 못한 동작 변화가 없는지 확인하기 위해 테스트 스위트를 돌려보는 것이 좋은 습관입니다.

RuboCop 설정하기

RuboCop은 프로젝트 루트에 위치한 .rubocop.yml 파일로 설정할 수 있습니다. 모든 프로젝트에서 같은 규칙을 사용하고 싶다면 홈 디렉터리(~/.rubocop.yml)나 XDG 설정 디렉터리(~/.config/rubocop/config.yml)에 전역 설정 파일을 두면 됩니다. 현재 디렉터리와 상위 디렉터리들에서 로컬 설정 파일을 찾지 못한 경우 이 전역 설정이 사용됩니다.

RuboCop의 기본 설정은 설정 홈 디렉터리(~/.config/rubocop/default.yml)에 있으며, 다른 모든 설정 파일은 이를 상속합니다. 따라서 프로젝트 설정을 구성할 때는 기본값과 다른 부분만 변경하면 됩니다. 특정 검사를 활성화·비활성화하거나, 파라미터를 받는 검사라면 동작 방식을 조정하는 식입니다.

RuboCop은 개별 검사 규칙을 'cop'이라 부르며, 각 cop은 특정 위반을 탐지하는 역할을 담당합니다. 사용 가능한 cop들은 다음 부서(department)로 묶여 있습니다.

  • Style cops: 앞서 언급한 Ruby Style Guide를 기반으로 코드의 일관성을 검사합니다.
  • Layout cops: 공백 사용처럼 서식과 관련된 문제를 잡아냅니다.
  • Lint cops: ruby -w와 비슷하게 코드의 잠재적 오류를 감지하며, 그 외에도 다양한 추가 검사를 제공합니다.
  • Metric cops: 클래스 길이, 메서드 길이 같은 코드 측정 관련 문제를 다룹니다.
  • Naming cops: 네이밍 컨벤션을 검사합니다.
  • Security cops: 잠재적인 보안 취약점을 잡는 데 도움을 줍니다.
  • Bundler cops: Gemfile 같은 Bundler 파일의 나쁜 관행을 검사합니다.
  • Gemspec cops: .gemspec 파일의 나쁜 관행을 검사합니다.

추가적인 린터와 포매터 확장을 통해 RuboCop의 기능을 넓힐 수도 있습니다. 직접 확장을 만들거나 프로젝트에 맞는 기존 확장을 활용하면 됩니다. 예를 들어 Rails 베스트 프랙티스와 코딩 컨벤션을 강제하기 위한 Rails용 확장이 별도로 존재합니다.

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

설정 파일을 처음 만들면, 새로 추가되었지만 아직 설정되지 않은 cop들이 있다는 경고 메시지가 여럿 표시됩니다. RuboCop은 릴리스마다 새로운 cop을 추가하는데, 이들은 사용자 설정에서 명시적으로 활성화하거나 비활성화할 때까지 'pending' 상태로 유지됩니다. 메시지에 나열된 cop들을 하나씩 처리할 수도 있고, 아래 스니펫처럼 새 cop을 전부 활성화(권장)해서 이후 메시지가 더 이상 나오지 않게 할 수도 있습니다.

# .rubocop.yml
AllCops:
  NewCops: enable

설정 파일과 방대한 옵션을 일일이 다루기 싫다면 Standard 프로젝트를 살펴보세요. Standard는 사실상 사전 설정된 RuboCop으로, 어떤 규칙도 커스터마이징할 수 없는 대신 Ruby 프로젝트에 일관된 스타일을 강제하는 데 초점을 맞춥니다. 이 도구가 처음 공개된 라이트닝 토크에서 탄생 배경과 동기를 자세히 들을 수 있습니다.

설치하려면 Gemfile에 아래 줄을 추가한 뒤 bundle install을 실행하세요.

# Gemfile
gem "standard", group: [:development, :test]

이후 다음과 같이 명령줄에서 Standard를 실행할 수 있습니다.

$ bundle exec standardrb

기존 프로젝트에 RuboCop 도입하기

대부분의 Ruby 개발자는 백지 상태(greenfield)의 신규 프로젝트만 맡을 수 있는 특권을 누리지 못합니다. 개발 시간의 상당 부분은 레거시 코드베이스에서 보내지는데, 이런 곳에서는 당장 감당하기 어려운 어마어마한 양의 린팅 위반이 쏟아질 수 있습니다. 다행히 RuboCop에는 기존 위반 목록을 허용 리스트(allowlist)로 생성해 주는 유용한 기능이 있어, 시간을 두고 천천히 개선해 나갈 수 있습니다. 덕분에 감당하기 힘든 린팅 오류의 산에 파묻히지 않으면서도, 이후 발생하는 새로운 위반은 즉시 잡아내는 방식으로 기존 프로젝트에 린팅을 도입할 수 있습니다.

$ bundle exec rubocop

523 files inspected, 1018 offenses detected

허용 리스트 설정 파일은 아래 명령으로 생성할 수 있습니다.

$ bundle exec rubocop --auto-gen-config
Added inheritance from `.rubocop_todo.yml` in `.rubocop.yml`.
Created .rubocop_todo.yml.

--auto-gen-config 옵션은 모든 위반과 그 건수를 수집해 현재 디렉터리에 .rubocop_todo.yml 파일을 생성하고, 기존 위반들이 모두 무시되도록 만듭니다. 마지막으로 .rubocop.yml이 이 파일을 상속하도록 변경하므로, 다시 RuboCop을 실행해도 더 이상 위반이 보고되지 않습니다.

$ bundle exec rubocop
523 files inspected, no offenses detected

허용 리스트 파일을 생성할 때, 특정 cop의 위반 수가 일정 임계값(기본 15건)을 초과하면 RuboCop은 해당 cop을 아예 꺼버립니다. 이는 기존 위반이 많다는 이유로 새 코드조차 그 cop의 검사를 받지 못하게 되므로 대개 바람직하지 않습니다. 다행히 임계값을 올려서 위반이 많더라도 cop이 비활성화되지 않도록 할 수 있습니다.

$ bundle exec rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 10000

--auto-gen-only-exclude 옵션은 허용 리스트의 각 cop에 대해, 제외 파일 최대 수를 지정하는 Max 대신 위반이 발견된 모든 파일을 나열하는 Exclude 블록을 생성하도록 합니다. --exclude-limit를 지정하면 각 cop의 Exclude 블록에 담을 수 있는 파일 수의 상한도 함께 조정됩니다. 검사 대상 파일의 총 개수보다 큰 임의의 값을 지정하면 어떤 cop도 통째로 비활성화되지 않으므로, 기존 파일이든 새 파일이든 앞으로 추가되는 코드는 모두 정상적으로 검사됩니다.

기존 위반 해결하기

.rubocop_todo.yml 파일을 생성한 후에는 기존 위반을 잊지 말고 하나씩 천천히 해결해야 합니다. 방법은 간단합니다. cop의 Exclude 블록에서 파일 하나를 제거하고, 보고된 위반을 수정한 뒤, 버그가 생기지 않았는지 테스트 스위트를 돌리고 커밋하세요. 어떤 cop에서 모든 파일을 제거했다면 해당 cop 항목을 파일에서 직접 삭제하거나 허용 리스트 파일을 다시 생성하면 됩니다. 가능한 곳에는 --auto-correct 옵션을 활용하면 과정이 훨씬 빨라집니다.

스타일 가이드 채택하기

RuboCop은 매우 유연하게 설정할 수 있어 어떤 종류의 프로젝트에도 적합합니다. 다만 기본 규칙에 동의하지 않는 부분이 많다면 원하는 대로 규칙을 맞추는 데 시간이 꽤 걸릴 수 있습니다. 이런 경우 기존에 공개된 스타일 가이드를 채택하는 편이 유리합니다. Shopify와 Airbnb를 비롯한 여러 회사가 자사의 Ruby 스타일 가이드를 공개해 두었으며, 원하는 가이드를 RuboCop에 적용하려면 관련 젬을 Gemfile에 추가하면 됩니다.

# Gemfile
gem "rubocop-shopify", require: false

그다음 프로젝트 설정에서 이를 불러옵니다.

# .rubocop.yml
inherit_gem:
  rubocop-shopify: rubocop.yml

린팅 오류 무시하기

RuboCop은 훌륭한 도구지만 때때로 오탐(false positive)을 내거나, 프로그래머의 의도에 오히려 해가 되는 방향의 수정을 제안하기도 합니다. 이런 상황에서는 소스 코드에 주석으로 해당 위반을 무시할 수 있습니다. 아래와 같이 비활성화할 개별 cop이나 부서를 지정할 수 있습니다.

# rubocop:disable Layout/LineLength, Style
[..]
# rubocop:enable Layout/LineLength, Style

또는 코드의 특정 구간에서 모든 cop을 한꺼번에 끌 수도 있습니다.

# rubocop:disable all
[..]
# rubocop:enable all

줄 끝 주석을 사용하면 지정된 cop이 해당 줄에서만 비활성화됩니다.

for x in (0..10) # rubocop:disable Style/For

에디터 연동

명령줄에서 매번 검사를 실행하는 대신, 에디터에서 코드를 입력하는 즉시 RuboCop의 경고와 오류를 확인할 수 있다면 매우 편리합니다. 다행히 대부분의 인기 있는 코드 에디터와 IDE에서는 주로 서드파티 플러그인을 통해 RuboCop 연동을 지원합니다. Visual Studio Code라면 Ruby 확장을 설치하고 사용자 settings.json 파일에 아래 내용만 추가하면 됩니다.

{
  "ruby.lint": {
    "rubocop": true
  }
}

Vim이나 Neovim 사용자는 coc.nvim을 통해 RuboCop 진단 결과를 표시할 수 있습니다. 먼저 Solargraph 언어 서버(gem install solargraph)를 설치하고, 이어서 coc-solargraph 확장(:CocInstall coc-solargraph)을 설치하세요. 그다음 coc-settings.json 파일을 아래와 같이 설정합니다.

{
  "coc.preferences.formatOnSaveFiletypes": ["ruby"],
  "solargraph.autoformat": true,
  "solargraph.diagnostics": true,
  "solargraph.formatting": true
}

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

Pre-commit 훅 설정하기

프로젝트의 모든 Ruby 코드가 소스 관리에 커밋되기 전에 린팅과 포매팅이 제대로 이루어졌음을 보장하는 훌륭한 방법은, Git pre-commit 훅을 설정해 스테이징된 각 파일에 RuboCop을 실행하는 것입니다. 이 글에서는 Git pre-commit 훅을 관리·설정하는 도구인 Overcommit을 사용하는 방법을 소개하지만, 이미 사용 중인 pre-commit 워크플로우가 있다면 다른 도구와도 연동할 수 있습니다.

먼저 RubyGems로 Overcommit을 설치한 뒤 프로젝트에 적용합니다.

$ gem install overcommit
$ overcommit --install # 프로젝트 루트에서 실행

두 번째 명령은 현재 디렉터리에 저장소 전용 설정 파일(.overcommit.yml)을 만들고, 기존 훅이 있다면 백업합니다. 이 파일은 기본 설정을 확장하는 구조이므로 기본값과 다른 부분만 작성하면 됩니다. 예를 들어 아래 스니펫으로 RuboCop pre-commit 훅을 활성화할 수 있습니다.

# .overcommit.yml
PreCommit:
  RuboCop:
    enabled: true
    on_warn: fail
    problem_on_unmodified_line: ignore
    command: ['bundle', 'exec', 'rubocop']

on_warn: fail 설정은 Overcommit이 경고를 실패로 간주하게 하고, problem_on_unmodified_line: ignore는 스테이징되지 않은 줄의 경고와 오류를 무시하게 합니다. 사용 가능한 모든 훅 옵션과 허용 값의 범위는 프로젝트 GitHub 페이지에서 확인할 수 있습니다. 설정 파일을 변경한 후에는 overcommit --sign을 실행해야 변경 사항이 적용될 수 있습니다.

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

가끔 모든 검사를 통과하지 못한 파일(예: 진행 중인 작업)을 커밋해야 할 때가 있습니다. 이런 경우 개별 검사를 건너뛸 수 있습니다.

$ SKIP=RuboCop git commit -m "WIP: Unfinished work"

CI 워크플로우에 RuboCop 추가하기

풀 리퀘스트마다 RuboCop 검사를 실행하면 잘못 포맷된 코드가 프로젝트에 병합되는 것을 또 한번 막을 수 있습니다. 어떤 CI 도구로도 설정할 수 있지만, 이 글에서는 GitHub Actions를 통해 RuboCop을 실행하는 방법만 다루겠습니다.

첫 단계는 프로젝트 루트에 .github/workflows 디렉터리를 만들고 그 안에 rubocop.yml 파일을 생성하는 것입니다. 에디터로 파일을 열어 아래와 같이 작성하세요.

# .github/workflows/rubocop.yml
name: Lint code with RuboCop

on: [push, pull_request]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]

    steps:
    - uses: actions/checkout@v2

    - name: Setup Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: '3.0'
        bundler-cache: true

    - name: Run RuboCop
      run: bundle exec rubocop

위 워크플로우 파일은 코드가 GitHub에 푸시되거나 어떤 브랜치든 풀 리퀘스트가 생성될 때 실행되는 단일 잡(job)을 정의합니다. 잡은 순차적으로 실행되는 단계(step)들의 연속입니다. 이 잡은 runs-onstrategy.matrix에 정의된 대로 GitHub Actions가 제공하는 최신 Ubuntu, macOS, Windows 환경에서 각각 한 번씩 실행됩니다. 첫 번째 단계는 저장소 코드를 체크아웃하고, 다음 단계는 Ruby 툴체인과 의존성을 세팅하며, 마지막 단계에서 RuboCop을 실행합니다.

파일 편집이 끝나면 저장하고 커밋한 뒤 GitHub에 푸시하세요. 이후 커밋과 풀 리퀘스트마다 보고된 문제가 화면에 인라인으로 표시됩니다.

RuboCop으로 루비 코드 린팅과 자동 포매팅 한 번에 끝내기

대체 자동 포매터

RuboCop은 포괄적인 자동 포매팅 기능을 제공하지만, 요구 사항을 충족하지 못할 경우를 대비해 다른 도구들도 알아두는 것이 좋습니다.

Prettier

Prettier는 원래 JavaScript를 위한 독선적(opinionated) 코드 포매터로 시작했지만, 지금은 Ruby를 포함한 다양한 언어를 지원합니다. Ruby 플러그인 설치는 간단합니다. Gemfileprettier 젬을 추가하고 bundle을 실행하면 됩니다.

# Gemfile
gem 'prettier'

이후 아래 명령으로 Ruby 코드를 Prettier로 포매팅할 수 있습니다.

$ bundle exec rbprettier --write '**/*.rb'

Prettier의 일부 규칙은 RuboCop의 규칙과 충돌하므로, Prettier와 간섭하지 않도록 후자의 포매팅 검사를 비활성화해야 합니다. 다행히 Prettier와 충돌하거나 불필요한 RuboCop 검사를 끄는 일은 쉽습니다. 프로젝트의 .rubocop.yml 파일 최상단에서 Prettier가 제공하는 RuboCop 설정을 상속하기만 하면 됩니다.

# .rubocop.yml
inherit_gem:
  prettier: rubocop.yml

이후부터 bundle exec rubocop으로 실행하면 레이아웃 관련 위반은 보고되지 않으며, Prettier가 자신의 규칙에 따라 이를 교정할 수 있습니다. Prettier 역시 설정 파일을 통해 출력을 조정할 수 있고, 같은 프로젝트의 JavaScript와 Ruby 코드 간에 설정을 공유할 수도 있습니다.

RubyFmt

RubyFmt는 Rust로 작성된 신생 코드 포매터로, 현재 활발히 개발이 진행 중입니다. Prettier처럼 코드 분석 도구가 아니라 포매터로 설계되었습니다. 아직 안정 버전이 출시되지 않았으니 지금 도입하기보다는 지켜볼 만한 도구입니다.

마무리

코드 린팅과 자동 포매팅은 코드베이스에 많은 이점을 가져다주며, 특히 여러 개발자가 함께하는 팀 환경에서 빛을 발합니다. 코드 서식을 남에게 지시받는 것을 좋아하지 않더라도, 린팅은 결국 나만을 위한 것이 아니라 함께 협업하는 모든 사람이 동일한 컨벤션을 지키도록 해 주는 장치라는 점을 기억해야 합니다. 덕분에 같은 프로젝트 안에서 여러 코딩 스타일이 뒤섞이는 문제를 없앨 수 있습니다.

동시에 린터의 출력을 절대적인 진리로 여겨서는 안 됩니다. 핵심 목표에 방해가 되지 않으면서 최대한의 이점을 주도록 설정을 다듬는 노력이 필요합니다. RuboCop은 방대한 설정 옵션을 제공하므로 큰 어려움은 없을 것입니다. 만약 RuboCop 설정에 시간을 너무 많이 쓰고 있다면 앞서 소개한 사전 정의된 스타일 가이드를 활용하거나, 설정 없이 누구나 바로 쓸 수 있는 대안으로 Standard를 채택해 세부 사항에 대한 고민을 덜어내는 것도 좋은 선택입니다.

읽어 주셔서 감사합니다. 즐거운 코딩 되세요!