Zshでコマンド履歴を快適に検索する方法(fzf導入ガイド)
問題点
ターミナルでCtrl+Rを使ってコマンド履歴を検索するとき、一つずつしか表示されず不便に感じたことはありませんか?特に似たようなコマンドを何度も実行している場合、目的のコマンドを見つけるのに時間がかかってしまいます。
解決策:fzfの導入
fzf(fuzzy finder)を使えば、コマンド履歴を一覧表示し、リアルタイムで絞り込み検索ができるようになります。
fzfのインストール
Homebrewを使う場合(macOS)
brew install fzf
aptを使う場合(Ubuntu/Debian)
sudo apt install fzf
Zshへの設定
.zshrcファイルを編集します。
vim ~/.zshrc
ファイルの末尾に以下を追加:
# fzfの設定 eval "$(fzf --zsh)" export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
設定を保存したら、反映させます:
source ~/.zshrc
使い方
設定後は、これまでと同じようにCtrl+Rを押すだけです。
すると、過去のコマンド履歴が一覧表示され: - 文字を入力するとリアルタイムで絞り込み - ↑↓キーで選択 - Enterキーで実行
非常に直感的で快適な操作が可能になります。
オプション設定のカスタマイズ
より見やすくしたい場合は、オプションを調整できます:
# プレビューウィンドウ付き export FZF_DEFAULT_OPTS='--height 50% --reverse --border --preview-window=:hidden' # 色をカスタマイズ export FZF_DEFAULT_OPTS='--color=fg:#f8f8f2,bg:#282a36,hl:#bd93f9'
トラブルシューティング
Ctrl+Rが動作しない場合
キーバインドが設定されているか確認:
bindkey | grep fzf
"^R" fzf-history-widgetのような行が表示されればOKです。
表示されない場合は、.zshrcの設定を見直し、以下の形式になっているか確認してください:
eval "$(fzf --zsh)"
注意: source ~/.fzf.zshやsource <(fzf --zsh)ではなく、eval "$(fzf --zsh)"の形式を使用してください。
セキュリティに関する補足
fzfは広く使われているOSSで、GitHub上で6万以上のスターを獲得している信頼性の高いツールです。
ただし、履歴に機密情報を残さないよう、以下の設定も併せて行うことをおすすめします:
# パスワードやトークンを履歴に残さない export HISTCONTROL=ignorespace export HISTIGNORE="*password*:*token*:*secret*" # 履歴ファイルのパーミッション設定 chmod 600 ~/.zsh_history
機密情報を扱うコマンドを実行する際は、コマンドの先頭にスペースを入れると履歴に残りません。
まとめ
fzfを導入することで、コマンド履歴の検索が劇的に快適になります。設定も簡単で、一度使うと手放せなくなるツールです。ぜひ試してみてください。
参考リンク - fzf GitHub リポジトリ - fzf 公式ドキュメント
Rollbar × Devin × GitHub Actions × coderabbitでエラー調査を自動化してみている話

