ハトネコエ Web がくしゅうちょう

プログラミングやサーバー関連、チームマネジメントなど得た技術のまとめ

jQuery の ajax の書き方を Pure JS の fetch で書き直す

最近(遅いんですけど) querySelector メソッド知ったんですよ。

今までは、jQuery を使わない JavaScript だと
getElementByIdgetElementsByClassName 使ってじゃないと DOM 要素を取れないと思っていたので、
jQuery はその点強いよな〜と思っていたんですが、jQuery 使わなくてもいけるぞ、と。

const inputElement = $('input[name="user_name"]');

じゃなくて

const inputElement = document.querySelector('input[name="user_name"]');

のように書けるわけですね。

Internet Explorer 8 にも対応してるそうですから、私が知らないだけで、昔からあったのでしょうね。

で、思ったのがよくある jQuery のファイルを素の JavaScript で書いたら?という話。*1

やってみた

APIサーバーを用意して、JSFiddle で書いてみました。

ポイント1: DOMContentLoaded

jQuery

$(function () {
});

JavaScript

document.addEventListener('DOMContentLoaded', () => {
});

load に対するイベントリスナーを昔は書いてたけど DOMContentLoaded でいいらしい。

参考: DOMContentLoadedイベントとloadイベントの違い[タイミング]

DOMツリーが読み込めた段階で発火して普通は影響ないもんな、なるほどね。

そもそも最近の主流通り、JavaScript ファイルを HTML の head タグ内で読み込まず、
body タグ内の最後で読み込んでいるならこのイベントリスナー必要ないんだけど、
まあ、 jQuery 使ってるような昔ながらのサイトなら head 内で JS を読み込んでるでしょう? ということで。

ポイント2: querySelector

jQuery

$('#displayListButton').click(function() {
  $('#debug').html('Show Items ボタンが押されたよ');
});

JavaScript

document.querySelector('#displayListButton').addEventListener('click', () => {
  document.querySelector('#debug').innerHTML = 'Show Items ボタンが押されたよ';
});

最初に話した querySelector
いや〜、やっぱ jQuery の方がスッキリは書けますね。

