Ruby on Rails 업그레이드

이 가이드는 애플리케이션을 새로운 버전의 Ruby on Rails로 업그레이드할 때 따라야 할 단계를 제공합니다. 이러한 단계는 개별 릴리스 가이드에서도 확인할 수 있습니다.


일반적인 조언

기존 애플리케이션을 업그레이드하려면 먼저 업그레이드할 타당한 이유가 있는지 확인해야 합니다. 새로운 기능에 대한 필요, 오래된 코드에 대한 지원 어려움 증가, 사용 가능한 시간과 기술 등 여러 요인을 균형있게 고려해야 합니다.

테스트 범위

애플리케이션이 업그레이드 후에도 제대로 작동하는지 확인하는 가장 좋은 방법은 업그레이드 전에 충분한 테스트 범위를 확보하는 것입니다. 애플리케이션의 대부분을 실행하는 자동화된 테스트가 없다면 변경된 모든 부분을 수동으로 테스트해야 합니다. Rails 업그레이드의 경우 애플리케이션의 모든 기능을 테스트해야 합니다. 업그레이드를 시작하기 전에 테스트 범위가 충분한지 확인하는 것이 좋습니다.

Ruby 버전

Rails는 일반적으로 릴리스 시점의 최신 Ruby 버전과 가깝게 유지됩니다:

  • Rails 7.2는 Ruby 3.1.0 이상을 요구합니다.
  • Rails 7.0과 7.1은 Ruby 2.7.0 이상을 요구합니다.
  • Rails 6은 Ruby 2.5.0 이상을 요구합니다.
  • Rails 5는 Ruby 2.2.2 이상을 요구합니다.

Ruby와 Rails를 별도로 업그레이드하는 것이 좋습니다. 먼저 최신 Ruby로 업그레이드한 다음 Rails를 업그레이드하세요.

업그레이드 프로세스

Rails 버전을 변경할 때는 한 번에 하나의 마이너 버전씩 천천히 이동하는 것이 좋습니다. 이렇게 하면 사용되지 않는 기능에 대한 경고를 잘 활용할 수 있습니다. Rails 버전 번호는 Major.Minor.Patch 형식입니다. Major와 Minor 버전은 공개 API를 변경할 수 있으므로 애플리케이션에 오류가 발생할 수 있습니다. Patch 버전은 버그 수정만 포함하며 공개 API를 변경하지 않습니다.

프로세스는 다음과 같습니다:

  1. 테스트를 작성하고 통과시킵니다.
  2. 현재 버전의 최신 Patch 버전으로 이동합니다.
  3. 테스트와 사용되지 않는 기능을 수정합니다.
  4. 다음 Minor 버전의 최신 Patch 버전으로 이동합니다.

이 프로세스를 대상 Rails 버전에 도달할 때까지 반복합니다.

버전 간 이동

버전 간 이동하려면:

  1. Gemfile의 Rails 버전 번호를 변경하고 bundle update를 실행합니다.
  2. package.json의 Rails JavaScript 패키지 버전을 변경하고 bin/rails javascript:install을 실행합니다(jsbundling-rails를 사용하는 경우).
  3. 업데이트 작업을 실행합니다.
  4. 테스트를 실행합니다.

릴리스된 모든 Rails 젬은 여기에서 확인할 수 있습니다.

업데이트 작업

Rails는 rails app:update 명령을 제공합니다. Rails 버전을 Gemfile에서 업데이트한 후 이 명령을 실행하세요. 이 명령은 새로운 파일 생성과 기존 파일 변경 사항을 대화형으로 알려줍니다.

$ bin/rails app:update
       exist  config
    conflict  config/application.rb
Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
       force  config/application.rb
      create  config/initializers/new_framework_defaults_7_2.rb
...

차이점을 검토하여 예상치 못한 변경 사항이 있는지 확인하세요.

프레임워크 기본값 구성

새로운 Rails 버전은 이전 버전과 다른 구성 기본값을 가질 수 있습니다. 그러나 위에서 설명한 단계를 따르면 애플리케이션은 이전 Rails 버전의 구성 기본값으로 계속 실행됩니다. 이는 config/application.rbconfig.load_defaults 값이 아직 변경되지 않았기 때문입니다.

새로운 기본값을 점진적으로 적용할 수 있도록 업데이트 작업은 config/initializers/new_framework_defaults_X.Y.rb 파일(원하는 Rails 버전이 파일 이름에 포함됨)을 생성했습니다. 이 파일에서 새로운 구성 기본값을 점진적으로 활성화할 수 있습니다. 애플리케이션이 새로운 기본값으로 실행될 준비가 되면 이 파일을 제거하고 config.load_defaults 값을 변경할 수 있습니다.