きっかけ
プロダクションでエラーが発生するたびに「Rollbarで確認して、スタックトレース見て、過去に似たエラーないか調べて...」という作業を毎回やっていて、正直面倒だなと思っていました。
エラー対応の最初の30分って、だいたい同じような調査をしているんですよね。「これ自動化できないかな?」と思ったのが始まりです。
作ろうとしているもの
理想は、エラーが起きたら勝手に初期調査が済んでいる状態です。
Rollbarでエラー検知 → GitHub Issueに自動記録 → Devin AIが分析 → CodeRabbitがレビュー → 開発者は調査済み状態から作業開始
要するに、朝起きて「昨日の夜にエラーが発生していますが、すでに原因と修正方針は調査済みです」みたいな状況を作りたいんです。
現在の進捗
完了したもの
実装中のもの
- CodeRabbitとの連携
まだ検討中のもの
- Notionの過去事例検索をRAGに置き換え: 今はNotionの読み取り専用APIを使ってるけど、もっと賢い検索ができるようにしたい
- APIキー管理: GitHub Secretsで管理してるけど、これで十分セキュアなのか検討中
工夫している点
コスト管理
GitHub ActionsとDevinのACU(計算ユニット)の料金が心配だったので、10分でタイムアウトする設定にしています。これで予算オーバーを防げるし、調査が長引きすぎるのも防げて一石二鳥です。
エラー分類の自動化
エラーの種類(Database Error、Timeout Error、Method Errorなど)と緊急度(Critical、High、Medium、Low)を自動で判定するようにしました。これで「どのエラーから対応するか」の優先順位付けも自動化できます。
段階的な実装
いきなり完璧なものを作ろうとせず、「まず動くものを作って、徐々に改善する」方針で進めています。
実際に使ってみての感想
まだ完全ではないですが、すでに効果を感じています。
良い点: - エラーが起きても「とりあえず5分で初期分析は終わってる」という安心感 - 調査の品質が安定する(人による差がなくなる) - 新人でも効率的にエラー対応できる
苦労している点: - エラーパターンが予想以上に多様で、完璧な分類は難しい - AIのAPI料金の見積もりが難しい - たまにRollbar以外のIssueも誤検知してしまう
今後の計画
短期(3ヶ月): - 基本機能の安定稼働 - エラー分類精度の向上
中期(6ヶ月): - NotionからRAGベースの検索に移行 - 修正提案の精度向上
長期(1年): - エラー予測機能 - 修正の自動化(提案だけでなく実際に修正まで)
技術選択で迷ったこと
APIキー管理
GitHub Organization Secretsで管理していますが、「これで十分セキュアなのか?」と時々悩みます。AWS Secrets ManagerやVaultも検討しましたが、Private Orgで適切な権限設定をしていれば、今のところGitHub Secretsで問題ないかなと思っています。
知識ベースの進化
現在はNotionの読み取り専用APIで過去事例を検索していますが、「もっと賢い検索ができないか?」と考えています。RAG(Retrieval-Augmented Generation)やベクトル検索を使えば、より文脈を理解した検索ができそうです。
ただ、コストと複雑さのバランスを考えると、まずは今の仕組みで運用してから改善していこうと思っています。
まとめ
完璧ではないですが、「エラー対応の初期調査が自動化されている」という安心感は大きいです。開発効率も確実に向上しています。
何より、「エラーが起きても慌てなくなった」のが一番の収穫かもしれません。
同じような課題を抱えているチームがあれば、まずは小さく始めて徐々に改善していくアプローチがおすすめです。完璧を目指さず、「今よりちょっと良くなる」を積み重ねていけば、気づいたら大きな改善になっていると思います。
追記:devinのところはgithub actions & claude codeでもいいかも
Claude Code GitHub Actions - Anthropic

