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

첫 루비 젬(Gem) 만들기 가이드: Rails 앱에서 코드 추출부터 배포까지

GitHub 커밋 그래프가 온통 회색으로만 채워져 있나요? 작업할 오픈소스 프로젝트가 필요하겠지만, 굳이 처음부터 새로 시작할 필요는 없습니다. 유용한 사이드 프로젝트를 만드는 가장 쉬운 방법은 지금 개발 중인 앱에서 코드를 꺼내는 것입니다. Rails가 탄생한 방식도 바로 이것이었죠!

그렇다면 어떤 코드를 추출해야 할까요? 그리고 기존 워크플로우를 망가뜨리지 않으면서 어떻게 젬(gem)으로 만들 수 있을까요?

추출할 코드 찾기

앱 깊은 곳 어딘가에, 사실 앱에 속하지 않아도 되는 코드가 숨어 있습니다. 자기 역할을 수행하는 데 앱이 필요 없는 코드 말이죠. 대체 어디에 있을까요?

때로는 감으로 찾아야 할 때도 있습니다. 하지만 저는 주로 다음 몇 곳에서 추출 가능한 코드를 발견하곤 합니다:

  • 유효성 검사(Validation)

    속성(attribute)에 대한 커스텀 유효성 검사를 직접 작성한 적이 있나요? 이런 코드들은 훌륭한 젬이 될 수 있습니다.

  • Rails에 적용한 변경 사항

    테스트에서 커밋 훅이 동작하게 하거나, 빈 속성이 데이터베이스에서 NULL로 저장되도록 처리하는 등 가끔은 Rails를 직접 건드려야 할 때가 있습니다. 이런 로직을 젬으로 옮길 때마다 코드가 더 안정적이고 이해하기 쉬워졌습니다.

  • 비(非) ActiveRecord 모델

    이메일 주소나 전화번호 파싱 로직이 너무 많아져서 별도 클래스로 분리한 적이 있나요? 이런 클래스들은 다른 앱에서도 유용하게 재사용될 수 있고, 젬으로 만들기도 꽤 수월합니다.

  • 목(Mock) 객체와 커스텀 단언문(Assertion)

    커스텀 단언문을 활용하면 훨씬 읽기 쉬운 테스트를 작성할 수 있습니다. 특정 라이브러리나 패턴을 위해 좋은 커스텀 단언문을 잘 만들어 두면, 그 라이브러리나 패턴을 사용하는 모든 개발자에게 도움이 됩니다.

거창하게 생각할 필요는 없습니다. 제가 가장 아끼는 젬들 중 일부는 파일 하나짜리이기도 하니까요!

그래도 무엇을 추출할지 결정이 안 서신다면, RubyToolbox의 카테고리 목록을 둘러보며 영감을 얻어보세요.

젬과 같은 디렉터리 구조로 코드 재배치하기

젬으로 만들 코드를 정했다면, 앱 내부에서도 젬과 비슷한 구조를 갖도록 코드 위치를 조정하세요.

Rails 앱에서는 lib/ 디렉터리를 일종의 '젬 대기 공간(staging area)'으로 사용합니다. 독립적인 젬으로 성장할 가능성이 있는 코드들을 이곳에 모아두는 것이죠. 예를 들어 'json_api_client'라는 젬을 만든다면, Rails 앱의 lib/ 디렉터리는 다음과 같은 모습일 수 있습니다:

...
my_rails_app/lib/json_api_client.rb
my_rails_app/lib/json_api_client/associations.rb
my_rails_app/lib/json_api_client/connection.rb
...

대부분의 젬은 lib/ 아래에 젬 이름을 딴 파일(lib/json_api_client.rb) 하나와, 젬 이름을 딴 디렉터리(lib/json_api_client/) 아래의 여러 파일로 구성됩니다. Rails 앱 안에서도 이와 동일한 구조를 미리 맞춰두면, 나중에 코드를 젬으로 옮기는 작업이 훨씬 수월해집니다.

젬의 디렉터리 구조가 어떻게 생겼는지 감이 잡히지 않는다면, GitHub에서 즐겨 쓰는 젬들의 소스 코드를 살펴보세요. 금방 패턴이 눈에 들어올 겁니다.

테스트는 어떻게 배치할까?

저는 예전에 test/unit/ 안에서도 lib/의 구조를 그대로 따라 배치했습니다:

...
my_rails_app/test/unit/json_api_client_test.rb
my_rails_app/test/unit/json_api_client/associations_test.rb
my_rails_app/test/unit/json_api_client/connection_test.rb
...

