arait-code’s RC

RailsとReact,Vue,TypeScriptメインのWEBエンジニアです。

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.zshsource <(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がレビュー 
→ 開発者は調査済み状態から作業開始

要するに、朝起きて「昨日の夜にエラーが発生していますが、すでに原因と修正方針は調査済みです」みたいな状況を作りたいんです。

現在の進捗

完了したもの

  • RollbarからGitHub Issueへの自動転送
  • GitHub ActionsでDevin AIを呼び出す基本機能
  • エラーの種類を自動で分類する仕組み
  • Devinの調査結果を自動で取得する機能

実装中のもの

  • 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原則の効果:

  1. SRP: 薬局管理、在庫管理、取引管理を分離 → 変更時の影響範囲が限定的
  2. OCP: 新しい支払い方法や通知方法を既存コード変更なしで追加可能
  3. LSP: どの通知方法でも同じように使える
  4. ISP: 読み取り専用サービスは書き込み機能に依存しない
  5. 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

バルクインサートの利点

  1. SQLクエリの最適化: 複数のINSERT文が単一のクエリにまとめられる
  2. バリデーション回数の削減: insert_allは個別のバリデーションをスキップ
  3. メモリ使用量の最適化: 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データ作成時のポイント

  1. 必須カラム: モデルのNOT NULL制約があるカラムは必ず含める
  2. 外部キー: 関連するモデルのIDを適切に設定
  3. タイムスタンプ: created_at, updated_atは固定値でOK
  4. データ量: テストに必要な件数を用意(通常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テストにおいて非常に効果的です:

  • 実行時間の大幅短縮: 1件ずつの処理と比較して劇的な改善
  • シンプルな実装: 複雑なロジックは不要
  • 測定可能な改善: ベンチマークにより効果を定量化可能

特に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による参照安定化で解決しました。重要なのは:

  1. 問題の正確な把握:190コンポーネントの不要な再描画
  2. 複数解決策の検討アーキテクチャ変更 vs 最小コスト最適化
  3. 適切な判断基準:変更頻度、実装コスト、効果のバランス
  4. 確実な検証:テストによる動作確認

パフォーマンス最適化は「何でもかんでもuseMemo」ではなく、問題の本質を理解し、コストと効果を天秤にかけた適切な判断が重要です。

参考リンク

無料で使える!Perchance AI Icon Generatorでアプリのアイコンを簡単作成

アプリやWebサービスを開発するとき、「アイコンデザインをどうしよう?」と悩んだ経験はありませんか? そんな時におすすめなのが、Perchance AI Icon Generatorという無料のアイコン生成ツールです。

この記事では、Perchance AI Icon Generatorの魅力や使い方、そして実際に生成したアイコン例をご紹介します。


Perchance AI Icon Generatorとは?

Perchance AI Icon Generatorは、AIによってアプリ用アイコンを自動生成できるツールです。 ブラウザ上で動作し、ユーザー登録不要、完全無料で利用できるのが大きな特徴です。

  • 無料で利用可能
  • 登録不要で即使える
  • プロンプトを入力するだけで自動生成
  • PNGでアイコンを保存可能

実際に使ってみよう

  1. 以下のURLにアクセス: → https://perchance.org/ai-icon-generator

  2. ページ中央のテキストエリアに生成したいアイコンの説明を入力します。 例: A minimalist shopping list app icon using warm colors like orange and coral. Flat design.

  3. Art Styleでトーンを、Shapeで形を設定できます。

  1. 「Generate」ボタンをクリック。

  2. 数秒待つと、AIが生成したアイコン画像が表示されます。

  3. 気に入ったものがあれば右クリックなどで画像を保存します(PNG形式)。


おすすめの使い方・活用例

  • スマホアプリのアイコンデザインに
  • サイドプロジェクトやプロトタイプのUI素材に
  • デザインのアイデア出しに(参考資料として)

注意点

  • 商用利用については各生成結果のライセンスを個別に確認することをおすすめします。
  • 高解像度や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指定で十分に軽量化できる
  • さらにこだわるならパレット生成もおすすめ

次回以降の開発でもスムーズに作業できるように、この手順をベースにカスタマイズして使っていきたい。