SOLID原則について(メモ)
アーキテクチャの学習で claudeとやりとりしていて理解しやすいものが出てきたので保存用に記事にしておきます。
SOLID原則を一言で表すと:
「変更に強い設計」
各原則を一言でまとめると:
| 原則 | 一言で |
|---|---|
| SRP | 「一つのことだけやる」 |
| OCP | 「修正せず拡張する」 |
| LSP | 「親の代わりに使える」 |
| ISP | 「必要な機能だけ使う」 |
| DIP | 「具体的なものに頼らない」 |
覚え方
「SOLID = 頑丈」
- S:責任を分ける(Single)
- O:開いて拡張、閉じて保護(Open/Closed)
- L:置き換え可能(Liskov)
- I:必要な分だけ(Interface)
- D:抽象に依存(Dependency)
薬局システムで例えると
# ❌ SOLIDに従わない設計 class PharmacySystem def everything # 全部やる(SRP違反) # 在庫管理も決済も通知も全部ここに... end end # ✅ SOLIDに従った設計 class InventoryService # S: 在庫管理だけ class PaymentProcessor # O: 新しい決済方法を拡張可能 class NotificationSender # L: メールでもSMSでも同じように使える class ReadOnlyRepository # I: 読み取り機能だけ class TransactionService # D: 具象クラスではなく抽象に依存
つまり:「後から変更・追加しやすい、壊れにくい設計」
これがSOLIDの本質です!
薬局同士での医薬品取引システムをSOLID原則に従って設計してみますね!
全体設計
# 1. 単一責任の原則(SRP)- 各クラスは一つの責任のみ # 薬局情報管理 class Pharmacy attr_reader :id, :name, :license_number, :address def initialize(id, name, license_number, address) @id = id @name = name @license_number = license_number @address = address end def valid_license? # ライセンス有効性チェック !@license_number.nil? && @license_number.length > 0 end end # 医薬品情報管理 class Medicine attr_reader :id, :name, :jan_code, :price, :expiry_date def initialize(id, name, jan_code, price, expiry_date) @id = id @name = name @jan_code = jan_code @price = price @expiry_date = expiry_date end def expired? @expiry_date < Date.current end end # 在庫管理 class Inventory attr_reader :pharmacy_id, :medicine_id, :quantity def initialize(pharmacy_id, medicine_id, quantity) @pharmacy_id = pharmacy_id @medicine_id = medicine_id @quantity = quantity end def available?(requested_quantity) @quantity >= requested_quantity end def reduce(quantity) @quantity -= quantity if available?(quantity) end end # 取引管理 class Transaction attr_reader :id, :seller_id, :buyer_id, :medicine_id, :quantity, :total_price, :status def initialize(id, seller_id, buyer_id, medicine_id, quantity, total_price) @id = id @seller_id = seller_id @buyer_id = buyer_id @medicine_id = medicine_id @quantity = quantity @total_price = total_price @status = 'pending' end def complete @status = 'completed' end def cancel @status = 'cancelled' end end
2. オープンクローズドの原則(OCP)- 拡張に開放、修正に閉鎖
# 支払い方法の抽象化 module PaymentProcessor def process_payment(amount, payment_details) raise NotImplementedError end def refund_payment(transaction_id, amount) raise NotImplementedError end end # 具体的な支払い方法 class BankTransferProcessor include PaymentProcessor def process_payment(amount, payment_details) # 銀行振込処理 puts "銀行振込で #{amount}円を処理" { success: true, transaction_id: "BANK_#{Time.current.to_i}" } end def refund_payment(transaction_id, amount) puts "#{transaction_id}の返金処理: #{amount}円" end end class CreditCardProcessor include PaymentProcessor def process_payment(amount, payment_details) # クレジットカード処理 puts "クレジットカードで #{amount}円を処理" { success: true, transaction_id: "CC_#{Time.current.to_i}" } end def refund_payment(transaction_id, amount) puts "#{transaction_id}のクレジット返金: #{amount}円" end end # 新しい支払い方法も既存コードを変更せずに追加可能 class DigitalWalletProcessor include PaymentProcessor def process_payment(amount, payment_details) puts "デジタルウォレットで #{amount}円を処理" { success: true, transaction_id: "DW_#{Time.current.to_i}" } end def refund_payment(transaction_id, amount) puts "#{transaction_id}のデジタルウォレット返金: #{amount}円" end end
3. リスコフの置換原則(LSP)- 子クラスは親クラスと置換可能
# 通知システムの基底クラス class NotificationSender def send_notification(recipient, message) raise NotImplementedError end def can_send? true # デフォルトでは送信可能 end end class EmailNotification < NotificationSender def send_notification(recipient, message) if can_send? puts "Email送信: #{recipient} - #{message}" true else false end end end class SmsNotification < NotificationSender def send_notification(recipient, message) if can_send? puts "SMS送信: #{recipient} - #{message}" true else false end end end # メンテナンス中の通知(送信しないが例外は投げない) class MaintenanceNotification < NotificationSender def send_notification(recipient, message) puts "メンテナンス中のため通知をスキップ: #{message}" false # 失敗だが例外は投げない end def can_send? false end end
4. インターフェース分離の原則(ISP)- 不要なインターフェースに依存しない
# 小さなインターフェースに分割 module Readable def find_by_id(id) raise NotImplementedError end def find_all raise NotImplementedError end end module Writable def save(entity) raise NotImplementedError end def update(entity) raise NotImplementedError end end module Deletable def delete(id) raise NotImplementedError end end # 薬局リポジトリ(読み書き両方必要) class PharmacyRepository include Readable include Writable def find_by_id(id) # データベースから薬局情報を取得 end def find_all # 全薬局情報を取得 end def save(pharmacy) # 薬局情報を保存 end def update(pharmacy) # 薬局情報を更新 end end # レポートサービス(読み取り専用) class ReportService include Readable def initialize(repository) @repository = repository end def generate_pharmacy_list pharmacies = @repository.find_all # レポート生成処理 end # Writableインターフェースは不要なので実装しない end
5. 依存性逆転の原則(DIP)- 抽象に依存、具象に依存しない
# 取引処理サービス(高レベルモジュール) class TransactionService def initialize(payment_processor, notification_sender, transaction_repository) @payment_processor = payment_processor # 抽象に依存 @notification_sender = notification_sender # 抽象に依存 @transaction_repository = transaction_repository # 抽象に依存 end def process_transaction(seller_id, buyer_id, medicine_id, quantity, total_price) # 取引作成 transaction = Transaction.new( Time.current.to_i, seller_id, buyer_id, medicine_id, quantity, total_price ) # 支払い処理 payment_result = @payment_processor.process_payment( total_price, { transaction_id: transaction.id } ) if payment_result[:success] transaction.complete @transaction_repository.save(transaction) # 通知送信 @notification_sender.send_notification( buyer_id, "取引が完了しました。取引ID: #{transaction.id}" ) transaction else transaction.cancel nil end end end # 使用例 class TransactionController < ApplicationController def create # DIP: 具象クラスではなく抽象に依存するよう注入 service = TransactionService.new( BankTransferProcessor.new, # 支払い方法は変更可能 EmailNotification.new, # 通知方法は変更可能 TransactionRepository.new # データ保存方法は変更可能 ) transaction = service.process_transaction( params[:seller_id], params[:buyer_id], params[:medicine_id], params[:quantity], params[:total_price] ) if transaction render json: { status: 'success', transaction_id: transaction.id } else render json: { status: 'error', message: '取引処理に失敗しました' } end end end
まとめ
SOLID原則の効果:
- SRP: 薬局管理、在庫管理、取引管理を分離 → 変更時の影響範囲が限定的
- OCP: 新しい支払い方法や通知方法を既存コード変更なしで追加可能
- LSP: どの通知方法でも同じように使える
- ISP: 読み取り専用サービスは書き込み機能に依存しない
- DIP: 具体的な実装ではなくインターフェースに依存 → テストしやすく変更に強い
この設計により、薬事法の変更や新しい決済方法の追加など、要件変更に柔軟に対応できるシステムになります。
Rails RSpecテスト高速化:times.doからCSVバルクインサートへの変更で実行時間短縮
はじめに
Rails RSpecテストで大量のテストデータが必要な場合、従来の1件ずつのレコード作成では実行時間が長くなってしまいます。この記事では、CSVファイルを使ったバルクインサートにより、実行時間を大幅に短縮した方法を紹介します。

