新卒エンジニアが書き溜めた技術メモ 4選
はじめに
こんにちは、namonaki です。新卒で入社して半年、分からなかったことを個人用のメモに書き留めてきました。その中からいくつか選りすぐって、改めて調べつつ記事にしてみました。
■ 対象
Rails や React でコードは書けるが、「なぜこう書くと動くのか」はまだ説明できない人
普段なんとなく使っている仕組みの裏側を、ソースコードや DevTools で確かめてみたい人
■ 扱うこと
ブラウザ・Web標準
React と Islands Architecture
Rails
テスト
各章は独立しているので、気になるところから読んでください。参考にした資料は、本文中の番号から末尾の脚注に飛べます。
!
前提のバージョン(2026年10月時点)
Ruby 4.0 / Rails 8.1
React 17 / Node.js 24
factory_bot 6.5
1. ブラウザ・Web標準
JSON-LD(検索エンジン向けの構造化データ)について調べていたとき、JS を書くわけでもないのに、なぜ <script> タグを使うのか疑問に思ったのがきっかけです。
1.1 <script> の属性なし / async / defer の違い
調べてみると、HTML の要素を探して操作する JS には、defer を付けるのが基本でした。ダウンロードは早く始めつつ、実行は HTML の解析が終わるまで待ってくれるからです。
<script src="a.js"></script> <!-- ① 属性なし -->
<script src="a.js" async></script> <!-- ② async -->
<script src="a.js" defer></script> <!-- ③ defer -->
HTMLの解析を止めるか
実行タイミング
実行順
属性なし
止める
読み込んだらすぐ
書いた順
async
ダウンロード中は止めない
ダウンロードが終わりしだい
保証なし
defer
止めない
HTMLの解析が終わった後
書いた順
defer は「ダウンロードは早く、実行は最後」という良いとこ取り1
念のため、コード側でも DOMContentLoaded を待つと二重の保険になる
querySelectorAll のように要素が見つからなくてもエラーにならない書き方では、何も起きないだけなので気づきにくい。defer を付けておけば、この「要素がまだ無い」事故を防げる
1.2 Webのルールは誰が決めているのか
調べてみると、Web のルールは1つの団体が決めているわけではなく、部分ごとに別の団体が決めていました。
パンくずリストを構造化データで書いたときの例で見てみます。
<script type="application/ld+json">
{
"@context": "schema.org",
"@type": "BreadcrumbList",
"itemListElement": [ ... ]
}
</script>
この数行でも、部分ごとにルールを決めている団体が違いました。
部分
ルール
決めている団体
<script> タグ
HTML
WHATWG
application/ld+json
MIMEタイプの仕組み / 登録
IETF / IANA
{ ... } の書き方
JSON
IETF・Ecma International
@context などの書き方
JSON-LD
W3C
BreadcrumbList という単語
語彙
schema.org
WHATWG:HTML と DOM。ブラウザを作る企業が中心。2019年に W3C と合意し、HTML は「HTML Living Standard」に一本化。版の区切りはなく更新され続ける2
W3C:CSS、アクセシビリティ(WAI-ARIA)、JSON-LD など3
IETF:HTTP や JSON など、通信とデータ形式の基礎(RFC)4
IANA:MIMEタイプの一覧、ドメイン名、IPアドレスの登録・管理5
Ecma International:JavaScript の文法(ECMAScript)6
schema.org:Google・Microsoft・Yahoo! などによる共同プロジェクト。標準化団体ではなく、構造化データ用の単語帳を作っている7
■ 補足
+json は「中身は JSON 形式」という意味の接尾辞8
<script> の type に JS 以外を指定すると、ブラウザは実行せずデータとして置いておく。検索エンジンはこれを読む
2. React と Islands Architecture
Wikipedia のような辞書的なページは、全体を SPA にするとかえって重くなるので、一部分だけを React で動くようにすることがあると思います。こうした構成にも何か名前が付いているはずだと思い、調べてみたのがきっかけです。
2.1 「一部だけReact」すなわち Islands Architecture …ではなかった
最初は、一部だけ React を使う構成ならすべて Islands Architecture だと思っていましたが、サーバーが空の div を返し、ブラウザで React が中身を一から作る構成は、Islands Architecture ではありませんでした。Islands では島の HTML もサーバーが作り、ブラウザはそれを hydrate するからです。
■ Islands Architecture の条件9
島(インタラクティブな部分)の初期 HTML もサーバーが描画する
ブラウザはその HTML に hydrate(既存の要素を再利用して、イベントや状態を結び付ける)する
島の外には JS を送らない
■ 空の div にマウントする構成(React 17 の例)
// サーバーが返すのは空の div だけ。ブラウザ側で中身を一から作る(render)
document.querySelectorAll('.search-widget').forEach((el) => {
// サーバーからの値は data-params 属性で受け取る
ReactDOM.render(<SearchWidget params={JSON.parse(el.dataset.params)} />, el);
});
サーバーが返す HTML と、JS 実行後の HTML を並べるとこうなります。
【render(空の div にマウントする構成)】
サーバーが返したHTML JS 実行後
<div class="search-widget"> <div class="search-widget">
(空っぽ) → <form>...</form> ← JS が一から作る
</div> </div>
【hydrate(Islands Architecture)】
サーバーが返したHTML JS 実行後
<div> <div>
<form>...</form> → <form>...</form> ← 要素は作り直さず、イベントや状態を結び付ける
</div> </div>
見分け方:JS を切ったときに島の中身が見えるか
React 17 なら ReactDOM.render が render、ReactDOM.hydrate が hydrate10
React 18 以降は、createRoot().render() と hydrateRoot() に置き換わっている11
「島の外には JS を送らない」点だけは Islands と共通
2.2 hydrate で起きていること
調べてみると、hydrate は、サーバーが作った要素をそのまま使い、クリック時の処理や状態を後から結び付ける処理でした。
サーバーから届いた HTML は「見た目」だけで、ボタンを押しても何も起きません。素の JS で、後から動くようにするとこうなります12。
<button id="like">いいね 0</button>
// 既にあるボタンを探して、処理を後付けする(要素は作り直さない)
const button = document.getElementById('like');
let count = 0;
button.addEventListener('click', () => {
count += 1;
button.textContent = いいね ${count};
});
これが「既存の HTML にイベントを結び付ける」いちばん素朴な形
React の hydrate も考え方は同じで、中では次のことをしている
サーバーが返したHTML(見た目だけ。押しても動かない)
<form><input><button>検索</button></form>
↓ ReactDOM.hydrate(<SearchForm />, el)
① コンポーネントを実行して、「本来こうなるはず」の設計図をメモリ上に作る
② 設計図と既存の要素を先頭から照らし合わせ、一致した要素を「この部品の担当」として紐付ける
③ onClick などのイベント処理を受け付けられるようにする
④ useState の状態や useEffect を動かし始める
↓
見た目は同じまま、押せば動く状態になる
設計図と HTML が食い違うと、React 17 は開発モードで警告を出す。テキストの違いは書き換えて合わせるが、属性の違いは直らないことがあるので、食い違いはバグとして直す10:1
②で作る「設計図 ↔ 実際の要素」の対応表は、正確には Fiber というオブジェクトで、React はこれを使って書き換える要素や、押されたボタンの担当を見つけている13
■ ブラウザで確かめる
Fiber が実際の DOM 要素に付いている様子は、React で作られたサイトなら DevTools で見られます。ここでは React 公式サイトの react.dev を使います。
Chrome で react.dev を開き、DevTools を開く(Mac なら ⌘ + Option + I)
DevTools の左上にある矢印のアイコン(要素を選ぶボタン)を押してから、「Learn React」ボタンをクリックする。矢印を押さずにクリックすると、リンク先へページが移動してしまう
Console タブで $0(Elements タブで選択中の要素)を実行し、<a … aria-label="Learn React" …>Learn React</a> が表示されることを確かめる
次を実行する
Object.keys($0)
// => ['reactFiber$9s34k04vvrf', 'reactProps$9s34k04vvrf']
// (2026年10月時点。$ の後ろの文字列はページを開くたびに変わる)
$0[Object.keys($0).find(k => k.startsWith('__reactProps$'))]
// => {className: 'w-full sm:w-auto …', aria-label: 'Learn React', target: '_self', ref: ƒ, onClick: ƒ, …}
reactFiber$… が Fiber への参照、reactProps$… が props。props の中に onClick の関数が入っているのが見える
react.dev は React 18 以降で作られているが、キーの名前は React 17 と同じ
これらは React 内部の仕組みで、公開された機能ではない。観察だけにとどめ、自分のコードから読み書きしない
2.3 render と hydrate の違いのまとめ
違うのは「要素を一から作るか、サーバーが作ったものを再利用するか」だけでした。Fiber との紐付け(2.2 の②)、イベントの受け付け(2.2 の③)、状態や副作用の開始(2.2 の④)は、どちらも同じです。
ここに「ページの一部か全体か」という範囲の軸を加えると、よく聞く構成は次の4つに整理できます。
render(要素を一から作る)
hydrate(サーバーが作った要素を再利用)
ページの一部
空の div にマウントする構成
Islands Architecture
ページ全体
SPA
サーバーで描画して hydrate(Next.js など)
どのマスでも、マウントした要素より内側は Fiber と紐付く。違うのは「どこまでを React に任せるか」と「要素を作るか、再利用するか」だけ
SPA もページ全体が Fiber と紐付くが、サーバーが返すのは空の div なので、hydrate ではなく render
Islands との違いは hydrate する範囲で、Islands は島ごとに別々の React として hydrate し、島の外は React と無関係の HTML のまま。Next.js はページ全体(document)を1つの React として hydrate する
3. Rails
Rails のチュートリアル系のサイトや動画で「コントローラーで @ を付けた変数は、ビューでも @ を付けて参照できる」と聞いたものの、裏で何が起きているのか分からず、Rails を学び始めてすぐに疑問に思ったのがきっかけで調べました。
3.1 コントローラーのインスタンス変数がビューに渡る仕組み
コントローラーのインスタンス変数がビューで使えるのは、Rails がビュー用のオブジェクトに書き写しているからでした。さらに、テンプレートはそのオブジェクトのメソッドに変換されて実行されていました(Rails 8.1 のソースより)14。
class UsersController < ApplicationController
def show
@user = User.find(params[:id]) # @ 付きならビューで使える
posts = @user.posts # @ なしはこのメソッドの中だけ
end
end
3.2 そもそも、なぜ書き写す必要があるのか
インスタンス変数は、それを持っているオブジェクトの中からしか見えません15。
@user を持っているのは、UsersController のインスタンス
テンプレートを実行するのは、それとは別のビュー用オブジェクト
別々のオブジェクトなので、ビューで @user を使うには、ビュー用オブジェクト側にも同じインスタンス変数が必要
そこで Rails が、コントローラーの変数を1つずつビュー用オブジェクトに書き写している
3.3 全体の流れ
ブラウザから GET /users/1(ユーザーの詳細画面)のリクエストが届く
↓ ルーティング(resources :users)が users#show に振り分け、params[:id] = "1" にする
① コントローラーを new して、show アクションを実行する
↓ @user = User.find(1)
↓ render(書かなくても自動で呼ばれる)
② 集める view_assigns … コントローラーのインスタンス変数をハッシュにする
↓ { "user" => #<User id: 1> }
③ 作る view_context … ビュー用オブジェクトを new する(ハッシュとコントローラー自身を渡す)
(new するクラスの正体は ④ で補足)
↓
⑤ 書き込む assign … ハッシュの中身を、ビュー用オブジェクトのインスタンス変数にする
↓
⑥ テンプレート実行 テンプレートを「ビュー用オブジェクトのメソッド」に変換して呼ぶ
→ テンプレートの @user は、ビュー用オブジェクトの @user
以下、順に見ていきます。
① コントローラーはリクエストごとに new される
ソースを読むと、Rails はリクエストが届くたびにコントローラーを new していました。ルーティングが users#show と判断すると、UsersController.dispatch というクラスメソッドが呼ばれ、その中で new されます(actionpack action_controller/metal.rb)。
def self.dispatch(name, req, res)
...
new.dispatch(name, req, res) # UsersController.new して、そのインスタンスで処理する
end
Web サーバー(Puma など)がリクエストを受け取る
↓ Rails.application.call(env)
Rack ミドルウェア(Rails の中で順番に呼ばれる前処理の部品。セッション・Cookie など)
↓
ActionDispatch::Routing::RouteSet#call … ルーティング
↓ Journey::Router#serve … GET /users/1 に合うルートを探す
↓ RouteSet::Dispatcher#serve … "users" → UsersController クラスに変換
UsersController.dispatch(:show, req, res) … Metal に定義されたクラスメソッド
↓ new.dispatch(:show, req, res) … self.new = UsersController.new
UsersController のインスタンス#dispatch … 以降はこのインスタンスの上で動く
Rack は、Ruby の Web サーバー(Puma など)とアプリ(Rails など)をつなぐ共通ルール。「call(env) を受け取って [ステータス, ヘッダー, 本文] を返す」という約束だけを決めている
env は、リクエストの情報(URL、HTTP メソッド、ヘッダーなど)を詰めたハッシュ
Puma・Rack ミドルウェア・Rails は別々のサーバーではなく、同じ1つのプロセスの中で動いている
リクエストごとに別のインスタンスなので、別のユーザーのリクエストと @user が混ざることはない
② 集める:view_assigns
actionpack abstract_controller/rendering.rb のメソッドです。
show の実行後に呼ばれたときの値を、コメントで書き込んでいます。
def view_assigns
variables = instance_variables - _protected_ivars
instance_variables => [:@_request, :@_response, :@_params, :@_routes, ..., :@user] ← Rails 内部用も混ざっている
配列の引き算で内部用を外す => [:@user]
variables.each_with_object({}) do |name, hash| # name = :@user、hash は最初は空の {}
hash[name.slice(1, name.length)] = instance_variable_get(name) # "@user" の2文字目以降 "user" をキーに、@user の値を詰める
end
=> { "user" => #<User id: 1> }
end
使っているのは、Ruby の標準メソッド2つだけでした16。
メソッド
何をするか
instance_variables
そのオブジェクトが持つインスタンス変数の名前を、シンボルの配列で返す
instance_variable_get(名前)
名前を指定して、その変数の値を取り出す
instance_variables はレシーバーを省略しているので、self.instance_variables と同じ。self は①で new されたコントローラーのインスタンス
除外する _protected_ivars は、@_params や @_request など、Rails 内部用の変数を並べた一覧
each_with_object({}) は、空のハッシュを用意して1件ずつ詰め、最後にそのハッシュを返すメソッド
つまり、コントローラーの中身を一覧して、@ 付きの変数を「名前 → 値」のハッシュに変換しているだけでした。
③ 作る:view_context
actionview action_view/rendering.rb のメソッドです。
def view_context
view_context_class.new(lookup_context, view_assigns, self)
end
第1引数:どのテンプレートファイルを使うかを決める条件(探すフォルダ、言語、html / json などの形式)
第2引数:②で作ったハッシュ
第3引数:コントローラー自身(self)
■ self はなぜコントローラーなのか
view_context は actionview の ActionView::Rendering モジュールに書かれていますが、それでも self はコントローラーでした。実行時の self は、「そのメソッドがどのファイルやモジュールに書かれているか」ではなく、「ドットの左側にいたオブジェクト(レシーバー)」で決まるからです17。
module Greeting
def who = self
end
class UsersController
include Greeting # Greeting のメソッドを、UsersController のインスタンスから呼べるようにする
end
c = UsersController.new
c.who.equal?(c) # => true(レシーバーの c が self になる)
UsersController.ancestors.first(2) # => [UsersController, Greeting]
include は、メソッドをコピーするのではなく、メソッドを探す経路(ancestors)にモジュールを追加する
c.who と呼ぶと、Ruby は UsersController → Greeting の順に who を探し、見つけたメソッドを c の上で実行する
Rails の view_context も同じで、コントローラーに include されたモジュールのメソッドです。①で new したコントローラーのインスタンスから、レシーバーを省略して(self.〜 として)呼ばれ続けるので、self はずっとそのインスタンスのままです。そのため第3引数の self には、リクエストを処理しているコントローラー自身が渡されます。
④ ビュー用のクラスは「無名クラス」
new しているクラス view_context_class は、ActionView::Base を継承した無名クラス(名前のないクラス)でした。
■ 無名クラスとは
Ruby では Class.new でクラスを作れます18。それを定数(大文字で始まる名前)に入れると名前が付き、入れなければ無名クラスになります。
Dog = Class.new { def hi = "hi" }
dog = Class.new { def hi = "hi" }
Dog.name # => "Dog"
dog.name # => nil(中身は同じでも、ローカル変数に入れただけでは名前が付かない)
dog.new.hi # => "hi"(変数 dog を通して、普通のクラスと同じように使える)
違うのは名前の有無だけで、継承・include・メソッド定義・new は普通のクラスと同じようにできる
無名クラスでも、使い回すには変数などに入れておく必要がある。ただし変数はクラスを入れておく箱にすぎず、変数名はクラスの名前にならない。箱は何個あってもよく入れ替えもできるので、クラス側には「自分がどの名前で呼ばれているか」が分からない。名前として記録されるのは、最初に入れた定数だけ
Rails はビュー用の無名クラスを、コントローラーのクラス側に @view_context_class として入れてキャッシュし、③の view_context_class.new(...) で取り出して new している
■ ヘルパーが呼べる理由
Rails は、この無名クラスにルーティングのヘルパーとアプリのヘルパーを include しています(action_view/rendering.rb)。
Class.new(klass) do
include routes.url_helpers(supports_path) # users_path など
include helpers # app/helpers/ のメソッド
end
ビュー用オブジェクトはこのクラスのインスタンスなので、ビューで users_path や自作のヘルパーが呼べる
ancestors をたどると、無名クラス → ヘルパーのモジュール → … → ActionView::Base の順にメソッドを探している
⑤ 書き込む:initialize と assign
③の new を呼ぶと、Ruby は次の2つを順に行います。
メモリ上に空のオブジェクトを作る
そのオブジェクトの initialize を、new と同じ引数で自動的に呼ぶ
action_view/rendering.rb
view_context_class.new(lookup_context, view_assigns, self)
↓ ↓ ↓
action_view/base.rb ↓ ↓ ↓
def initialize(lookup_context, assigns, controller)
@lookup_context = lookup_context
...
assign_controller(controller) # @_controller にコントローラーを入れる
...
assign(assigns) # 最後に、②のハッシュを書き込む
end
def assign(new_assigns)
@_assigns = new_assigns
new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
assign は、②の view_assigns のちょうど逆の操作でした。
② コントローラー側
⑤ ビュー側
使うメソッド
instance_variable_get
instance_variable_set
名前の変換
:@user → "user"
"user" → "@user"
結果
変数 → ハッシュ
ハッシュ → 変数
instance_variable_set("@user", value) は、@user = value を、変数名を文字列で指定して書いたもの19
@user = value と書くと変数名はコードを書いた時点で決まるが、instance_variable_set なら実行中に決められる
Rails は開発者がどんな名前の変数を作るか知らないので、ハッシュのキーから名前を組み立てて代入している
2つのオブジェクトの中身の移り変わりを図にすると、こうなります。
【コントローラー】 @_params, @user=#<User name: "山田"> (show を実行した直後)
│
│ ② view_assigns:内部用を除いて、名前と値のハッシュにする
▼
{ "user" => #<User name: "山田"> }
│
│ ③ ビュー用のクラス.new(ハッシュ, self)
▼
【ビュー用オブジェクト】 (まだ空っぽ)
│
│ ⑤ initialize の中で
│ assign_controller(self) … @_controller にコントローラーを入れる
│ assign(ハッシュ) … "user" を "@user" にして instance_variable_set
▼
【ビュー用オブジェクト】 @_controller, @user=#<User name: "山田">
│
│ ⑥ テンプレートを実行
▼
<p>山田さん</p>
⑥ テンプレートは「ビュー用オブジェクトのメソッド」になる
ここでいうテンプレートは、app/views/ 配下のファイル全部のことです。
種類
ファイル名の例
アクションのテンプレート
users/show.html.erb
partial(部品のテンプレート)
users/_form.html.erb
レイアウト
layouts/application.html.erb
_ で始まるのは partial だけで、どれも同じ仕組みで動きます。ここでは show.html.erb を例にします。
<p><%= @user.name %>さん</p>
Rails はこのファイルを、次の Ruby のメソッドに変換していました。rails runner で ActionView::Template を作り、compiled_source(action_view/template.rb)を表示させた結果です。実際の出力は1行にまとまっているので、見やすく改行してコメントを足しています。
def _app_views_users_show_html_erb__1476774381686002440_15872(local_assigns, output_buffer, &_)
@virtual_path = "users/show";
@output_buffer.safe_append='<p>'.freeze; # 固定の HTML はそのまま足す
@output_buffer.append=( @user.name ); # <%= @user.name %> は、式の結果を(エスケープして)足す
@output_buffer.safe_append='さん</p>'.freeze;
@output_buffer # 組み立てた HTML を返す
end
実行したコード
rails runner で、テンプレートの中身を直接渡して変換させました。ファイルを作ったり DB を書き換えたりはしないので、どの Rails アプリでも試せます。
bundle exec rails runner '
template = ActionView::Template.new(
"<p><%= @user.name %>さん</p>", # テンプレートの中身
"app/views/users/show.html.erb", # ファイルのパス(実在しなくてよい)
ActionView::Template.registered_template_handler(:erb),
locals: [],
format: :html,
virtual_path: "users/show"
)
puts template.send(:compiled_source) # Ruby のメソッドに変換した結果を表示
'
compiled_source は外から呼べない private メソッドなので、send で呼んでいる
メソッド名の末尾の数字は、実行するたびに変わる
テンプレートは、ファイルのまま実行されるのではなく、1つの def に変換される
メソッド名は、ファイルのパスと番号から自動で作られる。先頭の _ は他のメソッドと名前がぶつからないための接頭辞で、partial かどうかとは関係ない
Haml でも、生成されるコードの中身が違うだけで仕組みは同じ
■ 変換から実行までの3段階
show.html.erb(ただのテキストファイル)
│ 1. 変換:テンプレートエンジン(ERB / Haml)が "def ... end" という Ruby の「文字列」にする
▼
"def _app_views_users_show_html_erb...(local_assigns, output_buffer, &_) ... @user ... end"
│ 2. 定義:module_eval で読み込ませる → ビュー用のクラスで使える本物のメソッドになる
▼
ビュー用のクラスのメソッド _app_views_users_show_html_erb...
│ 3. 実行:ビュー用オブジェクト.public_send(:_app_views_users_show_html_erb__...)
▼
メソッドの中の self = ビュー用オブジェクト
├ @user → self の @user(⑤で assign した値)
└ users_path → self のメソッド(④で include されたヘルパー)
module_eval(文字列) は、文字列のコードをクラスの中に書いたものとして実行する。"def ... end" を渡せば、そのクラスにメソッドが追加される
public_send(:foo) は、メソッド名をシンボルで指定して呼ぶメソッド。obj.public_send(:foo) は obj.foo と同じ
テンプレートのメソッド名は実行中に自動で作られるので、obj._app_views_... のようにコードへ直接書いておけない。そのため、名前を値として受け取れる public_send で呼んでいる
klass = Class.new
klass.module_eval("def greet = 'こんにちは'") # 文字列のコードから greet メソッドを定義する
obj = klass.new
name = :greet # 呼びたいメソッドの名前を、値として持っている
obj.public_send(name) # => "こんにちは"(obj.greet と同じ)
1と2は最初の1回だけ。2回目以降のリクエストは、定義済みのメソッドを呼ぶだけなので速い
テンプレートの @user が読めるのは、Ruby に「メソッドの中の @xxx は、そのメソッドを呼ばれたオブジェクト(self)の変数を指す」というルールがあるから(③の self の話と同じ考え方で、つまり今回はビュー用オブジェクトの @user を指す)
3.4 この仕組みから分かること
書き写されるのは、オブジェクトへの参照でした。中身を丸ごと複製(ディープコピー)しているわけではありませんでした。
Ruby の変数に入っているのは、オブジェクトそのものではなく「そのオブジェクトがどこにあるか」を指す参照です。②⑤の書き写しは、この参照を受け渡しているだけで、オブジェクトを複製する処理(dup など)はどこにもありません。
ref = instance_variable_get(:@user) # ② コントローラーの @user から参照を取り出す
hash["user"] = ref # ハッシュに入れる(複製しない)
instance_variable_set("@user", ref) # ⑤ ビュー用オブジェクトの @user に入れる(複製しない)
そのため、2つの @user は同じオブジェクトを指します。
[コントローラー] @user ──┐
├──→ #<User name: "山田">(メモリ上に1つだけ)
[ビュー用オブジェクト] @user ──┘
現象
理由
ビューで @user = 別のもの と代入し直しても、コントローラー側は変わらない
変数は別々で、ビュー側の矢印だけが別のものを指し直すから
ビューで @user.name = "x" と中身を変えると、コントローラー側から見ても変わる
2つの変数が同じオブジェクトを指しているから
partial でも @user が使える
partial も同じビュー用オブジェクトのメソッドとして実行されるから
ビューでは params も使える
ビュー用オブジェクトは params のデータを持たず、⑤で @_controller に入れたコントローラーの params を呼んでいるから(delegate による委譲)
@usr と書き間違えてもエラーにならず、nil になる
Ruby では、作っていないインスタンス変数を読むと nil が返るから
4. テスト
いろいろなテストコードを読むうちに、FactoryBot には create だけでなく build / build_stubbed もあり、データの作り方が3つあると知り、なぜ使い分ける必要があるのか疑問に思ったのがきっかけで調べました。
4.1 FactoryBot の create / build / build_stubbed
調べてみると、DB から読み直す必要がないテストなら、create ではなく build_stubbed を使うと速くなると分かりました。テストデータを作る FactoryBot には、オブジェクトの作り方が3種類ありました20。
user = create(:user) # DB に保存する
user = build(:user) # 保存しない(User.new と同じ状態)
user = build_stubbed(:user) # 保存しないが、保存済みのふりをする
DB への保存
id
向いているテスト
速さ
create
する
本物
画面表示や検索など、DB から読み直す処理
遅い
build
しない
nil
保存前のバリデーションなど
速い
build_stubbed
しない
偽の id
DB を使わないメソッド
最速
DB に触らない分だけ速いので、create が要らない場面では build / build_stubbed を選ぶ
関連も、既定では親と同じ作り方になる(build なら関連も保存されない)
build_stubbed は、DB に保存しないまま、保存済みのように振る舞います21。
user = build_stubbed(:user)
user.id # => 1001(偽の id)
user.persisted? # => true(「DB に保存済みか」を聞くと、保存済みと答える)
user.save # => RuntimeError(本当に DB に触ろうとするとエラー)
persisted? の persist は「残り続ける」という意味。「DB に保存されて、プログラムが終わっても残る状態になったか(永続化されたか)」を聞くメソッド
build_stubbed はこれに true を返すので、user_path(user) のように保存済みを前提にした URL の生成なども、DB なしでテストできる
おわりに
最後まで読んでいただき、ありがとうございました。
脚注
MDN Web Docs「<script>: スクリプト要素」 ↩︎
WHATWG「HTML Living Standard」 ↩︎
W3C「JSON-LD 1.1」 ↩︎
IETF「RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format」 ↩︎
IANA「Media Types」 ↩︎
Ecma International「ECMAScript Language Specification(ECMA-262)」 ↩︎
schema.org「About Schema.org」 ↩︎
IETF「RFC 6839: Additional Media Type Structured Syntax Suffixes」 ↩︎
Jason Miller「Islands Architecture」 ↩︎
React(旧ドキュメント)「ReactDOM」 ↩︎ ↩︎
React「hydrateRoot」「createRoot」 ↩︎
MDN Web Docs「EventTarget: addEventListener() メソッド」 ↩︎
facebook/react v17.0.2「ReactDOMComponentTree.js」「ReactDOMHostConfig.js」 ↩︎
Rails ソースコード(8-1-stable)「actionpack/lib/abstract_controller/rendering.rb」「actionview/lib/action_view/rendering.rb」「actionview/lib/action_view/base.rb」「actionpack/lib/action_controller/metal.rb」「actionview/lib/action_view/helpers/controller_helper.rb」「actionview/lib/action_view/template.rb」 ↩︎
Ruby リファレンスマニュアル「変数と定数(インスタンス変数)」 ↩︎
Ruby リファレンスマニュアル「Object#instance_variables」「Object#instance_variable_get」 ↩︎
Ruby リファレンスマニュアル「変数と定数(擬似変数)」 ↩︎
Ruby リファレンスマニュアル「Class.new」 ↩︎
Ruby リファレンスマニュアル「Object#instance_variable_set」 ↩︎
factory_bot「Build strategies」 ↩︎
thoughtbot/factory_bot v6.5.5「lib/factory_bot/strategy/stub.rb」 ↩︎
Mentions
新卒エンジニアが書き溜めた技術メモ 4選 はじめに こんにちは、namonaki です。新卒で入社して半年、分からなかったことを個人用のメモに書き留めてきました。その中からいくつか選りすぐって、改めて調べつつ記事にしてみました。 ■ 対象 Rails や React でコードは書けるが、「なぜこう書くと動くのか」はまだ説明できない人 普段なんとなく使っている仕組みの裏側を、ソースコードや DevTools で確かめてみたい人 ■ 扱うこと ブラウザ・Web標準 React と Islands Architecture Rails テスト 各章は独立しているので、気になるところから読んでください。参考にした資料は、本文中の番号から末尾の脚注に飛べます。 ! 前提のバージョン(2026年10月時点) Ruby 4.0 / Rails 8.1 React 17 / Node.js 24 factory_bot 6.5 1. ブラウザ・Web標準 JSON-LD(検索エンジン向けの構造化データ)について調べていたとき、JS を書くわけでもないのに、なぜ <script> タグを使うのか疑問に思ったのがきっかけです。 1.1 <script> の属性なし / async / defer の違い 調べてみると、HTML の要素を探して操作する JS には、defer を付けるのが基本でした。ダウンロードは早く始めつつ、実行は HTML の解析が終わるまで待ってくれるからです。 <script src="a.js"></script> <!-- ① 属性なし --> <script src="a.js" async></script> <!-- ② async --> <script src="a.js" defer></script> <!-- ③ defer --> HTMLの解析を止めるか 実行タイミング 実行順 属性なし 止める 読み込んだらすぐ 書いた順 async ダウンロード中は止めない ダウンロードが終わりしだい 保証なし defer 止めない HTMLの解析が終わった後 書いた順 defer は「ダウンロードは早く、実行は最後」という良いとこ取り1 念のため、コード側でも DOMContentLoaded を待つと二重の保険になる querySelectorAll のように要素が見つからなくてもエラーにならない書き方では、何も起きないだけなので気づきにくい。defer を付けておけば、この「要素がまだ無い」事故を防げる 1.2 Webのルールは誰が決めているのか 調べてみると、Web のルールは1つの団体が決めているわけではなく、部分ごとに別の団体が決めていました。 パンくずリストを構造化データで書いたときの例で見てみます。 <script type="application/ld+json"> { "@context": "schema.org", "@type": "BreadcrumbList", "itemListElement": [ ... ] } </script> この数行でも、部分ごとにルールを決めている団体が違いました。 部分 ルール 決めている団体 <script> タグ HTML WHATWG application/ld+json MIMEタイプの仕組み / 登録 IETF / IANA { ... } の書き方 JSON IETF・Ecma International @context などの書き方 JSON-LD W3C BreadcrumbList という単語 語彙 schema.org WHATWG:HTML と DOM。ブラウザを作る企業が中心。2019年に W3C と合意し、HTML は「HTML Living Standard」に一本化。版の区切りはなく更新され続ける2 W3C:CSS、アクセシビリティ(WAI-ARIA)、JSON-LD など3 IETF:HTTP や JSON など、通信とデータ形式の基礎(RFC)4 IANA:MIMEタイプの一覧、ドメイン名、IPアドレスの登録・管理5 Ecma International:JavaScript の文法(ECMAScript)6 schema.org:Google・Microsoft・Yahoo! などによる共同プロジェクト。標準化団体ではなく、構造化データ用の単語帳を作っている7 ■ 補足 +json は「中身は JSON 形式」という意味の接尾辞8 <script> の type に JS 以外を指定すると、ブラウザは実行せずデータとして置いておく。検索エンジンはこれを読む 2. React と Islands Architecture Wikipedia のような辞書的なページは、全体を SPA にするとかえって重くなるので、一部分だけを React で動くようにすることがあると思います。こうした構成にも何か名前が付いているはずだと思い、調べてみたのがきっかけです。 2.1 「一部だけReact」すなわち Islands Architecture …ではなかった 最初は、一部だけ React を使う構成ならすべて Islands Architecture だと思っていましたが、サーバーが空の div を返し、ブラウザで React が中身を一から作る構成は、Islands Architecture ではありませんでした。Islands では島の HTML もサーバーが作り、ブラウザはそれを hydrate するからです。 ■ Islands Architecture の条件9 島(インタラクティブな部分)の初期 HTML もサーバーが描画する ブラウザはその HTML に hydrate(既存の要素を再利用して、イベントや状態を結び付ける)する 島の外には JS を送らない ■ 空の div にマウントする構成(React 17 の例) // サーバーが返すのは空の div だけ。ブラウザ側で中身を一から作る(render) document.querySelectorAll('.search-widget').forEach((el) => { // サーバーからの値は data-params 属性で受け取る ReactDOM.render(<SearchWidget params={JSON.parse(el.dataset.params)} />, el); }); サーバーが返す HTML と、JS 実行後の HTML を並べるとこうなります。 【render(空の div にマウントする構成)】 サーバーが返したHTML JS 実行後 <div class="search-widget"> <div class="search-widget"> (空っぽ) → <form>...</form> ← JS が一から作る </div> </div> 【hydrate(Islands Architecture)】 サーバーが返したHTML JS 実行後 <div> <div> <form>...</form> → <form>...</form> ← 要素は作り直さず、イベントや状態を結び付ける </div> </div> 見分け方:JS を切ったときに島の中身が見えるか React 17 なら ReactDOM.render が render、ReactDOM.hydrate が hydrate10 React 18 以降は、createRoot().render() と hydrateRoot() に置き換わっている11 「島の外には JS を送らない」点だけは Islands と共通 2.2 hydrate で起きていること 調べてみると、hydrate は、サーバーが作った要素をそのまま使い、クリック時の処理や状態を後から結び付ける処理でした。 サーバーから届いた HTML は「見た目」だけで、ボタンを押しても何も起きません。素の JS で、後から動くようにするとこうなります12。 <button id="like">いいね 0</button> // 既にあるボタンを探して、処理を後付けする(要素は作り直さない) const button = document.getElementById('like'); let count = 0; button.addEventListener('click', () => { count += 1; button.textContent = いいね ${count}; }); これが「既存の HTML にイベントを結び付ける」いちばん素朴な形 React の hydrate も考え方は同じで、中では次のことをしている サーバーが返したHTML(見た目だけ。押しても動かない) <form><input><button>検索</button></form> ↓ ReactDOM.hydrate(<SearchForm />, el) ① コンポーネントを実行して、「本来こうなるはず」の設計図をメモリ上に作る ② 設計図と既存の要素を先頭から照らし合わせ、一致した要素を「この部品の担当」として紐付ける ③ onClick などのイベント処理を受け付けられるようにする ④ useState の状態や useEffect を動かし始める ↓ 見た目は同じまま、押せば動く状態になる 設計図と HTML が食い違うと、React 17 は開発モードで警告を出す。テキストの違いは書き換えて合わせるが、属性の違いは直らないことがあるので、食い違いはバグとして直す10:1 ②で作る「設計図 ↔ 実際の要素」の対応表は、正確には Fiber というオブジェクトで、React はこれを使って書き換える要素や、押されたボタンの担当を見つけている13 ■ ブラウザで確かめる Fiber が実際の DOM 要素に付いている様子は、React で作られたサイトなら DevTools で見られます。ここでは React 公式サイトの react.dev を使います。 Chrome で react.dev を開き、DevTools を開く(Mac なら ⌘ + Option + I) DevTools の左上にある矢印のアイコン(要素を選ぶボタン)を押してから、「Learn React」ボタンをクリックする。矢印を押さずにクリックすると、リンク先へページが移動してしまう Console タブで $0(Elements タブで選択中の要素)を実行し、<a … aria-label="Learn React" …>Learn React</a> が表示されることを確かめる 次を実行する Object.keys($0) // => ['reactFiber$9s34k04vvrf', 'reactProps$9s34k04vvrf'] // (2026年10月時点。$ の後ろの文字列はページを開くたびに変わる) $0[Object.keys($0).find(k => k.startsWith('__reactProps$'))] // => {className: 'w-full sm:w-auto …', aria-label: 'Learn React', target: '_self', ref: ƒ, onClick: ƒ, …} reactFiber$… が Fiber への参照、reactProps$… が props。props の中に onClick の関数が入っているのが見える react.dev は React 18 以降で作られているが、キーの名前は React 17 と同じ これらは React 内部の仕組みで、公開された機能ではない。観察だけにとどめ、自分のコードから読み書きしない 2.3 render と hydrate の違いのまとめ 違うのは「要素を一から作るか、サーバーが作ったものを再利用するか」だけでした。Fiber との紐付け(2.2 の②)、イベントの受け付け(2.2 の③)、状態や副作用の開始(2.2 の④)は、どちらも同じです。 ここに「ページの一部か全体か」という範囲の軸を加えると、よく聞く構成は次の4つに整理できます。 render(要素を一から作る) hydrate(サーバーが作った要素を再利用) ページの一部 空の div にマウントする構成 Islands Architecture ページ全体 SPA サーバーで描画して hydrate(Next.js など) どのマスでも、マウントした要素より内側は Fiber と紐付く。違うのは「どこまでを React に任せるか」と「要素を作るか、再利用するか」だけ SPA もページ全体が Fiber と紐付くが、サーバーが返すのは空の div なので、hydrate ではなく render Islands との違いは hydrate する範囲で、Islands は島ごとに別々の React として hydrate し、島の外は React と無関係の HTML のまま。Next.js はページ全体(document)を1つの React として hydrate する 3. Rails Rails のチュートリアル系のサイトや動画で「コントローラーで @ を付けた変数は、ビューでも @ を付けて参照できる」と聞いたものの、裏で何が起きているのか分からず、Rails を学び始めてすぐに疑問に思ったのがきっかけで調べました。 3.1 コントローラーのインスタンス変数がビューに渡る仕組み コントローラーのインスタンス変数がビューで使えるのは、Rails がビュー用のオブジェクトに書き写しているからでした。さらに、テンプレートはそのオブジェクトのメソッドに変換されて実行されていました(Rails 8.1 のソースより)14。 class UsersController < ApplicationController def show @user = User.find(params[:id]) # @ 付きならビューで使える posts = @user.posts # @ なしはこのメソッドの中だけ end end 3.2 そもそも、なぜ書き写す必要があるのか インスタンス変数は、それを持っているオブジェクトの中からしか見えません15。 @user を持っているのは、UsersController のインスタンス テンプレートを実行するのは、それとは別のビュー用オブジェクト 別々のオブジェクトなので、ビューで @user を使うには、ビュー用オブジェクト側にも同じインスタンス変数が必要 そこで Rails が、コントローラーの変数を1つずつビュー用オブジェクトに書き写している 3.3 全体の流れ ブラウザから GET /users/1(ユーザーの詳細画面)のリクエストが届く ↓ ルーティング(resources :users)が users#show に振り分け、params[:id] = "1" にする ① コントローラーを new して、show アクションを実行する ↓ @user = User.find(1) ↓ render(書かなくても自動で呼ばれる) ② 集める view_assigns … コントローラーのインスタンス変数をハッシュにする ↓ { "user" => #<User id: 1> } ③ 作る view_context … ビュー用オブジェクトを new する(ハッシュとコントローラー自身を渡す) (new するクラスの正体は ④ で補足) ↓ ⑤ 書き込む assign … ハッシュの中身を、ビュー用オブジェクトのインスタンス変数にする ↓ ⑥ テンプレート実行 テンプレートを「ビュー用オブジェクトのメソッド」に変換して呼ぶ → テンプレートの @user は、ビュー用オブジェクトの @user 以下、順に見ていきます。 ① コントローラーはリクエストごとに new される ソースを読むと、Rails はリクエストが届くたびにコントローラーを new していました。ルーティングが users#show と判断すると、UsersController.dispatch というクラスメソッドが呼ばれ、その中で new されます(actionpack action_controller/metal.rb)。 def self.dispatch(name, req, res) ... new.dispatch(name, req, res) # UsersController.new して、そのインスタンスで処理する end Web サーバー(Puma など)がリクエストを受け取る ↓ Rails.application.call(env) Rack ミドルウェア(Rails の中で順番に呼ばれる前処理の部品。セッション・Cookie など) ↓ ActionDispatch::Routing::RouteSet#call … ルーティング ↓ Journey::Router#serve … GET /users/1 に合うルートを探す ↓ RouteSet::Dispatcher#serve … "users" → UsersController クラスに変換 UsersController.dispatch(:show, req, res) … Metal に定義されたクラスメソッド ↓ new.dispatch(:show, req, res) … self.new = UsersController.new UsersController のインスタンス#dispatch … 以降はこのインスタンスの上で動く Rack は、Ruby の Web サーバー(Puma など)とアプリ(Rails など)をつなぐ共通ルール。「call(env) を受け取って [ステータス, ヘッダー, 本文] を返す」という約束だけを決めている env は、リクエストの情報(URL、HTTP メソッド、ヘッダーなど)を詰めたハッシュ Puma・Rack ミドルウェア・Rails は別々のサーバーではなく、同じ1つのプロセスの中で動いている リクエストごとに別のインスタンスなので、別のユーザーのリクエストと @user が混ざることはない ② 集める:view_assigns actionpack abstract_controller/rendering.rb のメソッドです。 show の実行後に呼ばれたときの値を、コメントで書き込んでいます。 def view_assigns variables = instance_variables - _protected_ivars instance_variables => [:@_request, :@_response, :@_params, :@_routes, ..., :@user] ← Rails 内部用も混ざっている 配列の引き算で内部用を外す => [:@user] variables.each_with_object({}) do |name, hash| # name = :@user、hash は最初は空の {} hash[name.slice(1, name.length)] = instance_variable_get(name) # "@user" の2文字目以降 "user" をキーに、@user の値を詰める end => { "user" => #<User id: 1> } end 使っているのは、Ruby の標準メソッド2つだけでした16。 メソッド 何をするか instance_variables そのオブジェクトが持つインスタンス変数の名前を、シンボルの配列で返す instance_variable_get(名前) 名前を指定して、その変数の値を取り出す instance_variables はレシーバーを省略しているので、self.instance_variables と同じ。self は①で new されたコントローラーのインスタンス 除外する _protected_ivars は、@_params や @_request など、Rails 内部用の変数を並べた一覧 each_with_object({}) は、空のハッシュを用意して1件ずつ詰め、最後にそのハッシュを返すメソッド つまり、コントローラーの中身を一覧して、@ 付きの変数を「名前 → 値」のハッシュに変換しているだけでした。 ③ 作る:view_context actionview action_view/rendering.rb のメソッドです。 def view_context view_context_class.new(lookup_context, view_assigns, self) end 第1引数:どのテンプレートファイルを使うかを決める条件(探すフォルダ、言語、html / json などの形式) 第2引数:②で作ったハッシュ 第3引数:コントローラー自身(self) ■ self はなぜコントローラーなのか view_context は actionview の ActionView::Rendering モジュールに書かれていますが、それでも self はコントローラーでした。実行時の self は、「そのメソッドがどのファイルやモジュールに書かれているか」ではなく、「ドットの左側にいたオブジェクト(レシーバー)」で決まるからです17。 module Greeting def who = self end class UsersController include Greeting # Greeting のメソッドを、UsersController のインスタンスから呼べるようにする end c = UsersController.new c.who.equal?(c) # => true(レシーバーの c が self になる) UsersController.ancestors.first(2) # => [UsersController, Greeting] include は、メソッドをコピーするのではなく、メソッドを探す経路(ancestors)にモジュールを追加する c.who と呼ぶと、Ruby は UsersController → Greeting の順に who を探し、見つけたメソッドを c の上で実行する Rails の view_context も同じで、コントローラーに include されたモジュールのメソッドです。①で new したコントローラーのインスタンスから、レシーバーを省略して(self.〜 として)呼ばれ続けるので、self はずっとそのインスタンスのままです。そのため第3引数の self には、リクエストを処理しているコントローラー自身が渡されます。 ④ ビュー用のクラスは「無名クラス」 new しているクラス view_context_class は、ActionView::Base を継承した無名クラス(名前のないクラス)でした。 ■ 無名クラスとは Ruby では Class.new でクラスを作れます18。それを定数(大文字で始まる名前)に入れると名前が付き、入れなければ無名クラスになります。 Dog = Class.new { def hi = "hi" } dog = Class.new { def hi = "hi" } Dog.name # => "Dog" dog.name # => nil(中身は同じでも、ローカル変数に入れただけでは名前が付かない) dog.new.hi # => "hi"(変数 dog を通して、普通のクラスと同じように使える) 違うのは名前の有無だけで、継承・include・メソッド定義・new は普通のクラスと同じようにできる 無名クラスでも、使い回すには変数などに入れておく必要がある。ただし変数はクラスを入れておく箱にすぎず、変数名はクラスの名前にならない。箱は何個あってもよく入れ替えもできるので、クラス側には「自分がどの名前で呼ばれているか」が分からない。名前として記録されるのは、最初に入れた定数だけ Rails はビュー用の無名クラスを、コントローラーのクラス側に @view_context_class として入れてキャッシュし、③の view_context_class.new(...) で取り出して new している ■ ヘルパーが呼べる理由 Rails は、この無名クラスにルーティングのヘルパーとアプリのヘルパーを include しています(action_view/rendering.rb)。 Class.new(klass) do include routes.url_helpers(supports_path) # users_path など include helpers # app/helpers/ のメソッド end ビュー用オブジェクトはこのクラスのインスタンスなので、ビューで users_path や自作のヘルパーが呼べる ancestors をたどると、無名クラス → ヘルパーのモジュール → … → ActionView::Base の順にメソッドを探している ⑤ 書き込む:initialize と assign ③の new を呼ぶと、Ruby は次の2つを順に行います。 メモリ上に空のオブジェクトを作る そのオブジェクトの initialize を、new と同じ引数で自動的に呼ぶ action_view/rendering.rb view_context_class.new(lookup_context, view_assigns, self) ↓ ↓ ↓ action_view/base.rb ↓ ↓ ↓ def initialize(lookup_context, assigns, controller) @lookup_context = lookup_context ... assign_controller(controller) # @_controller にコントローラーを入れる ... assign(assigns) # 最後に、②のハッシュを書き込む end def assign(new_assigns) @_assigns = new_assigns new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end assign は、②の view_assigns のちょうど逆の操作でした。 ② コントローラー側 ⑤ ビュー側 使うメソッド instance_variable_get instance_variable_set 名前の変換 :@user → "user" "user" → "@user" 結果 変数 → ハッシュ ハッシュ → 変数 instance_variable_set("@user", value) は、@user = value を、変数名を文字列で指定して書いたもの19 @user = value と書くと変数名はコードを書いた時点で決まるが、instance_variable_set なら実行中に決められる Rails は開発者がどんな名前の変数を作るか知らないので、ハッシュのキーから名前を組み立てて代入している 2つのオブジェクトの中身の移り変わりを図にすると、こうなります。 【コントローラー】 @_params, @user=#<User name: "山田"> (show を実行した直後) │ │ ② view_assigns:内部用を除いて、名前と値のハッシュにする ▼ { "user" => #<User name: "山田"> } │ │ ③ ビュー用のクラス.new(ハッシュ, self) ▼ 【ビュー用オブジェクト】 (まだ空っぽ) │ │ ⑤ initialize の中で │ assign_controller(self) … @_controller にコントローラーを入れる │ assign(ハッシュ) … "user" を "@user" にして instance_variable_set ▼ 【ビュー用オブジェクト】 @_controller, @user=#<User name: "山田"> │ │ ⑥ テンプレートを実行 ▼ <p>山田さん</p> ⑥ テンプレートは「ビュー用オブジェクトのメソッド」になる ここでいうテンプレートは、app/views/ 配下のファイル全部のことです。 種類 ファイル名の例 アクションのテンプレート users/show.html.erb partial(部品のテンプレート) users/_form.html.erb レイアウト layouts/application.html.erb _ で始まるのは partial だけで、どれも同じ仕組みで動きます。ここでは show.html.erb を例にします。 <p><%= @user.name %>さん</p> Rails はこのファイルを、次の Ruby のメソッドに変換していました。rails runner で ActionView::Template を作り、compiled_source(action_view/template.rb)を表示させた結果です。実際の出力は1行にまとまっているので、見やすく改行してコメントを足しています。 def _app_views_users_show_html_erb__1476774381686002440_15872(local_assigns, output_buffer, &_) @virtual_path = "users/show"; @output_buffer.safe_append='<p>'.freeze; # 固定の HTML はそのまま足す @output_buffer.append=( @user.name ); # <%= @user.name %> は、式の結果を(エスケープして)足す @output_buffer.safe_append='さん</p>'.freeze; @output_buffer # 組み立てた HTML を返す end 実行したコード rails runner で、テンプレートの中身を直接渡して変換させました。ファイルを作ったり DB を書き換えたりはしないので、どの Rails アプリでも試せます。 bundle exec rails runner ' template = ActionView::Template.new( "<p><%= @user.name %>さん</p>", # テンプレートの中身 "app/views/users/show.html.erb", # ファイルのパス(実在しなくてよい) ActionView::Template.registered_template_handler(:erb), locals: [], format: :html, virtual_path: "users/show" ) puts template.send(:compiled_source) # Ruby のメソッドに変換した結果を表示 ' compiled_source は外から呼べない private メソッドなので、send で呼んでいる メソッド名の末尾の数字は、実行するたびに変わる テンプレートは、ファイルのまま実行されるのではなく、1つの def に変換される メソッド名は、ファイルのパスと番号から自動で作られる。先頭の _ は他のメソッドと名前がぶつからないための接頭辞で、partial かどうかとは関係ない Haml でも、生成されるコードの中身が違うだけで仕組みは同じ ■ 変換から実行までの3段階 show.html.erb(ただのテキストファイル) │ 1. 変換:テンプレートエンジン(ERB / Haml)が "def ... end" という Ruby の「文字列」にする ▼ "def _app_views_users_show_html_erb...(local_assigns, output_buffer, &_) ... @user ... end" │ 2. 定義:module_eval で読み込ませる → ビュー用のクラスで使える本物のメソッドになる ▼ ビュー用のクラスのメソッド _app_views_users_show_html_erb... │ 3. 実行:ビュー用オブジェクト.public_send(:_app_views_users_show_html_erb__...) ▼ メソッドの中の self = ビュー用オブジェクト ├ @user → self の @user(⑤で assign した値) └ users_path → self のメソッド(④で include されたヘルパー) module_eval(文字列) は、文字列のコードをクラスの中に書いたものとして実行する。"def ... end" を渡せば、そのクラスにメソッドが追加される public_send(:foo) は、メソッド名をシンボルで指定して呼ぶメソッド。obj.public_send(:foo) は obj.foo と同じ テンプレートのメソッド名は実行中に自動で作られるので、obj._app_views_... のようにコードへ直接書いておけない。そのため、名前を値として受け取れる public_send で呼んでいる klass = Class.new klass.module_eval("def greet = 'こんにちは'") # 文字列のコードから greet メソッドを定義する obj = klass.new name = :greet # 呼びたいメソッドの名前を、値として持っている obj.public_send(name) # => "こんにちは"(obj.greet と同じ) 1と2は最初の1回だけ。2回目以降のリクエストは、定義済みのメソッドを呼ぶだけなので速い テンプレートの @user が読めるのは、Ruby に「メソッドの中の @xxx は、そのメソッドを呼ばれたオブジェクト(self)の変数を指す」というルールがあるから(③の self の話と同じ考え方で、つまり今回はビュー用オブジェクトの @user を指す) 3.4 この仕組みから分かること 書き写されるのは、オブジェクトへの参照でした。中身を丸ごと複製(ディープコピー)しているわけではありませんでした。 Ruby の変数に入っているのは、オブジェクトそのものではなく「そのオブジェクトがどこにあるか」を指す参照です。②⑤の書き写しは、この参照を受け渡しているだけで、オブジェクトを複製する処理(dup など)はどこにもありません。 ref = instance_variable_get(:@user) # ② コントローラーの @user から参照を取り出す hash["user"] = ref # ハッシュに入れる(複製しない) instance_variable_set("@user", ref) # ⑤ ビュー用オブジェクトの @user に入れる(複製しない) そのため、2つの @user は同じオブジェクトを指します。 [コントローラー] @user ──┐ ├──→ #<User name: "山田">(メモリ上に1つだけ) [ビュー用オブジェクト] @user ──┘ 現象 理由 ビューで @user = 別のもの と代入し直しても、コントローラー側は変わらない 変数は別々で、ビュー側の矢印だけが別のものを指し直すから ビューで @user.name = "x" と中身を変えると、コントローラー側から見ても変わる 2つの変数が同じオブジェクトを指しているから partial でも @user が使える partial も同じビュー用オブジェクトのメソッドとして実行されるから ビューでは params も使える ビュー用オブジェクトは params のデータを持たず、⑤で @_controller に入れたコントローラーの params を呼んでいるから(delegate による委譲) @usr と書き間違えてもエラーにならず、nil になる Ruby では、作っていないインスタンス変数を読むと nil が返るから 4. テスト いろいろなテストコードを読むうちに、FactoryBot には create だけでなく build / build_stubbed もあり、データの作り方が3つあると知り、なぜ使い分ける必要があるのか疑問に思ったのがきっかけで調べました。 4.1 FactoryBot の create / build / build_stubbed 調べてみると、DB から読み直す必要がないテストなら、create ではなく build_stubbed を使うと速くなると分かりました。テストデータを作る FactoryBot には、オブジェクトの作り方が3種類ありました20。 user = create(:user) # DB に保存する user = build(:user) # 保存しない(User.new と同じ状態) user = build_stubbed(:user) # 保存しないが、保存済みのふりをする DB への保存 id 向いているテスト 速さ create する 本物 画面表示や検索など、DB から読み直す処理 遅い build しない nil 保存前のバリデーションなど 速い build_stubbed しない 偽の id DB を使わないメソッド 最速 DB に触らない分だけ速いので、create が要らない場面では build / build_stubbed を選ぶ 関連も、既定では親と同じ作り方になる(build なら関連も保存されない) build_stubbed は、DB に保存しないまま、保存済みのように振る舞います21。 user = build_stubbed(:user) user.id # => 1001(偽の id) user.persisted? # => true(「DB に保存済みか」を聞くと、保存済みと答える) user.save # => RuntimeError(本当に DB に触ろうとするとエラー) persisted? の persist は「残り続ける」という意味。「DB に保存されて、プログラムが終わっても残る状態になったか(永続化されたか)」を聞くメソッド build_stubbed はこれに true を返すので、user_path(user) のように保存済みを前提にした URL の生成なども、DB なしでテストできる おわりに 最後まで読んでいただき、ありがとうございました。 脚注 MDN Web Docs「<script>: スクリプト要素」 ↩︎ WHATWG「HTML Living Standard」 ↩︎ W3C「JSON-LD 1.1」 ↩︎ IETF「RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format」 ↩︎ IANA「Media Types」 ↩︎ Ecma International「ECMAScript Language Specification(ECMA-262)」 ↩︎ schema.org「About Schema.org」 ↩︎ IETF「RFC 6839: Additional Media Type Structured Syntax Suffixes」 ↩︎ Jason Miller「Islands Architecture」 ↩︎ React(旧ドキュメント)「ReactDOM」 ↩︎ ↩︎ React「hydrateRoot」「createRoot」 ↩︎ MDN Web Docs「EventTarget: addEventListener() メソッド」 ↩︎ facebook/react v17.0.2「ReactDOMComponentTree.js」「ReactDOMHostConfig.js」 ↩︎ Rails ソースコード(8-1-stable)「actionpack/lib/abstract_controller/rendering.rb」「actionview/lib/action_view/rendering.rb」「actionview/lib/action_view/base.rb」「actionpack/lib/action_controller/metal.rb」「actionview/lib/action_view/helpers/controller_helper.rb」「actionview/lib/action_view/template.rb」 ↩︎ Ruby リファレンスマニュアル「変数と定数(インスタンス変数)」 ↩︎ Ruby リファレンスマニュアル「Object#instance_variables」「Object#instance_variable_get」 ↩︎ Ruby リファレンスマニュアル「変数と定数(擬似変数)」 ↩︎ Ruby リファレンスマニュアル「Class.new」 ↩︎ Ruby リファレンスマニュアル「Object#instance_variable_set」 ↩︎ factory_bot「Build strategies」 ↩︎ thoughtbot/factory_bot v6.5.5「lib/factory_bot/strategy/stub.rb」 ↩︎