Rails 7.1에서 Rails 7.2로 업그레이드

Rails 7.2의 변경 사항에 대한 자세한 내용은 릴리스 노트를 참고하세요.

Rails 7.0에서 Rails 7.1로 업그레이드

Rails 7.1의 변경 사항에 대한 자세한 내용은 릴리스 노트를 참고하세요.

자동로드 경로가 더 이상 $LOAD_PATH에 포함되지 않음

Rails 7.1부터 자동로더가 관리하는 디렉토리가 더 이상 $LOAD_PATH에 추가되지 않습니다. 즉, 수동 require 호출로는 해당 파일을 로드할 수 없습니다. 이는 어차피 해서는 안 되는 일입니다.

$LOAD_PATH의 크기를 줄이면 bootsnap을 사용하지 않는 앱의 require 호출 속도가 빨라지고, bootsnap 캐시 크기도 줄어듭니다.

이러한 경로를 여전히 $LOAD_PATH에 포함하고 싶다면 다음과 같이 옵트인할 수 있습니다:

config.add_autoload_paths_to_load_path = true

그러나 이렇게 하는 것은 바람직하지 않습니다. 자동로드 경로의 클래스와 모듈은 자동로드되도록 설계되었기 때문입니다. 즉, 그냥 참조하면 됩니다.

lib 디렉토리는 이 플래그의 영향을 받지 않고 항상 $LOAD_PATH에 추가됩니다.

config.autoloadlib 및 config.autoloadlib_once

애플리케이션에 lib이 자동로드 또는 자동로드 원스 경로에 포함되어 있지 않다면 이 섹션을 건너뛰세요. 다음 명령어를 실행하여 확인할 수 있습니다:

# 자동로드 경로 출력
$ bin/rails runner 'pp Rails.autoloaders.main.dirs'

# 자동로드 원스 경로 출력
$ bin/rails runner 'pp Rails.autoloaders.once.dirs'

애플리케이션에 이미 lib이 자동로드 경로에 포함되어 있다면 일반적으로 config/application.rb에 다음과 같은 구성이 있을 것입니다:

# lib을 자동로드하지만 적극적으로 로드하지는 않음(간과되었을 수 있음).
config.autoload_paths << config.root.join("lib")

또는

# lib을 자동로드하고 적극적으로 로드함.
config.autoload_paths << config.root.join("lib")
config.eager_load_paths << config.root.join("lib")

또는

# 동일함. 모든 적극적 로드 경로가 자동로드 경로가 되기 때문.
config.eager_load_paths << config.root.join("lib")

이러한 줄을 다음과 같이 더 간단한 구문으로 바꾸는 것이 좋습니다:

config.autoload_lib(ignore: %w(assets tasks))

ignore 목록에 .rb 파일이 없거나 다시 로드되거나 적극적으로 로드되면 안 되는 lib 하위 디렉토리의 이름을 추가하세요. 예를 들어 애플리케이션에 lib/templates, lib/generators, lib/middleware가 있다면 다음과 같이 추가하세요:

config.autoload_lib(ignore: %w(assets tasks templates generators middleware))

이 한 줄로 lib의 (무시되지 않은) 코드가 config.eager_loadtrue(기본적으로 production 모드)인 경우 적극적으로 로드됩니다. 일반적으로 이것이 원하는 동작이지만, lib이 이전에 적극적 로드 경로에 추가되지 않았고 여전히 그렇게 하고 싶다면 다음과 같이 옵트아웃할 수 있습니다:

Rails.autoloaders.main.do_not_eager_load(config.root.join("lib"))

config.autoload_lib_once 메서드는 애플리케이션이 config.autoload_once_pathslib을 포함하고 있었던 경우의 유사한 메서드입니다.

ActiveStorage::BaseController가 더 이상 스트리밍 관심사를 포함하지 않음

ActiveStorage::BaseController를 상속받고 커스텀 파일 서빙 로직을 구현하기 위해 스트리밍을 사용하는 애플리케이션 컨트롤러는 이제 ActiveStorage::Streaming 모듈을 명시적으로 포함해야 합니다.

MemCacheStoreRedisCacheStore가 기본적으로 연결 풀링을 사용함

connection_pool 젬이 activesupport 젬의 종속성으로 추가되었으며, MemCacheStoreRedisCacheStore가 기본적으로 연결 풀링을 사용합니다.

연결 풀링을 사용하고 싶지 않다면 :pool 옵션을 false로 설정하세요:

config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }

caching with Rails 가이드에서 더 자세한 정보를 확인할 수 있습니다.

SQLite3Adapter가 이제 엄격한 문자열 모드로 구성됨