모델과 라이브러리가 같은 폴더에 섞여 다소 지저분해지긴 했지만, 나쁘지 않게 동작했습니다.

하지만 요즘 Rails는 test/unit/ 대신 test/models/를 사용합니다. 그렇다면 lib/용 테스트를 test/models/에 넣는 것은 크게 어울리지 않죠. 아직 저도 이 부분에 대한 명확한 관례를 정하지 못했습니다. 좋은 제안이 있다면 알려주세요.

의존성 끊기

코드가 한번 젬 안에 들어가면 더 이상 앱에 의존할 수 없습니다. 즉, lib/에 넣은 코드를 샅샅이 살펴보면서 앱 고유의 클래스, 객체, 동작에 의존하는 부분을 찾아야 합니다.

이런 의존성을 발견했다면 반드시 끊어내야 합니다. 의존성을 분리(또는 주입)하는 방법에 대한 좋은 글이 이미 많이 나와 있으므로, 여기서는 자세히 다루지 않겠습니다.

젬 생성하기

저는 젬을 만들 때 bundle gem 명령을 사용합니다. 예를 들어 bulk_cache_fetcher라는 젬을 만들려면 다음과 같이 입력합니다:

bundle gem bulk_cache_fetcher -t minitest

-t 옵션은 테스트 헬퍼 파일들과 함께 젬의 Rakefile에 테스트 작업(task)을 추가해 줍니다.

다음으로 기본적인 정리 작업이 필요합니다. .gemspec 작성, README 작성, LICENSE 선택 같은 것들이죠.

이미 젬 코드가 Rails 앱의 lib/ 폴더에 준비되어 있으므로, 해당 코드와 테스트를 새 젬의 lib/, test/ 폴더로 옮기기만 하면 됩니다.

대부분의 경우 놓치거나 잊어버린 부분이 남아 있거나, 젬 환경에서는 성립하지 않는 앱의 가정에 의존하는 코드가 존재합니다. 그러니 다음 단계로 넘어가기 전에, 옮긴 테스트를 실행해서 모두 통과하는지 꼭 확인하세요.

앱에서 새 젬 사용하기

이제 젬이 생겼으니 바로 사용하고 싶겠죠? 하지만 Rails 앱 안에서 젬의 변경 사항을 테스트하는 일은 순식간에 짜증나는 작업이 됩니다. 매번 다음 과정을 거쳐야 하거든요:

  1. 젬에서 코드 수정
  2. 젬 빌드
  3. 시스템에서 기존 젬 흔적을 모두 제거하거나 버전 업데이트
  4. 젬 설치
  5. 서버 재시작

꽤 끔찍한 과정입니다. 다행히 bundler가 더 간편한 방법을 제공합니다. 젬을 ~/Source/bulk_cache_fetcher에 만들었다고 가정하면, 젬 변경 사항을 테스트하는 동안 Rails 앱의 Gemfile에 다음과 같이 적으면 됩니다:

Gemfile
gem "bulk_cache_fetcher", path: "~/Source/bulk_cache_fetcher"

그런 다음 bundle install을 실행하면, 마치 해당 코드가 여전히 앱의 lib/ 폴더에 있는 것처럼 자유롭게 젬을 수정할 수 있습니다.

마지막으로 한 가지 주의할 점: 코드를 커밋하기 전에 반드시 path: 옵션을 제거하세요! 이 경로는 다른 시스템에서는 올바른 위치를 가리키지 않을 가능성이 높아, 본인의 머신 외에는 어디서도 동작하지 않습니다.

빌드하고, 배포하고, 즐기세요!

젬이 준비되었다면 이제 세상에 내보낼 차례입니다.

아직 계정이 없다면 RubyGems에 가입하고, 변경 사항을 커밋한 뒤 rake release를 실행하세요. 축하합니다, 이제 당신도 젬 작가입니다! 그리고 그 젬을 GitHub에 푸시하면, 오늘 날짜의 예쁜 초록색 칸도 확보됩니다.

당신의 앱 중에서도 추출해볼 만한 부분이 있나요? 좋은 젬이 될 것 같은 코드가 있다면 아래 댓글로 알려주세요. 기꺼이 듣고 싶습니다.

루비 젬을 만들고, 관리하고, 유지보수하는 방법에 대해 훨씬 더 깊이 배우고 싶다면 Brandon Hilkert의 『Build a Ruby Gem』을 강력 추천합니다. 이 책이 소개하는 과정은 위 내용과 거의 유사하면서도 훨씬 더 많은 내용을 다루고 있습니다.