あと、アロー関数( () => { の部分)を使っているけれど、
これは Internet Explorer だと動かないので、Internet Explorer にも対応させる場合はこうです。

document.querySelector('#displayListButton').addEventListener('click', function() {
  document.querySelector('#debug').innerHTML = 'Show Items ボタンが押されたよ';
});

ポイント3: fetch

jQuery

$.ajax({
  method: 'POST',
  url: 'https://example.com/api/third-power',
  data: { number: 22 },
})
.done(function(data) {
  $('#output').html(data.result);
})
.fail(function(error) { console.error(error); })

JavaScript

fetch('https://example.com/api/third-power', {
  method: 'POST',
  body: JSON.stringify({ number: 18 }),
  headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(jsonData => {
  document.querySelector('#output').innerHTML = jsonData.result;
})
.catch(error => { console.error(error); });

Internet Explorer 以外では動く fetch メソッドは超便利。
モダンな JavaScript なら、fetch と、Promise によるメソッドチェーンで普通に jQuery っぽく楽に書けるようになってていいですね。

Internet Explorer でも動かしたい場合は、XMLHttpRequests のラッパーである
axios というライブラリを使えばいいと思います。
Vue.js さわってる人たちの中ではおなじみのライブラリですね。

というわけで

jQuery オワコン言われながらも、
Bootstrap だとデフォルトで jQuery 読み込んでるし、つい便利メソッドあるから使っちゃうな〜、
って状態だったんですが、特によく使う便利メソッドの代替が存在していたのでまとめました。

Bootstrap 5 では jQuery 依存から脱却しますし、
今のうちにできれば jQuery 依存のコードを書かないようにしておきたい思いです。

*1:素のJavaScript: jQueryやKnockoutJSなどのラッパーライブラリを使わないで書くJavaScript

プログラミング初心者が HTML, CSS を独学で勉強するならなに使う?

HTMLCSSプログラミング言語じゃなくて
マークアップ言語スタイルシート言語じゃないか!」
…という話は置いておきます。

さて、Progateで勉強し始めた人と今日話して、
「あ〜そういえばテキストエディタや便利なサイトって知らないんだよなぁ」
と思ったので、まとめておきます。

Progate は無料で、あらゆるセットアップ抜きでプログラミング体験ができて良いサイトです。

とはいえ、1つ1つ進めていくのはけっこうかったるいし、勉強したことを見返しにくいしで、
ああいうドリル形式でやるより、参考書片手に自分の書きたいように書く、ってのが向いてる人もいると思いますので、その人向けに書きます。

1. まず、本を買う

Webでも情報は拾えるのですが、特に初心者は本が1冊はあるといいです
Webだと情報が点在しているのですが、本だと「あれやってこれやってこう」みたく体系的にまとまっているので、
1冊終わる頃にはその言語についての知識がしっかり定着するようになると思います。

また、情報が正確なのもいいところです。
Webの情報はほとんどが個人による執筆ですが、
本の場合は、たいてい著者の同僚エンジニア数人が査読(誤りがないかチェック)をおこないますし、
わかりにくい箇所がないか編集者のチェックがありますので、情報が洗練されています。

ただし、情報は古くなっていくものなので、
この2〜3年以内に発売された本を買うのが望ましいです。

選ぶ基準は、本屋で読み比べて自分が読み進められそうだなー、と思うものを買うのが一番いいです。
あとは Amazon レビューを参考にする手もありますが、名著だから評価高いけど読む気にならない本なんかもあるのでお気をつけて……。

JavaScript 第6版

JavaScript 第6版

↑ 例えばオライリーJavaScript本、名著だから☆4評価だけど、文字多いしレイアウトの「レ」の字もないような構成なので私は読む気になれない・・・

スラスラわかるHTML&CSSのきほん 第2版

スラスラわかるHTML&CSSのきほん 第2版

↑ ちょっと探した感じだと、例えばこんな本がおすすめです。
カラフルで画像が多くて読み進めやすく、本を真似すれば最終的に人に見せられるようなクオリティのものが作れる
という構成はモチベーション維持に役立つと思います。

2. 本をどう使うか?

さて本を買ってきたとしましょう。
初心者のうちは読み返すことが多いでしょうから、電子書籍でなく物理的な本がいいですよ!

買った本を読み進めながら、
本の中に書いてあるコードを実際に書き写して自分のパソコンで表示してみてください。

この書き写すって作業がけっこう大事で、本を眺めてるだけだとあんまり脳に残らないんですが、
実際に打ち込んでみると頭に残るし、さらにアレンジを加えてみる(例えば、本に書いてあるのと違う言葉を入れてみる、違う画像を使ってみる)と、
言われたままじゃない感じがして少し楽しくなります。

余談ですが、本に書いてあるコードを実際にパソコンで打ち込んでみる作業を、
仏教で経典を書き写す作業になぞらえて「写経(しゃきょう)」と言ったりします。

3. どこにコードを書くか

本を読みながら内容を書き写す、と上で言いましたが、
では HTML や CSS をどこに書くのがいいのでしょう。

3-1. Visual Studio Code

個人的には「Visual Studio Code」というテキストエディタ
プラグインとして

を入れて書いていく。という方法をおすすめしたいです。

f:id:nekonenene:20200321010949p:plain
「Live Server Previewプラグインで完成形を見ながら編集できる

言ってみればプロの開発者と同じ環境なので、
今後プログラミング経験をより積んでいく予定があるなら、この選択は大いにアリです。

3-2. JSFiddle

一方で、上の方法は
なんだかんだでセッティングにつまずいたり使い方がわからなかったり、で時間を多く使ってしまう可能性もあります。

それも勉強にはなるのですが、
「もっと手軽にやりたいな」って方や、「フロントエンドだけ勉強したら充分」って方には、
JSFiddle がオススメです。

f:id:nekonenene:20200321012732p:plain
JSFiddle でプレビューを見ながら作成できる!

HTML, CSS, JavaScript を手軽に書いて実行することが出来ます。

右上の設定から Code hinting(補完機能)と Auto-run code(Runボタンを押さなくても反映してくれる)をオンにしておくことをおすすめします。

f:id:nekonenene:20200321013434p:plain
オンにするとよい設定(※ Auto-run code はログイン済みユーザーのみ設定可能)

また、JavaScript のライブラリを設定することも可能なので、
jQuery を使いつつ JavaScript 書いてみたいな〜」っていう練習にも対応できます。

f:id:nekonenene:20200321014258p:plain
実はタブの上部を押すとこんな詳細設定が出てくる

4. HTML, CSS の勉強が終わったら?

本を1冊踏破して、本の内容を80%くらい理解したとしましょう。

なお、HTMLタグやCSSプロパティをすべて覚える必要はありません
多くの人が「こういうときどうするっけ」ってこういうサイトを見たり、やりたいことでググったりしています。
使用頻度の高いものは自然に覚えていくと思うので、丸暗記する必要は全くありません。

このあとは、作りたいサイトがある人はそれを作ってみたり、
プログラミングを勉強したい、という人は JavaScriptRuby, Python などに行ってみてもいいでしょう。

Progate とか見てると「HTML/CSSの次はJavaScript」と思うかもしれませんが、
決してそんなことはなくJavaScript を使わずとも PHPRuby on Rails でサイト作りをすることはできます。

「ユーザー登録の出来るWebアプリケーションを作りたい」と考えている人なら、
JavaScript の勉強は飛ばして LaravelRails の学習に進むのはアリです。
ただし、データベースを使うなど難度は上がるので、時間はかかるかもしれません。

個人の好みに合うプログラミング言語は人それぞれですから、
勉強し始めて挫折したら、他のプログラミング言語の勉強をしてみてもいいです。

プログラミング言語の本、なにか1冊を読破しましょう。
それが出来れば、プログラミング言語同士は共通点が多いので、
他の言語に関しては本いらずでだいたい理解できるようになっていると思います。

なにか迷った際は、お気軽にご相談ください。

twitter.com

PostgreSQL 公式の語るやっちゃいけないことリストがおもしろかった

なにげなく『週刊Railsウォッチ(20200115後編)』を読んでいたところ、

Don't Do This - PostgreSQL wiki

ってのが紹介されていたので読んだ。

BETWEEN を timestamp に対して使っちゃいけない話

これの『Don't use BETWEEN (especially with timestamps)』の項目が特に勉強になって、
項目見たときは「え、よく使ってるけどなんでダメだろう」と気になるも、解決法を見て納得。

記事では

SELECT * FROM blah WHERE timestampcol BETWEEN '2018-06-01' AND '2018-06-08'

でなく

SELECT * FROM blah WHERE timestampcol >= '2018-06-01' AND timestampcol < '2018-06-08'

を使いなさいと語られている。
悪い方のやり方だと 2018-06-08 00:00:00 が含まれてしまうためだ。

BETWEEN で 00:00:00 が含まれるのは知っている人が多いから、
Rails で例えば以下のように書かれているのを見たことがあった。

User.where(created_at: Time.parse("2019-12-01 00:00:00")..Time.parse("2019-12-01 23:59:59")).count

23:59:59 を終わりに指定するというのは、 Rails に限らずそこそこ見慣れた書き方だと思う。

ところがこれはミリ秒が考慮されていない。

MySQL の場合 v5.6 より前はミリ秒対応がされていなかったのでこれでも良かったけれど、
v5.6 になってミリ秒対応をしたため ( https://hacknote.jp/archives/2522/ )
対応するカラムでは 23:59:59.0001 から 23:59:59.9999 の間を無視してしまう。

というわけで、以下のように書くのがベターとなる。

User.where("created_at >= ?", Time.parse("2019-12-01")).where("created_at < ?", Time.parse("2019-12-02")).count

……まあ、こういう期間集計ってだいたい社内向けにデータを出すためのクエリなので、
そんなに大きな問題にならなかったりするけど。

Rails であれば BETWEEN を使うこんな書き方もいちおうアリ。

User.where(created_at: Date.parse("2019-12-01").beginning_of_day..Date.parse("2019-12-01").end_of_day).count

これは Time クラス及び TimeWithZone の end_of_day がマイクロ秒まで考慮されているため。

Date クラスの end_of_day は in_time_zone で TimeWithZone クラスに変換されたのちに end_of_day メソッドを呼び出しているようです。
https://github.com/rails/rails/blob/v5.2.4.1/activesupport/lib/active_support/core_ext/date/calculations.rb#L84-L87

ただしこれは罠があって、MySQL では範囲外の秒数が丸められるため、
結果 2019-12-02 00:00:00 が含まれる場合があります。
(参考: https://techlog.voyagegroup.com/entry/2015/04/09/162629

また、Rails v5.1.7 以前の DateTime クラスの end_of_day メソッドはミリ秒以下が考慮されていません。
Time クラスと同じメソッド名なのにややこしいです。

結果、やはり BETWEEN を timestamp 相手に使うのは得策でなく、

User.where("created_at >= ?", Time.parse("2019-12-01")).where("created_at < ?", Time.parse("2019-12-02")).count

のような書き方をするのがベターと言えるようです。
あまり Rails っぽい書きぶりではないけど。

timestamptz を使えの話

timestamp でなく timestamptz を使えという話も面白かった。
PostgreSQL ってそんな型もあるのか、という驚き。

でも Rails の migration で作れなくね? と思ったら
https://medium.com/@frodsan/activerecord-better-native-types-mappings-for-postgresql-b5391d14ea68 によると

t.column :updated_at, :timestamptz, null: false

のような書き方でいけるらしい。なるほどねーー!

今動かしてるもので変更するのは怖いけど、新しいWebアプリを作るとき(かつ、PostgreSQLでずっとやっていくと確定しているとき)は
考えてみてもいいかも。

文字数制限付きの varchar はあまり使用しないでの話

t.string :hogehoge, null: false, limit: 255, default: ""

こんな感じのスキーマ定義を見ることは多かったので、
「DBのサイズ抑えたいし limit は大事だよな〜」と思って、私も limit はよく付けてた。

でも、PostgreSQL としては「安直にそういうことしないで!」と書いている。
逆に「意味あるならOKよ」っていうところ。

記事内の例で書かれている「名字を20文字以内で設定した場合、Hubert Blaine Wolfe­schlegel­stein­hausen­berger­dorff さんが登録しようとしたときに困るよ!」って話がちょっとおもしろかった。
ググったら正確には

Wolfe­schlegel­stein­hausen­berger­dorff­welche­vor­altern­waren­gewissen­haft­schafers­wessen­schafe­waren­wohl­gepflege­und­sorg­faltig­keit­be­schutzen­vor­an­greifen­durch­ihr­raub­gierig­feinde­welche­vor­altern­zwolf­hundert­tausend­jah­res­voran­die­er­scheinen­von­der­erste­erde­mensch­der­raum­schiff­genacht­mit­tung­stein­und­sieben­iridium­elek­trisch­motors­ge­brauch­licht­als­sein­ur­sprung­von­kraft­ge­start­sein­lange­fahrt­hin­zwischen­stern­artig­raum­auf­der­suchen­nach­bar­schaft­der­stern­welche­ge­habt­be­wohn­bar­planeten­kreise­drehen­sich­und­wo­hin­der­neue­rasse­von­ver­stand­ig­mensch­lich­keit­konnte­fort­pflanzen­und­sicher­freuen­an­lebens­lang­lich­freude­und­ru­he­mit­nicht­ein­furcht­vor­an­greifen­vor­anderer­intelligent­ge­schopfs­von­hin­zwischen­stern­art­ig­raum

という598文字の名字らしい。長いよ、長すぎるよ!(笑)
https://en.wikipedia.org/wiki/Hubert_Blaine_Wolfeschlegelsteinhausenbergerdorff_Sr.

ところで、Postgres では(文字数制限なしの)ただの varchar と text はほとんど同じ挙動だそうな。
https://lets.postgresql.jp/documents/technical/text-processing/1

MySQL の場合は昔は TEXT が VARCHAR よりだいぶ検索遅かったらしいけど、
今はどのくらい両者に違いがあるのか。

varchar は text と違って SQL の標準の型らしいので、
とりあえず varchar 使っておけば安心かな。

なんだか想定より長い記事になってしまいましたが、
こんなところで最後にオススメの本を貼っておしまい。
BETWEEN の話は考えさせられたなあ……。

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

  • 作者:ミック
  • 出版社/メーカー: 翔泳社
  • 発売日: 2012/03/16
  • メディア: 単行本(ソフトカバー)

refile から shrine へ移行する方法を徹底解説

最近はレガシーなプロダクトの改善活動に没頭しているハトネコエです。

さて、そのプロダクトでは refile が使われていました。

の三点セットです。

もともと refile は CarrierWave の後継として同じ作者により作られたようですが、
上の3つの gem すべてが 2015年を境に更新されなくなり、 CarrierWave は2019年の今でも更新が続くという、
なんともかわいそうな結果になってしまったライブラリです。

1. 動機

さて、refile の欠点は、複数サイズのアップロードが出来ないことです。
基本的に、元の画像を拾ってきて refile-mini_magick を介してリサイズした画像を表示する、という思想なのですが、
これではユーザーが投稿した画像を並べるときにファイル取得に時間がかかってしまう場合があります。
また、ページ表示のたびに1枚1枚リサイズ処理をかけるのは、サーバーのCPUやメモリにも負担を与えていそうです。

できれば、アップロード時に複数サイズを上げるようにしたいな、と思い代替となるライブラリを探しました。
その中で、 shrinerefile からの移行方法についてもドキュメントに書かれており
「これは親切だな! 私でもできそう!」と思い、移行してみることにしました。

なお、 refile は依存ライブラリの関係で Rails 5 に上げることの障害ともなります。
こちらで紹介されているような、
モンキーパッチをあてたフォークリポジトリを用意して Gemfile に取り入れるなどで応急処置はできますが、
できれば早く refile から脱却するのが良いでしょう……。

2. shrine 導入の注意点

「よーし shrine にするぞー!」と思っても、shrine の最新版を入れることは出来ません。
なぜなら、 refile-s3 は aws-sdk-core v2 を使っていて
shrine の最新版である3系では aws-sdk-core v3 を使うためです。

以下のように、rubygems.org を使うのではなく GitHub から
特定のコミットIDを指定して aws-sdk v3 に対応した refile-s3 を取ってくる方法があると、この記事を書いているときに気付きましたが、
気付いていなかったので、私は shrine v2 を入れることで対応しました。
(とはいえ特定のコミットIDを指定して入れるのは挙動が大丈夫か怖いですね……)

gem 'refile-s3', github: 'refile/refile-s3', ref: '768d60d4e5e'

以下のように Gemfile に足しました。

gem 'shrine', '~> 2.19.0'
gem 'aws-sdk', '~> 2.11.421'

注意するのは、shrine のドキュメントは現在、基本的に shrine v3 に向けて書かれていることです。
shrine v2 と v3 では、プラグインの名前など異なります。

https://github.com/shrinerb/shrine/tree/v2.19.3/doc

ここにあるドキュメントを見て、 shrine v2 でのやり方を見ながら設定していきました。

3. refile から shrine へ

https://github.com/shrinerb/shrine/blob/v2.19.3/doc/refile.md#migrating-from-refile

基本的にはここに書かれています。

refile のための image_id カラムがあるテーブルに image_data カラムを追加し、
(hogehoge_id であれば hogehoge_data カラムを追加)
config/initializers/shrine.rb には例えば以下のように記述します。

require "shrine"
require "shrine/storage/file_system"
require "shrine/storage/s3"

# アップロードするディレクトリの指定
if Rails.env.test?
  Shrine.storages = {
    cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
    store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
  }
else
  s3_options = {
      bucket: ENV["AWS_S3_BUCKET_NAME"],
      region: ENV["AWS_REGION"],
      access_key_id: ENV["AWS_ACCESS_KEY_ID"],
      secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
  }

  Shrine.storages = {
      cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
      store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
  }
end

Shrine.plugin :activerecord
Shrine.plugin :validation_helpers

# CDN
# public: true を付けないとGET時にも X-Amz-Algorithm X-Amz-Credential などのパラメーターが付いたままになってしまう
if Rails.env.production?
  cdn_host = "http://xxxyyyzzz.cloudfront.net"
  Shrine.plugin :default_url_options, cache: { public: true, host: cdn_host }, store: { public: true, host: cdn_host }
end

# Refile からの移行用
module RefileShrineSynchronization
  def write_shrine_data(name)
    if read_attribute("#{name}_id").present?
      data = {
        storage: :store,
        id: send("#{name}_id"),
        metadata: {
          size: (send("#{name}_size") if respond_to?("#{name}_size")),
          filename: (send("#{name}_filename") if respond_to?("#{name}_filename")),
          mime_type: (send("#{name}_content_type") if respond_to?("#{name}_content_type")),
        }
      }

      write_attribute(:"#{name}_data", data.to_json)
    else
      write_attribute(:"#{name}_data", nil)
    end
  end
end

その上で、

class PhotoAlbum < ActiveRecord::Base
  attachment :image
end

のようなクラスがあるのなら、以下のように5行加えます。

class PhotoAlbum < ActiveRecord::Base
  attachment :image

  include RefileShrineSynchronization

  before_save do
    write_shrine_data(:image) if changes.key?(:image_id)
  end
end

これによって、refile による画像アップロードがおこなわれた際に、
shrine で扱う image_data カラムも同時に更新されるようになります。

attachment のある全てのモデルにこの追加をおこなったら、 まずは一度本番稼働させると良いでしょう。

4. image_id を image_data にコピー

3. の作業でユーザーがこれからアップロードするぶんはコピーされますが、
今までの image_id も image_data へ移行しなければいけません。

例えばこのようなスクリプトを用意して、移行させることが出来ます。

include RefileShrineSynchronization

def migrate(model_class: nil, column: :image)
  return if model_class.nil?

  puts "Start migrate #{model_class}"
  model_class.find_each do |model|
    if model.send("#{column}_id").present?
      model.write_shrine_data(column)
      model.save!
    end
  end
  puts "End migrate #{model_class}"
end

migrate(model_class: PhotoAlbum)
migrate(model_class: UserProfile, column: :avatar_image)

scripts/migrate_refile_to_shrine.rb として作ったとしたなら、

bundle exec rails runner scripts/migrate_refile_to_shrine.rb

で実行します。

5. attachment_image_tag 及び attachment_url からの卒業

ここが一番大変です。
こういうものがあるわけです。(※slim記法です)

= attachment_image_tag photo_album, :image, :fill, 320, 320, fallback: "blank.png"

fill なのでつまり、アスペクト比を保ったまま幅と高さを最低 320px に拡大し、
その上で 320x320 の正方形に切り取っているという処理です。

けっこう悩みましたが、最近のCSSには object-fit というものがあると知れたので、
なんとか以下のように書き直すことが出来ました。

= image_tag (photo_album.image_url || "blank.png"), style: "width: 320px; height: 320px; object-fit: cover;"

ただしこれをやると img の width, height を上書きすることになるので、
実は fill, 320, 320 で処理させてるけど、
CSS は width: 100%, height: 100% で、外側のコンテナサイズ的に 200px x 200px で表示されているのが元の表示という罠もあったりして、
実際の画面表示を確認することになりけっこうしんどかったです。

attachment_image_tag, attachment_url を無くすのが、移行の一番しんどい作業だと思います。

6. 複数サイズのアップロード

5. の作業のあと、もしくはある程度並行しておこなえるといいのが、
複数サイズのアップロード機能。

https://github.com/shrinerb/shrine/blob/v2.19.3/doc/plugins/versions.md に詳しく書かれています。

Gemfile に image_processingmini_magick を追加したあと、
app/uploaders/image_uploader.rb に、例えば以下のように記述します。

require "image_processing/mini_magick"

class ImageUploader < Shrine
  plugin :processing
  plugin :versions

  process(:store) do |file, context|
    versions = { original: file }

    file.download do |original|
      # loader(page: 0) により、アニメーションPNG画像がアップされた際にエラー吐かないように
      pipeline = ImageProcessing::MiniMagick.loader(page: 0).source(original)
      versions[:large]  = pipeline.auto_orient.resize_to_limit!(1280, 1280)
      versions[:medium] = pipeline.auto_orient.resize_to_limit!(640, 640)
      versions[:small]  = pipeline.auto_orient.resize_to_limit!(240, 240)
    end

    versions
  end

  Attacher.validate do
    validate_max_size 10 * 1024 * 1024, message: "のファイルサイズを10MB以内に収めてください"
    validate_mime_type_inclusion %w(image/jpeg image/png), message: "は JPEG, PNG 形式のみアップロード可能です"
  end
end

PhotoAlbum モデルが 3. の作業で以下のようになっていたと思います。

class PhotoAlbum < ActiveRecord::Base
  attachment :image

  include RefileShrineSynchronization

  before_save do
    write_shrine_data(:image) if changes.key?(:image_id)
  end
end

これを以下のようにします。

class PhotoAlbum < ActiveRecord::Base
  include ImageUploader[:image]
end

そうです、 refile からの完全な卒業です。
(ここ、安全に移行できる方法があったら教えてください……)

モデルをこのように変えると、 attachment_image_tag などの attachment 系がある view でエラーを吐くようになるのでお気をつけください。
なので、 5. の作業を終わらせてからが一番いいですね。

= f.attachment_field :image

= f.file_field :image, accept: "image/jpeg, image/png"

などと書き換えるのも忘れずにおこないましょう。

5. の作業で image_url を使うよう書き直したと思いますが、
ファイルにアクセスする方法が変わることにも気をつけましょう。

versions plugin を用いる場合、

= image_tag photo_album.image_url

のようにはアクセスできません。
must call Shrine::Attacher#url with the name of the version とエラーが出てしまいます。

= image_tag photo_album.image[:medium].url

もしくは

= image_tag photo_album.image_url(:medium)

という形で、バージョンを指定してあげる必要があります。

おすすめは後者の書き方で、 versions が存在しない image_data の場合、
image_url を見てくれます。

ちょっと説明しづらいんですが、
この 6. の作業終了と次の 7. の作業終了の合間にユーザーがアクセスしてきてもエラーが起きない、ということだと理解してもらえればと思います。

7. すべての画像のアップロードし直し

難度の高い 5. , 6. のタスクを終わらせて、
いよいよここまで来れたならほとんど終了です。

4. でおこなった image_id カラムからのコピー作業の結果、
image_data カラムの中身は以下のようになっています。

"{\"storage\":\"store\",\"id\":\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\"metadata\":{\"size\":null,\"filename\":null,\"mime_type\":null}}"

一方、 versions plugin とともにアップロードがおこなわれた image_data カラムは
以下のようになります。

"{\"original\":{\"id\":\"yyyyyyyyyyyyyyyyyyyyyyyy.png\",\"storage\":\"store\",\"metadata\":{\"filename\":\"aaaaaaaaaaaaaaaaaaaa.png\",\"size\":3139040,\"mime_type\":\"image/png\"}},\"large\":{\"id\":\"zzzzzzzzzzzzzzzzzzzzzzzz.png\",\"storage\":\"store\",\"metadata\":{\"filename\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.png\",\"size\":4239003,\"mime_type\":null}},\"medium\":{\"id\" # 以下省略

だいぶ構造が変わりますね。

現在のままでもサイトは動きますが、サイトのスピードアップのためにも、
過去のすべての画像に関しても複数サイズの画像を生成しましょう。

https://github.com/shrinerb/shrine/blob/v2.19.3/doc/regenerating_versions.md#adding-versions の記事を参考に、
以下のようにスクリプトを組み立てました。

require "image_processing/mini_magick"

class ReuploadImages
  def exec(model_class: nil, column: :image)
    return if model_class.nil?
    puts "##### #{model_class} #####"

    model_class.find_each do |model|
      begin
        attacher, attachment = model.send("#{column}_attacher"), model.send(column)
        if attacher.stored? && !attachment.is_a?(Hash)
          puts "Start #{model.id}"
          update_attach(attacher, attachment)
        else
          puts "Skipped #{model.id}"
        end
      rescue => e
        puts "!! Error in #{model_class} ID: #{model.id} !!"
        puts e
      end
    end
  end

  def update_attach(attacher, attachment)
    versions = { original: attachment }

    attachment.download do |original|
      pipeline = ImageProcessing::MiniMagick.loader(page: 0).source(original)
      versions[:large]  = attacher.store!(pipeline.auto_orient.resize_to_limit!(1280, 1280), version: :large)
      versions[:medium] = attacher.store!(pipeline.auto_orient.resize_to_limit!(640, 640), version: :medium)
      versions[:small]  = attacher.store!(pipeline.auto_orient.resize_to_limit!(240, 240), version: :small)
    end

    attacher.swap(versions)
  end
end

reupload = ReuploadImages.new
reupload.exec(model_class: PhotoAlbum)
reupload.exec(model_class: UserProfile, column: :avatar_image)

scripts/reupload_images.rb に作り、

bundle exec rails runner scripts/reupload_images.rb

で走らせるイメージです。
データ行の数によっては、分割して実行させるといいかもしれません。

8. おしまい!!(9. の追記が増えました)

長い道のりでしたが、あとは refile 系を Gemfile から削除し、
shrine v3 へのバージョンアップをドキュメントを読みながら進めていけば、
すっかり shrine への移行が完了です。

shrine v3 への移行ドキュメント : https://shrinerb.com/docs/upgrading-to-3

shrine はドキュメントが充実していて、それに助けられる部分が多いです。
この記事を書いている段階では私は 5. 〜 6. のステップにいるのですが、
なんとかして終わらせて画像の読み込みスピードを改善したいと思います。

ここまで読んでいただき、どうもありがとうございました!

Ruby on Rails 6 実践ガイド (impress top gear)

Ruby on Rails 6 実践ガイド (impress top gear)

9. CloudFront 設定の変更【追記:2020/01/05】

いよいよほとんどの作業が終わった段階でハマったところがあったので記します。

refile と shrine の挙動の違いによる、CloudFront に必要な設定変更です。

refile では CloudFront の Origin をサイトURLにしていたと思いますが、
shrine を使う上では Origin を S3 のアドレスにする必要があります

また、S3 の画像が公開設定でない場合、
以下の記事を参照し、CloudFront から S3 へのアクセス許可を付与してください。

S3の特定バケットへのアクセスを特定のCloudFrontからのみ許可する。 - Qiita

コードをちゃんと追ってないので不正確かもですが、
refile ではデフォルトでは /attachments にルーティングを付与していて
CDN に画像がなければ S3 にアクセスし、ファイルを取得し、必要あれば加工して表示する、という一連の流れを踏むわけです。

というわけで サイト → CloudFront → キャッシュなければサイト → S3 → サイトで加工 → ユーザーへ という通信の流れです。

一方、 shrine ではデフォルトではルーティングを作成していません。

というわけで サイト → CloudFront → キャッシュなければS3 → ユーザーへ という通信の流れにしないといけません。

Origin をサイトURLのままにしておくと、サイトにアクセスしたところでその画像はないので 404 が返ります。
めちゃくちゃ悩みました。『S3 404 エラー』で検索したりしてましたが、S3の問題ではなかったですね(笑)

対応としては、CloudFront の Distribution を新たに作って、
そちらの CDN ホストを shrine では扱うようにしました。

いやぁ〜、難しかった。
追記は以上です!