従来の方法の課題
遅いコード例
# FactoryBotを使った従来の方法(遅い) 500.times do |i| FactoryBot.create(:prescription, pharmacy: pharmacy) end
この方法では以下の問題がありました: - 各レコードごとにActiveRecordの処理が発生 - バリデーションやコールバックが500回実行される - データベースへのINSERT文が500回発行される
CSVバルクインサートによる解決
基本的な実装方法
# CSVを使ったバルクインサート(速い) [ ["patients.csv", Patient], ["prescriptions.csv", Prescription], ["dispensing_records.csv", DispensingRecord] ].each do |csv_filename, model| csv_file = Rails.root.join('spec/fixtures/files', csv_filename) rows = CSV.read(csv_file, headers: true) # 必要に応じてデータを加工 data = rows.map do |row| row_hash = row.to_h row_hash['pharmacy_id'] = pharmacy.id if row_hash.key?('pharmacy_id') row_hash end model.insert_all(data) end
バルクインサートの利点
- SQLクエリの最適化: 複数のINSERT文が単一のクエリにまとめられる
- バリデーション回数の削減:
insert_allは個別のバリデーションをスキップ - メモリ使用量の最適化: ActiveRecordオブジェクトを大量に生成しない
簡単なベンチマーク測定
パフォーマンス改善効果を測定するための簡単なベンチマーク方法:
# ベンチマーク測定のヘルパーメソッド def benchmark_test(description) puts "🚀 #{description} - 測定開始" start_time = Time.current yield end_time = Time.current execution_time = end_time - start_time puts "✅ #{description} - 完了: #{execution_time.round(3)}秒" execution_time end # 使用例 RSpec.describe 'Performance Test' do it 'バルクインサートの実行時間測定' do benchmark_test("CSVバルクインサート") do # CSVバルクインサート処理 csv_data = [ {name: 'Test1', email: 'test1@example.com'}, {name: 'Test2', email: 'test2@example.com'} ] User.insert_all(csv_data) end end end
CSVファイルの準備
基本的なCSVデータの作成
CSVファイルはモデルの属性に対応したカラムで構成します:
# spec/fixtures/files/users.csv id,name,email,created_at,updated_at 1,Test User 1,test1@example.com,2025-01-01,2025-01-01 2,Test User 2,test2@example.com,2025-01-01,2025-01-01 3,Test User 3,test3@example.com,2025-01-01,2025-01-01
CSVデータ作成時のポイント
- 必須カラム: モデルのNOT NULL制約があるカラムは必ず含める
- 外部キー: 関連するモデルのIDを適切に設定
- タイムスタンプ:
created_at,updated_atは固定値でOK - データ量: テストに必要な件数を用意(通常100〜500件程度)
より現実的なCSVデータの例
# spec/fixtures/files/prescriptions.csv id,patient_id,pharmacy_id,doctor_name,prepared_on,created_at,updated_at 1,1,1,田中医師,2025-01-01,2025-01-01,2025-01-01 2,2,1,佐藤医師,2025-01-01,2025-01-01,2025-01-01 3,3,1,山田医師,2025-01-01,2025-01-01,2025-01-01
# spec/fixtures/files/patients.csv id,name,phone,address,created_at,updated_at 1,患者太郎,090-1234-5678,東京都渋谷区,2025-01-01,2025-01-01 2,患者花子,090-2345-6789,東京都新宿区,2025-01-01,2025-01-01 3,患者次郎,090-3456-7890,東京都品川区,2025-01-01,2025-01-01
CSVデータの生成方法
手動作成が大変な場合は、Rakeタスクで生成することも可能:
# lib/tasks/generate_test_csv.rake namespace :test do desc "Generate CSV files for bulk insert tests" task generate_csv: :environment do # 100件のユーザーデータを生成 CSV.open("spec/fixtures/files/users.csv", "w", headers: true) do |csv| csv << ["id", "name", "email", "created_at", "updated_at"] 100.times do |i| csv << [ i + 1, "Test User #{i + 1}", "test#{i + 1}@example.com", "2025-01-01", "2025-01-01" ] end end puts "CSV files generated successfully!" end end
まとめ
CSVバルクインサートは、大量のテストデータが必要なRSpecテストにおいて非常に効果的です:
特に500件以上のレコードが必要なテストでは、この手法により開発効率を大幅に向上させることができます。
React Native + Redux パフォーマンス最適化事例:useMemoによる参照安定化
はじめに
React NativeアプリケーションでReduxを使用している際、特定の状態変更が多数のコンポーネントに不要な再描画を引き起こすパフォーマンス問題に遭遇することがあります。
本記事では、実際のプロダクトで発生した「190個のコンポーネントが不要に再描画される」問題を、useMemoによる参照安定化で解決した事例を紹介します。
問題の背景
システム概要
- アプリケーション: 調剤薬局向けのピッキング・調剤支援アプリ
- 技術スタック: React Native + Redux + TypeScript
- 問題箇所: 調剤リスト表示画面(ListData.hook.ts)
発生していた問題
// 問題のあった実装 const useListData = ({ item, onPressListItem, fromPickingDispensingListScreen }: ListDataProps) => { // 非リアクティブな状態取得(変更を検知できない) const pharmacyPatientCharacteristics = useStore().getState().dispensing.pharmacyPatientCharacteristics // 他の処理... }
主な課題:
1. useStore().getState()は非リアクティブのため、Redux状態変更を検知できない
2. EveryStockで患者特性フラグを変更してもEveryPickの画面に即座に反映されない
3. 後にuseSelectorに変更したところ、190個のコンポーネントが毎回再描画される新たな問題が発生
技術的背景知識
1. Redux状態取得の違い
useStore().getState() vs useSelector()
// ❌ 非リアクティブ:状態変更を検知しない const state = useStore().getState() const data = state.dispensing.pharmacyPatientCharacteristics // ✅ リアクティブ:状態変更を自動検知 const data = useSelector((state: StateProps) => state.dispensing.pharmacyPatientCharacteristics )
重要なポイント:
- useStore().getState():スナップショット取得(その時点の値のみ)
- useSelector():状態購読(変更時に自動で再描画をトリガー)
2. React再描画の仕組み
Reactでは参照の同一性(===)で再描画が必要かを判定します:
// 毎回新しい配列参照が作られる場合 const Component = () => { const data = someApiCall() // 毎回新しい配列 [] return <ChildComponent data={data} /> // 毎回再描画 } // 参照が安定している場合 const Component = () => { const data = useMemo(() => someApiCall(), [dependency]) return <ChildComponent data={data} /> // 依存関係が変わらない限り再描画しない }
3. useMemoによる参照安定化
const pharmacyPatientCharacteristics = useMemo( () => rawCharacteristics, // 計算結果 [rawCharacteristics] // 依存配列 )
動作原理: 1. 初回実行時:計算結果をメモ化 2. 再実行時:依存配列の値を前回と比較 3. 変更なし:メモ化された値(同じ参照)を返す 4. 変更あり:新しい計算結果をメモ化して返す
実装の変遷
Phase 1: 非リアクティブ状態からの脱却
// Before: 状態変更を検知できない export const useListData = (props: ListDataProps) => { const pharmacyPatientCharacteristics = useStore().getState().dispensing.pharmacyPatientCharacteristics // ... } // After: 状態変更を検知できるが、パフォーマンス問題発生 export const useListData = (props: ListDataProps) => { const pharmacyPatientCharacteristics = useSelector((state: StateProps) => state.dispensing.pharmacyPatientCharacteristics ) // ... }
Phase 2: パフォーマンス問題の発見
CodeRabbitによる指摘:
🚨 重要なパフォーマンス問題
ListDataコンポーネントが190個存在し、pharmacyPatientCharacteristicsが変更される度に全て再描画される
Phase 3: 最適化アプローチの検討
選択肢1: アーキテクチャ変更(推奨だが高コスト)
// 親コンポーネントで一度だけuseSelectorを使用 const DispensingListContainer = () => { const characteristics = useSelector(state => state.dispensing.pharmacyPatientCharacteristics) return ( <div> {items.map(item => <ListData key={item.id} item={item} characteristics={characteristics} // Propsで渡す /> )} </div> ) }
選択肢2: useMemoによる参照安定化(選択した方法)
// 最小コストで最大効果 export const useListData = (props: ListDataProps) => { const rawCharacteristics = useSelector((state: StateProps) => state.dispensing.pharmacyPatientCharacteristics ) // 参照安定化:内容が同じなら同じ参照を返す const pharmacyPatientCharacteristics = useMemo( () => rawCharacteristics, [rawCharacteristics] ) return { values: { pharmacyPatientCharacteristics }, handlers: { /* ... */ } } }
最終実装とテスト
実装コード
import { useSelector, useDispatch } from 'react-redux' import { useMemo, useCallback } from 'react' import { StateProps } from '@types' export const useListData = ({ item, onPressListItem, fromPickingDispensingListScreen, }: ListDataProps) => { // Redux状態をリアクティブに取得 const rawCharacteristics = useSelector((state: StateProps) => state.dispensing.pharmacyPatientCharacteristics ) // useMemoで参照安定化(配列内容が同じなら同じ参照を返す) const pharmacyPatientCharacteristics = useMemo( () => rawCharacteristics, [rawCharacteristics] ) const dispatch = useDispatch() const navigation = useNavigation<NavigationProp>() const onPressListData = useCallback(() => { // ハンドラーの実装... }, [/* 依存配列 */]) return { values: { pharmacyPatientCharacteristics, }, handlers: { onPressListData, }, } }
テストによる検証
describe('useMemoによる参照安定化', () => { it('同じ内容の場合、参照が安定していることを確認', () => { const testCharacteristics = [{ id: 1, name: 'テスト特性' }] const mockStore = createStore(() => ({ dispensing: { pharmacyPatientCharacteristics: testCharacteristics, }, picking: { matchedList: [] }, })) const wrapper = ({ children }: { children: React.ReactNode }) => ( <Provider store={mockStore}>{children}</Provider> ) const { result, rerender } = renderHook(() => useListData(baseProps), { wrapper }) const firstRender = result.current.values.pharmacyPatientCharacteristics // 同じpropsでrerenderしても参照が安定していることを確認 rerender(baseProps) const secondRender = result.current.values.pharmacyPatientCharacteristics expect(firstRender).toBe(secondRender) // 参照が同じ ✅ expect(firstRender).toEqual(testCharacteristics) // 内容も正しい ✅ }) })
判断基準とトレードオフ
なぜuseMemo最適化を選択したか
| 要因 | アーキテクチャ変更 | useMemo最適化 |
|---|---|---|
| 実装コスト | 高(複数ファイル変更) | 低(1行追加) |
| テストコスト | 高(広範囲な回帰テスト) | 低(単体テスト追加のみ) |
| 変更頻度 | - | 月数回程度と低頻度 |
| パフォーマンス効果 | 最大 | 充分 |
| 保守性 | 最良 | 良好 |
決定要因: - 変更頻度が月数回と低い - 最小コストで問題解決可能 - ROI(投資対効果)が高い
useMemoの制限事項
// ❌ 浅い比較のみ:オブジェクトの中身は比較しない const data = useMemo(() => complexObject, [complexObject]) // ✅ 適切な依存関係の指定が必要 const processedData = useMemo( () => rawData.map(item => ({ ...item, processed: true })), [rawData] // rawDataが変わった時のみ再計算 )
学んだ教訓
1. パフォーマンス問題の早期発見
- CodeRabbitなど静的解析ツールの活用
- 実装時のパフォーマンス影響の考慮
2. 適切な最適化手法の選択
- 問題の規模と頻度の評価
- コスト対効果の分析
- 段階的な最適化アプローチ
3. テスト駆動での検証
- 参照安定化の動作確認
- パフォーマンス向上の定量的測定
まとめ
本事例では、Redux + React Native環境でのパフォーマンス問題をuseMemoによる参照安定化で解決しました。重要なのは:
- 問題の正確な把握:190コンポーネントの不要な再描画
- 複数解決策の検討:アーキテクチャ変更 vs 最小コスト最適化
- 適切な判断基準:変更頻度、実装コスト、効果のバランス
- 確実な検証:テストによる動作確認
パフォーマンス最適化は「何でもかんでもuseMemo」ではなく、問題の本質を理解し、コストと効果を天秤にかけた適切な判断が重要です。
参考リンク
無料で使える!Perchance AI Icon Generatorでアプリのアイコンを簡単作成
アプリやWebサービスを開発するとき、「アイコンデザインをどうしよう?」と悩んだ経験はありませんか? そんな時におすすめなのが、Perchance AI Icon Generatorという無料のアイコン生成ツールです。
この記事では、Perchance AI Icon Generatorの魅力や使い方、そして実際に生成したアイコン例をご紹介します。
Perchance AI Icon Generatorとは?
Perchance AI Icon Generatorは、AIによってアプリ用アイコンを自動生成できるツールです。 ブラウザ上で動作し、ユーザー登録不要、完全無料で利用できるのが大きな特徴です。
- 無料で利用可能
- 登録不要で即使える
- プロンプトを入力するだけで自動生成
- PNGでアイコンを保存可能
実際に使ってみよう
以下のURLにアクセス: → https://perchance.org/ai-icon-generator
ページ中央のテキストエリアに生成したいアイコンの説明を入力します。 例:
A minimalist shopping list app icon using warm colors like orange and coral. Flat design.- Art Styleでトーンを、Shapeで形を設定できます。

おすすめの使い方・活用例
注意点
- 商用利用については各生成結果のライセンスを個別に確認することをおすすめします。
- 高解像度やSVG形式のエクスポートは対応していません。
- デザインの細かな調整は、FigmaやLunacyなどのツールで行うのがベターです。
まとめ
Perchance AI Icon Generatorは、デザインの知識がなくても直感的に使える便利なツールです。特に個人開発やアイデアスケッチ段階のアプリにおいて、スピーディーに見た目を整えるのに最適です。
興味がある方は、ぜひ一度使ってみてください!
macOSでMP4からスマホ向けオンボーディング用GIFを作成する手順メモ(ffmpeg + gifsicle活用)
はじめに
現在スマホアプリを開発していて 初回起動時のスライドチュートリアル(オンボーディングスライド)を作成する際に スマホの画面からgifを作成する機会があり、今後も使用することがあると思ったので手順を残しておきます。

この記事で得られること
- スマホで表示するサイズ感に最適なGIFの作り方がわかる
- 再生速度を自然に調整する方法がわかる
- ファイルサイズを小さく、かつ綺麗にする方法がわかる
- 実際に使用するコマンド例をそのままコピーして使える
手順
1.FFmpegとgifsicle(GIF操作ツール)をインストールします。
brew install ffmpeg
brew install gifsicle
2.iphoneの画面収録機能などでMP4画像を作成してAirDropなどでmacへ送る
3. MP4ファイルからGIFを作成する
まずはスマホの録画動画(mp4ファイル)から、GIFファイルを作成します。
ここではffmpegを使います。
# ファイルを置いている場所と同じ場所で以下コマンドを打つ ffmpeg -i slide1.mp4 -vf "fps=15,scale=720:-1:flags=lanczos" slide1.gif
このコマンドの例ではslide1.mp4という動画をslide1.gifというgifに変換します。 ファイル名は使っている環境に合わせて変更してください。
- fps=15:スムーズな再生のためフレーム数を15fpsに指定
- scale=720:-1:横幅を720px、高さは自動調整
- flags=lanczos:高品質なリサイズ方法
3. 複数のGIFを連結してスライドショーにする
# GIFを連続再生用に結合する(ループあり) gifsicle --loop --delay=5 slide1.gif slide2.gif > onboarding.gif
このコマンドで上で作成したgif画像をこのonboarding.gifという一つのループするgifに変換します。 こちらもファイル名は使っている環境に合わせて変更してください。
- --loop:無限ループ再生
- --delay=5:1フレームごとの待機時間(0.05秒) 数値を小さくすると速く、大きくすると遅くなる
私の環境ではdelay=5で良さそうでした。
一応ここまででgif画像の生成としては終わりです、簡単です。 以下はおまけでサイズを小さくする方法と画像を綺麗にする方法を記載します。
4. ファイルサイズを小さく、綺麗にする最適化
# 高圧縮・軽量化(最適化) gifsicle -O3 --colors 64 onboarding.gif > onboarding_optimized.gif
- -O3:最大限の最適化
- --colors 64:使用する色数を64色に減らす(十分綺麗)
上記を打つと20秒程度の画像で370kbから330kb程度にサイズを縮小できました。 私は余り変わらないと思ったのでこちらは適用しませんでした! 他にパレット生成という方法もある様ですので、こだわる方は試してみても良いかもしれません。
まとめ
- スマホ用のスライドチュートリアルGIFは、ffmpeg + gifsicleで簡単に作成できる
- 適切なfps設定と、delay値の調整で自然な動きになる
- gifsicleの-O3と--colors指定で十分に軽量化できる
- さらにこだわるならパレット生成もおすすめ
次回以降の開発でもスムーズに作業できるように、この手順をベースにカスタマイズして使っていきたい。