엄격한 문자열 모드 사용은 이중 인용 문자열 리터럴을 비활성화합니다.

SQLite에는 이중 인용 문자열 리터럴과 관련된 몇 가지 특이점이 있습니다. 먼저 이중 인용 문자열을 식별자 이름으로 간주하지만 존재하지 않는 경우 문자열 리터럴로 간주합니다. 이로 인해 오타가 조용히 지나갈 수 있습니다. 예를 들어 존재하지 않는 열에 대한 인덱스를 생성할 수 있습니다. 자세한 내용은 SQLite 문서를 참고하세요.

SQLite3Adapter를 엄격한 모드로 사용하고 싶지 않다면 다음과 같이 비활성화할 수 있습니다:

# config/application.rb
config.active_record.sqlite3_adapter_strict_strings_by_default = false

ActionMailer::Preview에 대한 다중 미리보기 경로 지원

`계속해서 번역하겠습니다.

config.action_mailer.preview_path 옵션이 config.action_mailer.preview_paths로 대체됨

config.action_mailer.preview_path 옵션이 config.action_mailer.preview_paths로 사용되지 않게 되었습니다. 이 구성 옵션에 경로를 추가하면 메일러 미리보기 검색 경로로 사용됩니다.

config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"

config.i18n.raise_on_missing_translations = true가 이제 모든 누락된 번역에서 예외를 발생시킴

이전에는 뷰나 컨트롤러에서만 예외를 발생시켰습니다. 이제 I18n.t에 인식되지 않는 키가 제공되면 언제든 예외가 발생합니다.

# config.i18n.raise_on_missing_translations = true 인 경우

# 뷰 또는 컨트롤러에서:
t("missing.key") # 7.0에서는 예외 발생, 7.1에서도 예외 발생
I18n.t("missing.key") # 7.0에서는 예외 발생하지 않음, 7.1에서는 예외 발생

# 어디서든:
I18n.t("missing.key") # 7.0에서는 예외 발생하지 않음, 7.1에서는 예외 발생

이 동작을 원하지 않는다면 config.i18n.raise_on_missing_translations = false로 설정할 수 있습니다:

# config.i18n.raise_on_missing_translations = false인 경우

# 뷰 또는 컨트롤러에서:
t("missing.key") # 7.0에서는 예외 발생하지 않음, 7.1에서도 예외 발생하지 않음
I18n.t("missing.key") # 7.0에서는 예외 발생하지 않음, 7.1에서도 예외 발생하지 않음

# 어디서든:
I18n.t("missing.key") # 7.0에서는 예외 발생하지 않음, 7.1에서도 예외 발생하지 않음

또는 I18n.exception_handler를 사용자 정의할 수 있습니다. 자세한 내용은 i18n 가이드를 참고하세요.

AbstractController::Translation.raise_on_missing_translations가 제거되었습니다. 이는 비공개 API였으며, 이를 사용하고 있었다면 config.i18n.raise_on_missing_translations 또는 사용자 정의 예외 처리기로 마이그레이션해야 합니다.

bin/rails test가 이제 test:prepare 작업을 실행함

bin/rails test를 통해 테스트를 실행할 때 rake test:prepare 작업이 테스트 실행 전에 실행됩니다. test:prepare 작업을 향상시켰다면 향상된 내용이 테스트 실행 전에 실행됩니다. tailwindcss-rails, jsbundling-rails, cssbundling-rails와 같은 다른 타사 젬도 이 작업을 향상시킵니다.

자세한 내용은 Testing Rails Applications 가이드를 참고하세요.

단일 파일의 테스트(bin/rails test test/models/user_test.rb)를 실행하는 경우 test:prepare가 실행되지 않습니다.

@rails/ujs에서 가져오는 구문이 수정됨

Rails 7.1부터 @rails/ujs에서 모듈을 가져오는 구문이 수정되었습니다. Rails는 더 이상 @rails/ujs에서 모듈을 직접 가져오는 것을 지원하지 않습니다.

예를 들어, 라이브러리에서 함수를 가져오려고 하면 실패합니다:

import { fileInputSelector } from "@rails/ujs"
// ERROR: export 'fileInputSelector' (imported as 'fileInputSelector') was not found in '@rails/ujs' (possible exports: default)

Rails 7.1에서는 먼저 @rails/ujs에서 Rails 객체를 가져와야 합니다. 그런 다음 Rails 객체에서 특정 모듈을 가져올 수 있습니다.

Rails 7.1에서의 가져오기 예시는 다음과 같습니다:

import Rails from "@rails/ujs"
// 메서드 별칭
const fileInputSelector = Rails.fileInputSelector
// 또는 사용할 때 Rails 객체에서 참조
Rails.fileInputSelector(...)

Rails.logger가 이제 ActiveSupport::BroadcastLogger 인스턴스를 반환함

ActiveSupport::BroadcastLogger 클래스는 다양한 싱크(STDOUT, 로그 파일 등)에 로그를 쉽게 브로드캐스트할 수 있는 새로운 로거입니다.

로그를 브로드캐스트하는 API(ActiveSupport::Logger.broadcast 메서드)는 이전에 비공개였으며 제거되었습니다. 애플리케이션이나 라이브러리에서 이 API를 사용하고 있었다면 다음과 같이 변경해야 합니다:

logger = Logger.new("some_file.log")

# 이전에는

Rails.logger.extend(ActiveSupport::Logger.broadcast(logger))

# 이제는

Rails.logger.broadcast_to(logger)

애플리케이션에서 사용자 정의 로거를 구성했다면 Rails.logger가 모든 메서드를 프록시할 것입니다. 이 동작을 작동하게 하려면 별도의 변경이 필요하지 않습니다.

사용자 정의 로거 인스턴스에 액세스해야 하는 경우 broadcasts 메서드를 사용할 수 있습니다:

# config/application.rb
config.logger = MyLogger.new

# 애플리케이션 어디서든
puts Rails.logger.class #=> BroadcastLogger
puts Rails.logger.broadcasts #=> [MyLogger]

Active Record 암호화 알고리즘 변경

Active Record 암호화가 이제 SHA-256을 해시 다이제스트 알고리즘으로 사용합니다. 이전 Rails 버전에서 데이터를 암호화한 경우 두 가지 시나리오를 고려해야 합니다:

  1. config.active_support.key_generator_hash_digest_class를 SHA-1(Rails 7.0 이전의 기본값)로 구성한 경우 Active Record 암호화에도 SHA-1을 구성해야 합니다:

    config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1
    
  2. config.active_support.key_generator_hash_digest_class를 SHA-256(7.0의 새로운 기본값)으로 구성한 경우 Active Record 암호화에도 SHA-256을 구성해야 합니다:

    config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
    

Configuring Rails Applications 가이드에서 config.active_record.encryption.hash_digest_class에 대한 자세한 정보를 확인할 수 있습니다.

또한 a bug로 인해 일부 속성이 SHA-1로 암호화되는 문제를 해결하기 위해 새로운 구성 config.active_record.encryption.support_sha1_for_non_deterministic_encryption이 도입되었습니다.

기본적으로 Rails 7.1에서 config.active_record.encryption.support_sha1_for_non_deterministic_encryption은 비활성화되어 있습니다. Rails < 7.1 버전에서 암호화된 데이터가 위에서 언급한 버그의 영향을 받을 수 있다고 생각되는 경우 이 구성을 활성화해야 합니다:

config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true

암호화된 데이터를 다루는 경우 위의 내용을 주의 깊게 검토하세요.

컨트롤러 테스트, 통합 테스트, 시스템 테스트에서 예외 처리 새로운 방법

config.action_dispatch.show_exceptions 구성은 요청에 대한 응답 중 발생한 예외를 Action Pack에서 처리하는 방식을 제어합니다.

Rails 7.1 이전에는 config.action_dispatch.show_exceptions = true로 설정하면 Action Pack이 예외를 처리하고 적절한 HTML 오류 페이지(예: public/404.html404 Not found 상태 코드로 렌더링)를 렌더링했습니다. config.action_dispatch.show_exceptions = false로 설정하면 Action Pack이 예외를 처리하지 않았습니다. Rails 7.1 이전에는 새로 생성된 애플리케이션에 config/environments/test.rbconfig.action_dispatch.show_exceptions = false를 설정하는 코드가 있었습니다.

Rails 7.1은 허용 가능한 값을 truefalse에서 :all, :rescuable, :none으로 변경했습니다.

  • :all - 모든 예외에 대해 HTML 오류 페이지 렌더링(true와 동일)
  • :rescuable - config.action_dispatch.rescue_responses에 선언된 예외에 대해 HTML 오류 페이지 렌더링
  • :none(false와 동일) - 어떤 예외도 처리하지 않음

Rails 7.1 이상으로 생성된 애플리케이션은 config/environments/test.rb에서 config.action_dispatch.show_exceptions = :rescuable로 설정합니다. 업그레이드 시 기존 애플리케이션은 config.action_dispatch.show_exceptions = :rescuable로 변경하여 새로운 동작을 활용하거나, 이전 값을 해당하는 새로운 값으로 대체할 수 있습니다(true:all, false:none으로 대체).