s4na's blog

s4naのテックブログ

ファーストペンギン

$bundle install を実行した際に "1 installed gem you directly depend on is looking for funding. Run `bundle fund` for details" と表示されたのでその意味を調べた

結論

何か重大なエラーがあるとかいう話ではなく、 $bundle fund を実行するとメンテナーが資金を探しているGemのURL一覧が表示されるということを伝えるためのメッセージみたいです。

$bundle fund コマンドについて

最近追加されたばかりからか、公式ドキュメントにもまだ追加されていませんでした。

bundler.io

どういうコマンドなのか知るためには追加したPRに書いてあるRFCが参考になりました。

$bundle fund を追加したPR

github.com

$bundle fund を提案したRFC

github.com

簡単に要約すると、

「メンテナーが積極的に資金を探しているGemについて、そのリポジトリのURL一覧が表示されるコマンド」

らしいです。

追加方法はこちらに書いてある通り、 gemspecに funding_uri というパラメーターを追加するだけで良いそうです。

Gem::Specification.new do |gem|
  gem.name = "#{GEM_NAME}"
  gem.homepage = "#{GEM_HOMEPAGE}"
  s.metadata = {
    "funding_uri" => "#{GEM_FUNDING_PAGE}"
  }
end

sass-railsを使っていて"Ruby Sass has reached end-of-life and should no longer be used."とエラーが表示されたら

sass-railsを使うと表示されるエラーメッセージ

Ruby Sass has reached end-of-life and should no longer be used.

* If you use Sass as a command-line tool, we recommend using Dart Sass, the new
  primary implementation: https://sass-lang.com/install

* If you use Sass as a plug-in for a Ruby web framework, we recommend using the
  sassc gem: https://github.com/sass/sassc-ruby#readme

* For more details, please refer to the Sass blog:
  https://sass-lang.com/blog/posts/7828841

エラーの意味

どうやらsassというGemに寿命が来てしまったらしいです。
sassじゃなくてsasscを使えば良いと書いてありました。

とはいえsassを使っているのはsass-rails。僕じゃどうしようもないのかな?🤔

sass-lang.com

と思って調べていたところ、sass-rails側で対応されていました。 github.com

github.com

対処方法

ということで、Gemfileでsass-railsのバージョンを6以降にしましょう。

# Gemfile
gem "sass-rails", "~> 6"

さいごに

エラーメッセージに書いてある

https://sass-lang.com/blog/posts/7828841 にアクセスしてみてもNot foundが返ってきてしまいますね。

たぶん https://sass-lang.com/blog/ruby-sass-is-unsupported だと思うんですけど、

GitHubリポジトリがread-onlyになっているので、PRも投げられなさそう?🤔

github.com

最新版のDevise 4.7.3が入っている状態でOmniAuth 2.0.0をインストールすると、Rails Serverを起動することができない問題の対処方法

Dependabotに「Devise 1.9.1はSecurityに脆弱性があるからDevise 2.0.0にアップデートしよう」と通知をもらったので、ライブラリのバージョンをアップデートしようとしたところ、Rails Serevrが起動しなくなりました。😢

実行環境

  • Ruby 2.7.2
  • Rails 6.1.0
  • Devise 4.7.3
  • OmniAuth 2.0.0

エラー内容

/Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/devise-4.7.3/lib/devise/omniauth.rb:12:in `<main>': You are using an old OmniAuth version, please ensure you have 1.0.0.pr2 version or later installed. (RuntimeError)

OmniAuthのバージョンは2.0.0なので、1.0.0より古いはずはない・・・🤔

エラー全文

$ bundle exec rails c
/Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/devise-4.7.3/lib/devise/omniauth.rb:12:in `<main>': You are using an old OmniAuth version, please ensure you have 1.0.0.pr2 version or later installed. (RuntimeError)
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/zeitwerk-2.4.2/lib/zeitwerk/kernel.rb:34:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:332:in `block in require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:299:in `load_dependency'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:332:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/devise-4.7.3/lib/devise.rb:443:in `omniauth'
    from /Users/mbp2021/projects/s4na/twi-note/config/initializers/devise.rb:266:in `block in <main>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/devise-4.7.3/lib/devise.rb:307:in `setup'
    from /Users/mbp2021/projects/s4na/twi-note/config/initializers/devise.rb:5:in `<main>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:326:in `block in load'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:299:in `load_dependency'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:326:in `load'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/engine.rb:681:in `block in load_config_initializer'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/notifications.rb:205:in `instrument'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/engine.rb:680:in `load_config_initializer'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/engine.rb:634:in `block (2 levels) in <class:Engine>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/engine.rb:633:in `each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/engine.rb:633:in `block in <class:Engine>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:32:in `instance_exec'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:32:in `run'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:61:in `block in run_initializers'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:228:in `block in tsort_each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:422:in `block (2 levels) in each_strongly_connected_component_from'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:431:in `each_strongly_connected_component_from'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:421:in `block in each_strongly_connected_component_from'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:50:in `each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:50:in `tsort_each_child'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:415:in `call'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:415:in `each_strongly_connected_component_from'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:349:in `block in each_strongly_connected_component'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:347:in `each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:347:in `call'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:347:in `each_strongly_connected_component'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:226:in `tsort_each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/2.7.0/tsort.rb:205:in `tsort_each'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/initializable.rb:60:in `run_initializers'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.1.0/lib/rails/application.rb:384:in `initialize!'
    from /Users/mbp2021/projects/s4na/twi-note/config/environment.rb:7:in `<main>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/zeitwerk-2.4.2/lib/zeitwerk/kernel.rb:34:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:332:in `block in require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:299:in `load_dependency'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.0/lib/active_support/dependencies.rb:332:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application.rb:106:in `preload'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application.rb:157:in `serve'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application.rb:145:in `block in run'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application.rb:139:in `loop'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application.rb:139:in `run'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/spring-2.1.1/lib/spring/application/boot.rb:19:in `<top (required)>'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:85:in `require'
    from /Users/mbp2021/.rbenv/versions/2.7.2/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:85:in `require'
    from -e:1:in `<main>'

原因

原因を調べてみたところ、

lib/devise/omniauth.rb のファイルに

github.com

unless OmniAuth::VERSION =~ /^1\./
  raise "You are using an old OmniAuth version, please ensure you have 1.0.0.pr2 version or later installed."
end

と書いてあって、OmniAuthのバージョンが 1.X.X じゃないとエラーになってしまうようでした。

対処方法(更新:2021/5/5)

Devise 4.8.0がリリースされているので、Gemのバージョンアップをすることで対処可能です。

github.com

バージョンアップの方法はGemfileに

gem "devise"

と記載して、ターミナルで

$ bundle update devise

を実行すれば更新されます。

おわりに

相談に乗ってくださった神速さんありがとうございました🙏

ChromeでURLを検索するときに表示される候補の順番を変更する方法

右側に❌ボタンがあるので、❌を押して消すと次の候補が表示されるようになりました。

f:id:s4na:20210118234310p:plain

知らなかった。便利。

他にも情報がわかったら追記します。

Karabiner Elementsに「Option Keyで "英数入力"と"かな入力"を切り替えを行うルール」を追加しました。

Karabiner Elements の拡張機能リポジトリにPRを出しました。

github.com

出したPRはこちらです。 🙇‍♂️

github.com

PRの内容

「Option Key で "かな入力" と "英数入力" を変換できるルール」の追加を行っています。

ルールをオンにすることで、「左のOption Keyを押すと "英数入力" 」「右のOption Keyを押すと "かな入力" 」に切り替わります。

f:id:s4na:20201116235712p:plain

karabiner-elements-complex_modifications

PRを出した背景

もともと Command Key を使うルールもあったのですが、直後に文字を入力するとショートカットキーを実行されてしまうことがあったりして不便に感じていました。 Option Keyならショートカットキーに指定していることが少なく便利だと思い、PRを出しました。

今回追加したルールの使い方

以下のサイトにアクセスして、

karabiner-elements-complex_modifications

「For Japanese (日本語環境向けの設定) (rev 6)」をImportします。

f:id:s4na:20201117000745p:plain

Import したルールを Enable します。

f:id:s4na:20201117000831p:plain

おわりに

自分で書いたコードが、色んな人に良い影響を与えられたら嬉しいな〜と思います 😊

※誤字脱字ございましたら、こっそり教えてください🙏

Kaigi on Rails(2020/10/3開催)のスライドをまとめました。

はじめに

Kaigi on Railsのスライドについて、見直したかったので調べていたところ、
「まとまっているサイトがないな〜」と思っていましたが、

「いや、ないなら自分で作ればいいじゃん!!」と思いつき、まとめてみました。

これでつつがなく資料を見返せそうです。😌

概要

2020/10/3 に行われた Kaigi on Rails について、資料をまとめました。

※一部資料見つからなかったため、まとめきれておりません。
ご存知の方がいらっしゃいましたら、こっそり教えていただけると幸いです🙇‍♂️

連絡先 👉 Twitter: @s4na_penguin

Kaigi on Railsの公式サイト

kaigionrails.org

Aaron Patterson さん

www.youtube.com

kokuyouwind さん

slides.com

関連ブログ

blog.kokuyouwind.com

toshimaru_e さん

www.youtube.com

speakerdeck.com

lulalala_it さん

makicamel さん

s01 さん

speakerdeck.com

yucao24hours さん

関連ブログ

fukajun さん

speakerdeck.com

関連ブログ

techblog.locoguide.co.jp

pink_bangbi さん

osyo-manga.github.io

akiko_pusu さん

koic さん

speakerdeck.com

関連ブログ

shimoju_ さん

scrapbox.io

関連

joker1007 さん

Akira Matsuda さん

speakerdeck.com

OKURA Masafumi さん

okuramasafumi.hatenablog.jp

「32inch 4K IPS 非光沢 HDMIx2」のディスプレイを購入しました。

最近、「32inch 4K IPS 非光沢 HDMIx2」のディスプレイを購入しました。

購入したのは以下のモニターです。

www.amazon.co.jp

探した時の優先度

  1. 32inch
  2. 4K
  3. 非光沢
  4. IPS(VAより良さそう)
  5. Type-c

このモニターに出会うまでいろんな人に話を聞き、最終的に悩みに悩み抜いて2ヶ月ほどかかったのですが、買って良かったと思っています。

小話として、一度42inchのディスプレイも購入したのですが、サイズなど測っていたのですが、想定していたより大きく返品(クーリングオフではないので半額ほど請求された)しました。

購入を通して学んだこと

  • 32inch最高
  • 4K最高(2Kとして使っているけど)(しばらくモニターの狭さに怯えることはなさそう)
  • 非光沢最高(自分の顔が写ったりしないのが良い)
  • IPSの良さはまだわからない
  • Type-cじゃなくてもあまり気にならない(ハブにいろいろつなげているし)
  • モニターアームは正直必要なかった。というか失敗した・・・
  • 42inchはデカすぎる。一度購入してから返却した。
  • モニターの起動音、音をオフにしても鳴るのがびみょい

メインのMacのOSを入れ直したので、個人的な手順書を更新しました。

macOS を Catalina に更新してから、色々不調だったので、思い切ってOSをクリーンインストールしました。

クリーンインストールする際、参考にした記事。

qiita.com

macOS Catalinaは他と少し手順が変わっているので、注意が必要です。 ディスクユーティリティーでストレージを2つ消さないと、やり直すことになるかもしれません。(私はやり直しました。)

今回の手順を踏まえて更新した手順書。

https://github.com/s4na/setup/blob/master/docs/setup.mdgithub.com

何度もクリーンインストールする際はこういう手順書があると便利ですよね。 今回も途中でミスに気づいてやり直したのですが、その際に手順書を書いていたことにとても助けられました。

2021/1/27 追記

環境構築はdotfilesで行うようになりました。

dotfilesに関しては以下リンクにまとめています。

scrapbox.io

Sendagaya.rbで、RubyKaigi2020で頂いたりんごジュースを決めるためのくじ引きを作りました

所要時間10分で、Sendagaya.rbのメンバーそれぞれ書きました。

書いた後、「あれ、どのメンバーのコードを使ってくじをするのか決めるためのくじも必要だな」と思ったのですが、それはまた別の機会に書きます。

私のコード

class Amida
  def initialize(members)
    @members = members.split(' ')
  end

  def result()
    p "参加者:#{members}"
    p "参加人数:#{num}"
    p "当選者:#{winner}"
  end

  def members
    @members
  end

  def num
    members.size
  end

  def winner
    @winner ||= members[rand(num)]
  end
end

Amida.new('Asan Bsan Csan').result

ポイントは、 #winner||= で書かれていることで、インスタンス変数に格納されるので、 winner を呼び出す度に結果が変わりません。

Webマーケッター瞳を読みました

はじめに

Webマーケティングに関してわかりやすく説明されているWeb漫画があったので読みました。その中でも今回は、気になった単語をピックアップして調べてまとめました。

読んだWeb漫画

気になった単語

  • UU
    • ユニークユーザー
  • CVR: Conversion Rate
    • =登録者数/UU
    • 購入や申し込みに至った顧客率
  • 流入経路
  • LP
    • ランディングページ
  • LPO
  • KPI設定
    • PVよりCVRが大事
  • Webコミュニケーション
    • キャラクター目当てできた客が期待と違うサイトで1PV見た後流出した場合
      • キャラクターサイトを作ることで、顧客を逃さない。
  • PVの分析
    • FAQのPVが伸びると、顧客からの問題意識が高まっているかも
  • PPM: Product Portfolio Management
  • リスティング広告
    • 検索語句に連動して表示される広告

has_many throughは直訳するとわかりやすい

has_many through、ずっと「うーん、いまいちわかったようなわかってないような〜」と思っていたのですが、直訳したらとてもわかりやすかったので簡単にまとめます。

このブログに書かれている User, Comment, Blosの関係を参考にします。

mikamisan.hatenablog.com

英文に直すと、以下のようになります。

User has many comments, through blogs

これをGoogleで翻訳すると以下のようになります。

ユーザーはブログを通じて多くのコメントを持っています

Userはcommentsを持っていて、 それを通じて更にblogsを持っているんです。

ひとこと

最近までthrow(投げる)だと勘違いしていて、「何を投げるんだ?」と思っていました。

ノートにツイートを貼れるサービス「twi-note」を作成しました

目次

はじめに

今回の記事では、私がフィヨルドブートキャンプで作成した、「twi-note」というサービスについて書きました。

twi-noteとは?

github.com

twi-note.herokuapp.com

twi-noteはノートにツイートが貼れるサービスです。

一番のポイントは「ツイートが簡単に貼れる」ところです。

ツイートを簡単に貼るための4つの工夫

ポイント1:検索結果の全ツイートを一括で貼れます。

ツイートの検索結果から一つ一つツイートをいちいち選ぶのが面倒な場合、一気に貼ることができます。

ポイント2:ツイートをドラッグ&ドロップで貼れます。

ツイートの検索結果からツイートをドラッグ&ドロップで貼ることができます。

ポイント3:時間を指定して、ツイートを検索できます。

ツイッター公式の検索機能では日付単位でしか検索できないのですが、twi-noteを使えば時間指定で検索する事ができます。 ※Twitter APIの関係上、現状1週間ほど前までのツイートを検索することができます。

ポイント4:作成したノートをダウンロードできます

作成したノートを、テキストファイルとしてダウンロードすることができます。 そのため、その後自分のエディタやノートサービスで作業を継続することができ、今まで使ってきたノートと競合することがありません。

作った理由は、ノートにツイートを貼るのをもっと簡単にしたかったからです

サービスを作り始めたきっかけは単純で、ノートにツイートを貼るのを「もっと簡単にしたかったから」です。

私は技術系の勉強会に参加するのが大好きで、昨年だけでも55回参加しています。(数えたらそうでした)

そして、勉強会に参加した際、ほぼ毎回ノートを作成しており、 そのノート作成にあたって、勉強会のツイートを使っていたことから、 ツイートをまとめられるサービスがあったら便利だな〜と思い作りました。

似ているサービスとの違い

「なぜ自分で作成するの?Togetterさんがあるのでは?」 そう思われるかもしれません。

Togetterさんは大変便利なサイトです。 ですがTogetterさんの場合、Togetterさんのサイトでツイートをまとめることがメインです。

私の場合は最終的には自分のノート(Markdown)に情報をまとめることがメインなので、 目的と手段が違っているし、やりたいことができなかったから作りました。

フィヨルドブートキャンプというプログラミングスクールでサービスを作成しました

今回の記事ではフィヨルドブートキャンプの話が多分に出てきますので、まずは簡単にご説明させていただきます。

フィヨルドブートキャンプとは、現場の即戦力になれるプログラミングスクールです。

bootcamp.fjord.jp

フィヨルドブートキャンプでの「現場の即戦力」の定義は「Issueを一人でこなせる人」になります。

詳しい定義については、フィヨルドブートキャンプのメンターであるkomagataさんのブログにまとまっていますので、下記記事をご確認いただければと思います。

Railsエンジニアとして就職できるレベルとは - komagataのブログ

今回私は、このフィヨルドブートキャンプで「デザインレビュー+コードレビュー」を受けながらサービスを作成しました。

初めてのサービスづくりで挑戦したことと、学んだこと、こうすれば良かったと思ったことについてまとめ

サービス企画編

Getting Realという本のやり方を真似する事で、ユーザーが0じゃないサービスを作ることができる

フィヨルドブートキャンプでは、サービスを企画するにあたりBasecampが作成したGetting Realという本をもとにしてサービスを企画します。 ※Basecamp=Ruby on Railsを開発したDHHさんが所属されている会社です。

このGetting Realを参考にする事で、「自分の問題を解決することができる = ユーザーが最低一人は居るサービス」を作ることができます。

どんな方法なの?と思った方もいらっしゃると思います。 自分なりに重要だと思った事をまとめ他内容を以下に書いておきます👍

Getting Realまとめ
## 最も重要なこと3点
- シンプルに作る
- 実際の画面から作り始める
- 自分=顧客が本当に必要なものだけを提供する

## シンプルに作る理由
素晴らしいソフトウェアには、無駄な線や無駄なパーツがないので、全てが作り手の表現したいことになり、伝わりやすくなります。
また、技術的負債や肥大化が起きず、大きなお金やチーム、長い開発サイクルが不要になります。

## 実際の画面から作り始める理由
機能的な仕様は一種の虚像であり、現実のユーザーの体験は実際の画面にあります。
実際の画面から作り始めることで、より早く現実のユーザーの体験に辿りつけるからです。

## 自分=顧客が本当に必要なものだけを提供する理由
自分の問題である時、本当に必要なものだけを作ることができます。
顧客にとって本当に必要なものを作る時、それは付加価値ではありません。
付加価値は、ユーザーのためではなく競争相手に打ち勝つために追加されています。

本を読んだだけではすぐ実践できない

Getting Realを読み終わった当初、サービスの企画案をガンガン出していったのですが、全部落ちました。

改めて考えてみると、Getting Realの概念が自分のものになっていなかったからだなと思います。

まとめるのは重要ですね・・・これからも頑張ってブログを書いていきたい・・・

イデア出しに挑戦

では実際にどういうアイデア出しを行ったのか。 通った企画と通らなかった企画を以下に載せておきます。

通った企画(今回作成したサービスの企画)
# twi-note

## エレベーターピッチ
[twi-note]というサービスは、
[勉強会参加時、ノートばかりとらず、楽しみながら参加したいけど、やはりノートを取らないと知見がたまっていかない問題]を解決したい
[勉強会参加者]向けの、
[ノート作成支援アプリ]です。
ユーザーは [勉強会のハッシュタグのついたツイッターのタイムラインをノートに貼り付けること] ができ、
[通常のテキストだけのメモ] とは違って、[勉強会のハッシュタグと時間を入力するだけで、簡単にmarkdownのノートとしてまとまる機能] が備わっている事が特徴です。

## 似ているサービス
[togetter](https://togetter.com/hot)
ツイートまとめサービス

[min.t](https://min.togetter.com/)
togetterと同じ開発会社がツイートまとめアプリとして開発

通らなかった企画+フィードバック

# myなろうランキング

## エレベーターピッチ
[myなろうランキング] というサービスは、
[小説家になろうというサービスで、ユーザーが新しい作品を探そうとした時、ランキングを見てもに既にみたことのあるの作品が表示されるという問題] を解決したい
[小説家になろうのヘビーユーザー(毎日ランキングを見るような)] 向けの、
[スコップ(新規作品を探す時のスラング)アプリ]です。
ユーザーは [サイトに登録して、作品を既読リストに追加することで、自分用にカスタマイズされた日次、週次、月次、年次ランキングを見ることができる。] ができ、
[既存の紹介サイト、ランキングサイト] とは違って、
[既に読んだことのない作品が見つかるという要素] が備わっている事が特徴です。

## 備考
- [小説家になろう](https://syosetu.com/)のホームページ
- 「小説家になろう」は情報取得のため[API](https://dev.syosetu.com/)を用意してくださっている。
- 商標利用について、表記のルールはあるものの、ルールを守れば利用可能みたいです
  - https://hinaproject.co.jp/hina_guideline.html

## FB
> 機能追加という時点で不要な機能になってしまいそう
> これは小説家になろうさんが解決する課題
> 会社や面接官によっては理解されないことはあるかもしれないですね。この問題自体、悪くはなさそうです
> 自分が採用する側に立って考える
> どっちを評価する会社に入りたいか
# アーチャーチューナー

## エレベーターピッチ
[アーチャーチューナー] というサービスは、
[アーチェリーの上達を慣れや勘任せにしている] を解決したい
[自分の癖がわからない人] 向けの、
[癖分析アプリ]です。
ユーザーは [打った矢の位置を記録すること] ができ、
[手書きのメモ] とは違って、
[経過時間による癖がわかる機能] が備わっている事が特徴です。

## FB
> 今現在の s4naさんの 問題ではない(学生時代やっていたものの、私は今現役でアーチェリーをしていない為)
# 年間行事スケジューラー

## エレベーターピッチ
[年間行事スケジューラー] というサービスは、
[例年ある作業だが、作業日がパッとしないためギリギリまで残る問題] を解決したい
[年間行事を記憶頼りに行なっている人] 向けの、
[スケジュール管理サービス]です。
ユーザーは [一度行事の設定を行うだけで、作業日を決めることができ] ができ、
[記憶頼りの作業] とは違って、
[サービスが作業日を自動で決めてくれる] が備わっている事が特徴です。

## ユーザーの入力作業
作業名:夏の大掃除
作業日:8月第2週金曜日
作業日オプション:平日を除く
通知:メール
通知対象:本人・家族

## 対象の行事と効果
- 誕生日の一週間前に準備する
    - ギリギリになりがち
- 家族で夏に大掃除
    - 毎年時期が近くなってきたらいつやるのか?いつが空いてるのか?問題になる
- 年賀状準備
    - 毎年遅くなる。

## FB
> Googleスケジューラーで良さそう
# 予定かぶらな〜い

## エレベーターピッチ
[予定かぶらな〜い] というサービスは、
[予定調整アプリで複数の予定を調整すると被ったり調整が入る問題] を解決したい
[忙しい人] 向けの、
[予定調整サービス]です。
ユーザーは [予定の候補日を入力すること] ができ、
[調整さん] とは違って、
[予定が重複しない機能] が備わっている事が特徴です。

## 問題
本アプリ以外での予定調整情報が出揃わないと、予定重複が回避できない。
ネーミングから必ず回避できるというユーザーの誤った印象を与えそう

予定管理者が予定決定ボタンを押さないと反映されない

## FB
> 全ての予定をサービスで管理しなければいけないのが弱い

デザイン編

ペーパープロトタイプのデザインに挑戦

人生の中で、デザインを積極的に行なってこなかったツケが回ってきたな・・・と思いながら挑みました。

実際に作ってみたことで、もっと細かく作り込んでおけば手戻りがなかったな・・・ということがたくさんあります。 特にランディングページの内容について細かく決めておくべきだったと思っています。

f:id:s4na:20200204191928p:plain

f:id:s4na:20200204192007p:plain

f:id:s4na:20200204192017p:plain

f:id:s4na:20200204192036p:plain

f:id:s4na:20200204192052p:plain

f:id:s4na:20200204192108p:plain

アイコンがきれいだと、やる気が出る。サービス感が出てくる

はてなブックマークで以前見かけた記事を参考に、作成しました。 (今見たところ、記事が非公開になっていたので、リンクは貼り付けておりません)

アイコンを作成したことで、サービスを作ってる感が出てきてテンションが上がりました💪

f:id:s4na:20200224191201p:plain

アイコンの作成方法

アイコンは以下のステップで作成しています。

  • フリーアイコンを探す。(ライセンスもちゃんと確認する。)
  • グラデーションの画像を探す。
    • itmeoで探しました。
  • マスク処理のできる画像編集アプリを用意する
    • 今回画像編集アプリにFigmaを利用しました。
  • グラデーション画像をフリーアイコンでマスク処理をする

サイトに使う色決め、とても難しい

色を決める際について考えていたことは、主に2つです。

  1. 「色相=色の意味」を決める
  2. 「色の明度、彩度=色づかい」を決める
「色相=色の意味」を決める

調べたところ、optinmonsterというサイトにサイトで使う色使い(危険、安全などなど)が載っていたので、こちらを参考に色の意味を決めていきました。

また、共通化するために、以下のようにSassのファイルに色を定義しました。

$normal-button-color: #7d666a
$primary-color: #2d92c4
$secondary-color: #239986
$danger-color: #e9566e
$warning-color: #dfc333
$infomation-color: #2fcd64
$disabled-color: #c1c5b9
「色の明度、彩度=色づかい」を決める

色の明度、彩度によって色の見え方感じ方が全然変わります。

まずは自分で作ってみようと思い、作ってみたものの。全然ダメでした😢 調べてみたところ、ジェネレーターを使うといいという記事があったので、それを参考にしてみたら、上手くいきました。

使用したサイトについて、簡単に説明させていただきます。

色々な色の組み合わせを見せてくれるサイト

https://coolors.co/

このサイトを使い、ランディングページ(トップページ)の色使いを決めました。 色を組み合わせるのが苦手なので、大変助かりました。

1つの色を入力するとそれに合わせて130色作成してくれるサイト

https://palx.jxnblk.com/

このサイトを使うと、1つの色を入力するだけで130色(=13色相×明暗10色)の色を作成することができるのできます。

色の色相だけ変える作業が苦手なので大変助かりました🙏

CSSフルスクラッチに挑戦

1つのサイト全てのCSSを1から書くのは、今回が初めてでした。 フィヨルドブートキャンプの課題でも、CSSを書いたりしたのですが、CSSのクラス名命名から何から行ったのは初めてなので、結構学ぶことが多かったです。

CSSのクラス名の命名は、BEMを使うと簡単に決まる

BEMとは、Block, Element, Modifierの略称です。

  • Block = 親が何か
  • Element = 要素名
  • Modifier = 状態など

BEMは住所みたいな感じで、使うことで間違ってCSSのクラス名が被ることを避けてくれたり、再利用可能になったりします。

BEMについて詳しくは以下の記事を参考にしたり、フィヨルドのデザイナーのmachidaさんにレビューしていただきながら作成しました。

https://qiita.com/Takuan_Oishii/items/0f0d2c5dc33a9b2d9cb1

CSSのクラス名について、属性を与える場合はBEMではなく .a-XXX, .is-XXX を使い分けると良い

フィヨルドブートキャンプでスクラムをしている際、デザインはほとんど担当してきませんでした。

そのため、ソースコードを読んでいる際、CSSのクラス名で .a-XXX, .is-XXX というのを度々見かけて、 「なんだこれは?」と思っていました。

実際にCSSを書く段階になったところで理解できました。

a-XXX は「オブジェクトが "何" であるか」(箱なのかフォームなのかカードなのか)を表したものです。(一つしか存在しない)

is-XXX は「オブジェクトの状態」(危険なのか、安全なのか)を表したものであることを表しています。

これがわかってからはスッキリ書けるようになりました。

命名については、@sanfrecce_osaka さん(フィヨルドブートキャンプの先輩)が教えてくださった以下のサイトが大変役に立ちました。 ありがとうございます🙇‍♂️

http://sparkle-day.com/weblog/make-website/497

レスポンシブ対応

主に、スマホサイズ(979以下)とPCサイズ(980以上)の時のCSSを作成しました。 ※細かいところのデザインを修正するために微調整は行っています。

@media screen and (max-width:979px)
  // スマホサイズ
@media screen and (min-width:980px)
  // PCサイズ

スワイプ機能の実装に挑戦

Swiper.jsを利用して実装しました。

以下サイトを参考にすれば、簡単に実装できました。

https://www.willstyle.co.jp/blog/724/

WebpackerでJS、CSSライブラリの読み込みに挑戦

大変苦労しました。 JS、CSSライブラリの読込先が別なことが肝だなと思いました。 もしかしたらもっと上手くやる方法があるのかもしれません。

Webpackerなので最終的には一つにまとめるのですが、 Railsの上にあるVue.jsの中で使っているせいで、少し複雑になっています。

以下のように、ひとえにライブラリと言っても、「JSライブラリ」、「JSライブラリ付属のCSS」、「Vue.js内のJSライブラリ」という3つに分かれ、別々のところに別々の書き方をしなければなりません。

- Rails
  - Webpacker
    - CSS(application.css or application.sassで設定)
    - JS(application.jsで設定)
    - Vue.js
      - JS(個別のvueコンポーネントで設定)

しかも、微妙に書き方が違っています。 もしかしたらWebpackerへの読み込み設定を変更する事で、統一できたりするのかな?とも思い調べましたがよくわかりませんでした。

参考:外部ライブラリ読み込みの書き方

参考までに、私の書き方を載せておきます。

JSライブラリ付属のCSS

JSライブラリ

Vue.js内のJSライブラリ

テーブルのレスポンシブ対応

以下のサイトを参考に、テーブルのレスポンシブ対応を行いました。

https://cotodama.co/table_responsive/ https://www.webcreatorbox.com/tech/responsive-table

色々やり方はあるみたいなのですが、私の場合は表示する項目が違うことから、 PC版とスマホ版で別の要素を表示する実装方法をとりました。

# イメージ
- テーブルのカラム
  - PC版の要素
  - PC版の要素
  - PC版の要素
  - スマホ版の要素

iPhoneでみた時に画像が横向きに表示されてしまった

iPhoneからアップロードしたJPEG写真が横向きになる問題(EXIF, Orientation)をみたところ、EXIFのOrientationがおかしいとのことです。

簡単に修正を行い、横向きにならなくなりました。

タブの押してる感を出した

タブの押してる感を出したいな〜ということをツイッターでつぶやいていたところ、

親切な@yrinda_ さん, @sea_baya さん が助けてくださいました。 ありがとうございます🙇‍♂️

ツイートを参考に、実装させていただいたところ、見事解決しました👍

CSSのレイアウトに関するディレクトリ構成を「Atoms + Blocks」で作成

初めはAtomic Designに挑戦してディレクトリを分けたりしたのですが、最終的には「Atoms + Blocks」に落ち着きました。

採用した一番の理由は、どこに何があるか調べるのが容易だったためです。

- atoms
- blocks
  - note
    - note.sass
    - note-show.sass
    - ...
参考:実際のディレクト

https://github.com/s4na/twi-note/tree/master/app/assets/stylesheets

Rails

ユーザー認証機能の追加

Devise, OmniAuthを利用して実装しました。

セキュリティの観点から、routes, controllerで不要な機能を無効化するのが肝だと感じた

CSRF対策

https://github.com/s4na/twi-note/pull/97

OmniAuth gemを利用する際は、外部Gemとcontroller側でセキュリティの設定が必要なようです。 ※設定するように、GitHubのセキュリティアラート(The request phase of the OmniAuth Ruby gem is vulnerable ...)も発生します。

対策に必要なGemはこちら(OmniAuth - Rails CSRF Protection)です。

Twitter Gemの利用に挑戦

探していたらGemがあったので、APIではなくGemを利用しました。

利用方法については、こちらの記事を参考に進めていきました。

qiita.com

下記サイトにアクセスして、Developer登録を行います。

https://developer.twitter.com/content/developer-twitter/ja.html

Twitter APIは利用申請の承認に時間がかかるという話を聞いていたのですが、すぐに使えるようになりました。

ただ、サイト毎にオリジナルの英文数百文字×5本ほど書く必要があるので、英語を書くのが苦手な人は、ここで苦労するかもしれません。 私は苦労しました😢

JS/Vue.js/API

Twitter連携でWrite権限を外す

Twitter Developerのサイトで、認証したユーザーのアカウントを編集できる、Write権限をオフにしました。 ※デフォルトはオンです。

これは重要な設定だと思いました。

ユーザーからしたら「s4naという名前も聞いたこともない個人開発者に、ツイッターのフォロー・アンフォローを操作できる権限を与えたくないな・・・」という風に考えるのでは?と思ったからです。

ユーザーの不安を減らすため、必要がないのでオフにしました。

Twitter APIのID&SECRETの環境変数を設定

Heroku本番環境での .env ファイルの運用について悩んでいたところ、ツイッターではくどーさんよりアドバイスをいただきました。ありがとうございます!

アドバイスを元に、Herokuサーバーに .env ファイルを置こうとするのではなく、環境変数を設定しました。

環境変数の設定については、以下サイトを利用しました。 シェルのコマンドで、簡単に設定することができます。

https://qiita.com/kazuhikoyamashita/items/2c3c31155e98675f780f

フロントエンドをVue.jsで実装

フィヨルドスクラムでVue.jsのコードを書いていたので、APIとのやりとりはなんとか実装できました。

https://github.com/fjordllc/bootcamp/pull/1230

Vue.jsのコンポーネントの設計、とても難しい

Roppongi.vueで似たような事例のLTを聞いていたこともあり、

https://docs.google.com/presentation/d/1wwikvKkwCT9p8AeR73UhzGMw6WTLMwT4CnxV8tGtXjI/edit#slide=id.gcb9a0b074_1_0

「進○ゼミで聞いたやつだ!!」というノリで、 自作コンポーネントに対してv-modelで良い感じ実装しようとしてみました。

ところが、既存のv-modelであるtextareaに対して、更にその親のv-modelを継承しようとすると、警告が発生して上手く実装することができませんでした。残念です😢

ただ、その過程でリファクタリングしていたおかげで、コードを書き始めた初期の 「親から子にメソッドと渡して、そのメソッド内で子から親の変数を参照する」みたいな、 ツラミのある実装は回避できるようになったので、成長を実感しました。

もっと上手くなりたい・・・

JavaScriptのClassにJestでテストを追加

JavaScriptのテストライブラリ、たくさんあってどれにしようか迷っていたのですが、Sendagaya.rb@tkawaさん@fukajunさんに教えていただいた、Jestを採用しました。ありがとうございます!

公式のドキュメント通りに作業すれば、素直に追加できました。

https://jestjs.io/docs/en/getting-started

Sendagaya.rb、良い勉強会です!! 毎週月曜日行っていて、私はここ数ヶ月、毎回参加しています!!

ドラッグ&ドロップを実装

本アプリでは、検索して取得したツイートを移動する機能があります。

@mpywさんに教えていただいた、Vue.Draggableを選択しました。

外部ライブラリ利用は、ドキュメントを読めば簡単に実装できて良いですね。。。

Datetime型が落とし穴、Datetime pickerの導入

Datetime pickerとして、vue-datetimeというライブラリを追加しました。

結論からいうのですが、すっごいハマりました。。。

どういうことかというと、JavaScriptのDate型、moment.jsのdatetime型、LuxonのDatetime型がそれぞれ別物だからです😢

特にLuxonのDatetime型のformatはISO 8601 and RFC 2822という規格で定められており、年月日の表現が少し特殊です。

Formatting - Luxon

例えば日時は英単語の頭文字を取って、 yyyy/mm/dd hh:mm で表したいところですが、 yyyy/LL/dd HH:mm になります。

私は始め yyyy/mm/dd hh:mm と記載していたため、月と時間がズレるという現象が起きました。

時間がズレたのに気づいた時、Herokuのサーバーはアメリカにあるため、 タイムゾーンがズレているからだと勘違いして、コードの奥深くまで探検しにいってました。

ところが、フロント側まで遡ってみても、データは日本時間で、 「あれ?おかしいな?・・・もしかして!!」と思い、勘違いに気づきました😢

また、 yyyy/mm/dd hh:mm 形式で日時を管理していた場合、細かなところで変換を噛ませないといけなかったりして大変でした。

CI編

GitHub ActionsでCIに挑戦

GitHub Actionsは2019年11月頃に正式リリースしたばかりです。

2019年11月13日の Ebisu.rb#26 でも話があり、@yasuhirokiさんというGitHubのリポジトリGitHub Actionsの全コマンド?を試されている方がLTしていて、面白そうだな〜と思い実装してみることにしました。

結構簡単に実装できた。 ・・・と思ってました。

ハマりポイント

実は、途中までCIが機能していないことに気づいていませんでした。 GitHub Actionsはコマンドに対する戻り値が0かどうかで判断しています。 例えばrubocopは0~9が正常なのでそのままだとエラーになったりします。

私は /bin/lint にlintのシェルを作成していたこともあり、CI上でシェルをそのまま叩いていました。 その場合、判定される戻り値はシェルの戻り値なので、一番最後の行の結果だけになります。 そのため、エラーが検知できていませんでした。

#!/bin/bash

bundle exec rubocop -a

# 👇もしここで失敗しても、正常終了になる
bundle exec slim-lint app/views -c config/slim_lint.yml
bin/yarn eslint app/javascript
bin/yarn eslint test/javascript

GitHub Actionsを通して、CIについて理解が深まりました💪

GitHub Actionsのキャッシュ機能追加

CIの時間を待つのが嫌だったので、GitHub Actionsのキャッシュ機能を使いたいな〜とつぶやいていたところ、みかみんさんさんにサイトをご紹介していただけました。

記事を紹介してくださったみかみんさんさん、途中サポートしてくださったmikkameeeさん、記事を作成したVincent Voyerさん本当にありがとうございました🙇‍♂️

記事を参考にしたら基本的に動いたのですが、追加で実装している部分で少し躓いたのでので書いておきます。

躓いたところ
Chromeのインストール

前設定ではCabybaraでシステムテストを行うために、Chromeのインストールを行なっていました。 今回それを写したところ、sudoを追加してほしいとメッセージが表示されるようになったので、追加しました。

Jestの実行

今回Jestでテストを実行しています。ローカルでは問題がなかったのですが、サーバー上で行うときはGem内のテストも実行してしまいそうになりエラーになったので、 testPathIgnorePatterns にvenderを追加しました。

参考

A Rails and PostgreSQL setup for GitHub actions (CI)

GitHub

Dependabotを設定

前にOmotesando.rb保立さん(@purunkaoru)Dependabotからの脱却というDependabotの運用についてLTをしていたので、そちらを参考にさせていただきました。

内容を簡単に説明させていただきますと、以下のようになります。

  • Dependabotは依存関係のあるライブラリがアップデートされると、PRを投げてくれるサービス
  • でも、導入するとPRをたくさん投げすぎて人件費というコストがかかる
  • Dependabotの設定変更で、かかるコストを減らすことができる
  • 具体的なやり方は、Dependabotの設定を「セキュリティアップデートのみ」に変更すること

今回はこのLTを参考に、「セキュリティアップデートのみ」の設定にしました。

CIにバッジがあると、本物のOSSっぽさが出る

READMEにGitHub Actionsのpassing badgeを追加

CIの設定を毎日実行するように変えた上でこのバッジを追加することで、(テストが書かれている動作については)アプリがちゃんと動く証明ができるようになりました。

f:id:s4na:20200129115521p:plain

issue率表示

https://github.com/s4na/twi-note/pull/162

こちらのサイトで作成しました。

https://isitmaintained.com/

f:id:s4na:20200129115548p:plain

ツイートボタンの埋め込み追加

https://github.com/s4na/twi-note/pull/192

下記サイトを参考に、README.mdにツイートボタンを追加しました。

github.com

f:id:s4na:20200129115341p:plain

タスク管理編

タスクは小粒にすることで、進み続けることができる

フィヨルドブートキャンプの進捗報告会で、「タスクが小さい方が良い」という話を聞きました。 どうしてもタスクが大きい場合は、大きいタスクをそのまま設定するのではなく、「タスクを分割するタスクを設定してもいい」ということでした。

「とりあえずフィードバックしていただいたことは実践しよう💪」と思い、実践してみたところ、嬉しい効果がいくつかありました。

実践したことで、「次に何をするか迷うことが減った」ように感じました

大きいタスクを実行しようとしていた時は「大きいタスクの中にはまた細かいAとBというタスクがあったりします。その場合、AとBから先に行う方を選ぶ」という作業が発生します。 小さいタスクになったことで、こういった迷いが減りました。

毎日何かしらのタスクを終了させることができ、毎日進捗を報告することができました

フィヨルドブートキャンプでは、学習を行った日は日報を書くという決まりがあります。 悩んでいることなどについて、メンターが相談に乗ったり、アドバイスのコメントを書くためです。 また、明言はされていませんが「個人としてその日1日の進捗に達成感を感じるためという効果もあるのかな?」と思っております。 毎日何かしらのタスクが終了することで、達成感があり、開発が長続きしても、気持ちが落ちることはほぼありませんでした。

「いつかやる」リストを作ることで、本当に必要なことだけ取り組める

本サービスを開発するにあたって、「無限にやりたいことが出てくる」という問題がありました。

今回が「初めての個人開発」であったこと。 「就職活動で利用するポートフォリオ」であったことも合間って、無限にやりたいことが出てきました。

そして同時に、やらないことが出てきました。

以下、やらなかったことリストです。

・・・などなど合計で30件以上になりました。

やりたいことは無限に出てくる・・・でも、サービス開発は、どこかで区切りをつけなければなりません。

今回私は、サービスのコアの価値に寄与するかどうかで判断しました。

具体的には、エレベーターピッチの問題に対する解決です。

はてなブックマークの記事をみて、自分のサービスに適応したいものを見つけたら、とりあえず「いつかやる」リストに追加する

この施策は大変上手く機能しました。

かねてより、はてなブックマークを見たり勉強会に参加すると、度々面白そうな話を見聞きすることがあったので、 自分のサービス開発に利用できそうなことをまとめていました。

その結果、本記事でも紹介しているように、色々なツールやライブラリを参考にすることができました。 そのおかげか、サービスの開発において「どんなツールがあるのかわからないので調べる」みたいなところで詰まることも少なかったように感じます。

コツコツ情報を貯めるの大事ですね。

備考

まだベータ版ですが、以下のようなデザインツールの共有サイトもあるみたいなので、参考にしてみても良いかもしれません

https://www.designvalley.club/

一人でアジャイルを回して学んだこと

https://github.com/s4na/twi-note/wiki

サービス開発において、一人ふりかえりミーティングを行なっていました。 フィヨルドブートキャンプのスクラムという課題でアジャイル開発手法のスクラムというやり方で開発していたので、 それを参考に、一人でアジャイルしていました。

毎週何をやるのか明確になり、一人で開発し続ける際にやる気が継続できました。

納期の管理に失敗した

失敗したことも正直に書こうと思います。 全体としての個人的な納期は全然間に合いませんでした。

納期が遅延したことの主な原因としては、「初めて自分の作ったサービスなので、機能をモリモリ追加したくなったこと」でした。 エンジニアリング組織論で、「エンジニアリングとは不確実性」「不安なタスクから先にこなす」という話を読んでいました。 そのため、理性としては「不確実なこと」「不安なこと」から取り組んで、後半は納期が見えるようにしていくはずでした。 しかし、後半になってからも色々挑戦したいことがたくさん出てきて、そこに時間を投入してしまいました。

また一方で、1イテレーション毎の納期の管理は成功しました。 「今週はこういうことをやろう」と決めたら、守ることができました。

個人開発で、ストーリーポイントは導入しなくて良かった

フィヨルドブートキャンプのスクラムでも使っていたため、その流れで導入してみました。 正直、「必要なら取り掛かるし、あとでもいいならやらないだけ」なので、ポイントを降ったことでの効果は感じませんでした。

仕様変更が多く、突発的に要望がどんどん増えていく場合、あまり必要ないのかな?とも思いました。

ストーリーにタグ付けするのは良かった

「バグの修正については、早く取り組みたい」と思ったりしました。 必要に応じてタグつけしていくのは「テンションを上げる」という面でメリットを感じました。

参考:一人ふりかえりの記録について

WikiのPagesに記録を作成しております。

github/s4na/twi-note/wiki

今後の課題

個人的に思っている課題

Slackとの連携

勉強会によっては、Slack上で勉強会の情報をやりとりしているところがあります。 そのため、Twitterと同様、Slackとの連携も行いたいと考えています。

Connpassの開始終了時間との連携

Connpassの利用規約確認の上、スクレイピングを行うことで、 勉強会の「勉強会ハッシュタグ」「開始時間」、「終了時間」の取得を行いたいと考えています。

時間予約

数が増えていくと勉強会終了後、毎回人が手動でノートを作成するのが手間です。 あらかじめ「勉強会ハッシュタグ」「終了時間」を入力しておくと、時間になったらノートを作成してくれる機能があると便利そうです。

meguro.rbという勉強会で使用して出てきた課題

2019/1/31、meguro.rbという勉強会に参加した際、実際に使ってみました。 その際、課題が見つかったので、追記させていただきます。

一時保存機能が欲しい

通信が不安定なところでノートを作成していた時に「データがなくなってしまうのでは?」と心配になる時がありました。

ツイートの並びが昇順になっていないバグがあった

ツイートの並びを昇順に設定していたつもりが、ソートが上手くいっていませんでした。

👉対応済み

分単位だと細かくて時間指定するのが面倒。10分単位など、もっとざっくり指定したい

1分単位だと、スマホで指定する際移動するのが大変でした。 10分、15分単位で選べると便利だなと思いました。

👉対応済み

「誰がツイートしたか」という情報が必要だと感じた

どういう文脈での発言なのか、経緯が知りたいと思うことがありました。 誰がツイートしたかの情報を書いても良いのかな?と思いました。

👉対応済み

勉強会ハッシュタグを保存するため、前回の検索情報を保存したい

一時保存してから再度編集しようとした時、 前回の検索情報がなく、再度検索の設定するのが手間だと感じました。

👉対応済み

一度に表示されるツイートの表示件数を絞りたい

現状、ツイートが100件縦長に表示される状態になっています。 これだと作業しづらさを感じるため、表示件数を絞りたいと思いました。

やっておけばよかったと思ったことについて

本ブログにも色々書いているのですが、他に書いていないことについて書いていきます🖋

最終的な成果発表の形を意識しておけば良かった

もちろんサービスづくりなので、一番大事なのはより良いサービスを作る事だと思うのですが、 私は目標の一つとして、最終的な成果報告の形をブログにしようと決めていました。

しかし、それを作り始めたのが結構終わりの方だったので、作り始めてから「あの時どうしたんだっけ?」と調べる事が多かったです。 そのため、早い段階で「どんな形でブログとして残してこう?」というのを考えておけばよかったな〜と後悔しています。

さいごに

本サービスの開発において、大変多くの方にサポートしていただきました。 フィヨルドブートキャンプのkomagataさん、machidaさん、並びにメンター・アドバイザーの皆さま、Rubyコミュニティの皆さま、きゅきゅさん主催の弱々エンジニア会の皆さま、ツイッターの皆さま、多くの方にご支援いただいたことで、サービスを完成させることができたと思っております。

この場をお借りして御礼申し上げます。 本当にありがとうございました。

今後もサービスの改善など、やっていきたいと思いますので、その際はご相談など乗っていただけると幸いです。

また、色々な人にお手伝いいただいたご恩に関しては、後進の方にも繋げていきたいと思っております。 「サービスの開発について聞いてみたいことがある!」という方がいらっしゃいましたら、 お気軽にご連絡いただければと思います。

連絡先:Twitter@s4na_penguin

以上、貴重なお時間頂きありがとうございました。

GitHubのOpen Souce Guideを読んで、オープンソースに貢献したいな〜と思い、誤字を探してコントリビュートした話

目次

この記事の内容について

GitHubのOpen Souce GuideにPRしてマージしていただいたことについて、 そのやり方と方法について書いていきたいと思います👍

github.com

f:id:s4na:20200224200853p:plain

ことのはじまり

先日はてなブックマークのテクノロジーランキングを見ていたところ、GitHubのOpen Souce Guideというのが載っていました。

b.hatena.ne.jp

※日本語版もあります

opensource.guide

サイトの内容としてはオープンソースについて

貢献する方法、始める方法、コミュニティの作り方などなどです。

そして、このサイト自体もオープンソースでありコントリビュートを求めているという話が書いてあったので、

「公開したばかりなら、なにかしら貢献できるはずだ!よし!やろう!!」と決めて、コントリビュートすることにしました。

PRネタ探し

オープンソースへの一番簡単な貢献が、誤字脱字だと聞いたことがあり、過去にしたこともあるので、今回もその方法を取りました。

まずは日本語の言語ファイルがどこにあるか調べようと思いました。

サイトにアクセスして

opensource.guide

言語設定を日本語に切り替え

f:id:s4na:20200224201441p:plain

表示されたディレクトリを元にして、

f:id:s4na:20200224201538p:plain

言語設定ファイルの保存先を特定

github.com

特定したファイルは31行しかないので、他にも設定ファイルがありそうだなと判断しました

f:id:s4na:20200224201851p:plain

他のページの文字を検索すると、別のディレクトリで管理されていることが判明しました

f:id:s4na:20200224202018p:plain

f:id:s4na:20200224202019p:plain

特定したディレクトリを元に、誤字を捜索

※特定したディレクト

opensource.guide/_data/locales/ja.yml

opensource.guide/_articles/ja/*

ディレクトリの中で誤字を発見したのでコミットを作成しました

f:id:s4na:20200224202231p:plain

f:id:s4na:20200224202230p:plain

PRの作成

ここでやっとPRということですが・・・

オープンソースにPRを作成する場合、英文なことが多いです。 (稀に例外もありますが)

私は英語が苦手です。 なのでどういう文面を送って良いか悩みました。

今回は過去に誤字についてPRを送っている方を調べて参考にさせていただきました。

リポジトリを「Japan」で検索したところ、日本語の誤訳についてPRを送っている方がいらっしゃいました。

f:id:s4na:20200224202747p:plain

単語を検索して、これで良さそうか確認して、PRを作成します。

今回の場合、PRを送るためのガイド(Contributing to Open Source Guides)が用意されていたので、そちらも確認します。

f:id:s4na:20200224202932p:plain

github.com

そして、優しいコメント共に、マージしていただくことができました🎉🎉🎉

ありがとうございます🙇‍♂️

f:id:s4na:20200224203135p:plain

さいごに

誤字脱字に関するPRばかりではありますが、最近少しずつオープンソースにPRを送ってマージしていただけることが増えてきて嬉しいです😄

もっともっと腕を上げていきたい💪💪💪

Webmockを使って、OAuth認証のあるTwitter Gemの処理をモックする

昨日フィヨルドブートキャンプでペアプログラミングを行い、OAuth認証のある(2回通信を行っている)Gemの処理をモックしたのでその内容についてまとめました。

目次

【重要】追記:Header情報にAuthorizationを記述してはいけません

ブログの記事の中にも書いたのですが、Webmockのエラー内容通りに作業を行っているとテストにAuthorization情報を記載してしまう可能性があります。

AuthorizationはBase64というエンコード(暗号化ではない)処理を行ったAPIのIDとSECRETを記載してしまっているので、公開(Gitログに残った状態でGitHubにPushしてしまうなど)してしまうとAPIのID&SECRETが流出してしまいます。 ※私はこのブログで既に一度やらかしてしまいました・・・

十分扱いには注意しましょう。

環境

今回使用する主なRuby, Gemのバージョンです。

まずは実装を用意

モデルに以下内容のファイルを用意しました。

Twitter::REST::Client を作成してから、#search するという簡単な実装です。

app/models/TweetRepository.rb

# frozen_string_literal: true

class TweetRepository
  def initialize(twitter_app_id, twitter_app_secret)
    @twitter_app_id = twitter_app_id
    @twitter_app_secret = twitter_app_secret
  end

  def search(params)
    search_tweets
  end

  private
    def client
      @client ||= Twitter::REST::Client.new(
        consumer_key: @twitter_app_id,
        consumer_secret: @twitter_app_secret
      )
    end

    def search_tweets
      client.search(
        "s4na_penguin",
        count: 2,
        result_type: "recent",
        until: "2020-02-19",
        exclude: "retweets",
        since_id: nil).to_h
    end
end

テスト書きはじめ

下準備が完了したので、テストを書いていきましょう。

まずは失敗しないテストを作成

以下ファイルを作成します。 実行して、結果が正常に終了することを確認します。

test/models/tweet_repository_test.rb

# frozen_string_literal: true

require "test_helper"
require "webmock/minitest"

class TweetRepositoryTest < ActiveSupport::TestCase
  test "#search" do
    TweetRepository.new(ENV["TWITTER_APP_ID"], ENV["TWITTER_APP_SECRET"]).search
  end
end

下準備として、APIのレスポンスを用意

モックする際にレスポンスが必要になります。 以下手順に従って用意しましょう。 ※あとで用意してもいいのですが、一度設定したwebmockをオフにする必要があるので少し複雑になります

  • search_tweetspp search_tweets と編集
  • テストを一度実行
  • コンソールの出力結果をファイルに保存

APIのIDとSECRETを偽物に変更する

【重要】そのまま本番の環境変数を実行してしまうと、外部に流出する恐れがあるので書き換えます。

test/models/tweet_repository_test.rb

    # 変更前
    TweetRepository.new(ENV["TWITTER_APP_ID"], ENV["TWITTER_APP_SECRET"]).search
    # 変更後:引数を変更しています。引数は数が合えばなんでもOKです👍
    TweetRepository.new("ID", "SECRET").search

test_helperにWebmockの設定を追加

test/test_helper.rb

# require "rails/test_help" の下あたりに👇を追加
require "webmock/minitest"

追加した後で実行すると、テストが失敗するようになります。

エラー例

E

Error:
TweetRepositoryTest#test_#search:
WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled.

...長いので以下省略

ここでようやくWebmockを作成

特に制約のないAPIへのモックだと、エラーメッセージに書いてある You can stub this request with the following snippet: より下の行をコピーして貼り付けることでWebmockが完了するのですが、Twitter Gemはそうではありません。

追加してテストしてみると、このようなエラーメッセージが表示されます。

E

Error:
TweetRepositoryTest#test_#search:
HTTP::Error: Unknown MIME type: 
    app/models/tweet_repository.rb:22:in `search_tweets'
    app/models/tweet_repository.rb:10:in `search'
    /test/models/tweet_repository_test.rb:20:in `block in <class:TweetRepositoryTest>'

ではどうすればいいのか?🤔

Twitter Developer公式のガイドによるとheadersの指定がある

Twitter Developer/Authentication

ドキュメントの Example response: によれば、以下のように書いてあればいいそうです。

HTTP/1.1 200 OK
Status: 200 OK
Content-Type: application/json; charset=utf-8
...
Content-Encoding: gzip
Content-Length: 140

{"token_type":"bearer","access_token":"AAAA%2FAAA%3DAAAAAAAA"}

テストの to_return の内容を、以下のように書き換える

書き換えたらテストを実行しましょう。

      to_return(
        status: 200,
        body: { "token_type": "bearer", "access_token": "AAAA%2FAAA%3DAAAAAAAA" }.to_json,
        headers: { "Content-Type": "application/json; charset=utf-8" },
      )

2つ目のエラー「WebMock::NetConnectNotAllowedError」

以下のようなエラーが表示されます。 これは1つ目のエラーとは別のエラーです。

E

Error:
TweetRepositoryTest#test_#search:
WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: GET https://api.twitter.com/1.1/search/tweets.json?count=2&exclude=retweets&q=s4na_penguin&result_type=recent&since_id&until=2020-02-19 with headers {'Authorization'=>'Bearer AAAA%2FAAA%3DAAAAAAAA', 'Connection'=>'close', 'Host'=>'api.twitter.com', 'User-Agent'=>'TwitterRubyGem/6.2.0'}

You can stub this request with the following snippet:

stub_request(:get, "https://api.twitter.com/1.1/search/tweets.json?count=2&exclude=retweets&q=s4na_penguin&result_type=recent&since_id&until=2020-02-19").
  with(
    headers: {
      'Connection'=>'close',
      'Host'=>'api.twitter.com',
      'User-Agent'=>'TwitterRubyGem/6.2.0'
    }).
  to_return(status: 200, body: "", headers: {})

registered request stubs:

stub_request(:post, "https://api.twitter.com/oauth2/token").
  with(
    body: {"grant_type"=>"client_credentials"},
    headers: {
      'Accept'=>'*/*',
      'Connection'=>'close',
      'Content-Type'=>'application/x-www-form-urlencoded',
      'Host'=>'api.twitter.com',
      'User-Agent'=>'TwitterRubyGem/6.2.0'
    })

============================================================
    app/models/tweet_repository.rb:22:in `search_tweets'
    app/models/tweet_repository.rb:10:in `search'
    /test/models/tweet_repository_test.rb:24:in `block in <class:TweetRepositoryTest>'

2つ目のエラーに書いてある通り、コードを追加

【重要】Authorization情報は追加してはいけません。もし本番の環境変数を適応したまま行った場合、エンコードされたAPIのIDとSECRET情報が含まれてしまっています。

エラーに書いてある内容のうち「Authorization」を除いてTweetRepositoryTest.rbに追記を行います。

1つ目のモックと同じく、headersに "Content-Type": "application/json; charset=utf-8" を追記します。

    stub_request(:get, "https://api.twitter.com/1.1/search/tweets.json?count=2&exclude=retweets&q=s4na_penguin&result_type=recent&since_id&until=2020-02-19").
      with(
        headers: {
        'Connection'=>'close',
        'Host'=>'api.twitter.com',
        'User-Agent'=>'TwitterRubyGem/6.2.0'
        }).
      to_return(status: 200, body: "", headers: { "Content-Type": "application/json; charset=utf-8" })
    
    # 👇の上に追加
    TweetRepository.new(ENV["TWITTER_APP_ID"], ENV["TWITTER_APP_SECRET"]).search

3つ目のエラー「NoMethodError: undefined method `fetch' for "":String」が発生

bodyのレスポンスがjsonではなくstringなため、エラーが発生しています

E

Error:
TweetRepositoryTest#test_#search:
NoMethodError: undefined method `fetch' for "":String
    app/models/tweet_repository.rb:22:in `search_tweets'
    app/models/tweet_repository.rb:10:in `search'
    /test/models/tweet_repository_test.rb:34:in `block in <class:TweetRepositoryTest>'

APIのレスポンスを修正

ここで「下準備として、APIのレスポンスの用意」で作成したAPIのレスポンスを使います!

bodyに「APIのレスポンス + 末尾に .to_json 」を追記します。

長くなるのですが、まずはテストのグリーンを目指しているので保留します。

      to_return(status: 200, body: {:statuses=>
        [{:created_at=>"Tue Feb 18 17:48:37 +0000 2020",
          :id=>1229825058098466816,
          :id_str=>"1229825058098466816",
          :text=>"@s4na_penguin ありがとうございます!",
          :truncated=>false,
          :entities=>
           {:hashtags=>[],
            :symbols=>[],
            :user_mentions=>
             [{:screen_name=>"s4na_penguin",
               :name=>"s4na🐧/ Nabetani そうだ!Rubyを書こう!",
               :id=>1072823223190835201,
               :id_str=>"1072823223190835201",
               :indices=>[0, 13]}],
            :urls=>[]},
# 長い為省略

       :search_metadata=>
        {:completed_in=>0.016,
         :max_id=>1229825058098466816,
         :max_id_str=>"1229825058098466816",
         :next_results=>
          "?max_id=1229693053872431103&q=s4na_penguin%20until%3A2020-02-19%20exclude%3Aretweets&count=2&include_entities=1&result_type=recent",
         :query=>"s4na_penguin+until%3A2020-02-19+exclude%3Aretweets",
         :refresh_url=>
          "?since_id=1229825058098466816&q=s4na_penguin%20until%3A2020-02-19%20exclude%3Aretweets&result_type=recent&include_entities=1",
         :count=>2,
         :since_id=>0,
         :since_id_str=>"0"}}.to_json,
      headers: {
        'Content-Type': "application/json; charset=utf-8",
      })

テストにassertの追加

テストの最下行を以下のように書き換えてテストを実行しましょう!

テストが成功すれば、グリーンです。 ※なぜ tweets[:statuses].first[:text] という書き方をするのかわからなければ、 ptweets, tweets[:status] ・・・という感じで出力してみるのがおすすめです👍

before

tweets = TweetRepository.new(ENV["TWITTER_APP_ID"], ENV["TWITTER_APP_SECRET"]).search

after

tweets = TweetRepository.new(ENV["TWITTER_APP_ID"], ENV["TWITTER_APP_SECRET"]).search
assert_equal "@s4na_penguin ありがとうございます!", tweets[:statuses].first[:text]

リファクタリング

長いところをリファクタリングしていきましょう。 今回利用しているのは testTwitter Gem側で ID なので、最終的に以下のように省略できます。

※IDが必要なことは消すとエラーになり、追加したら正常終了していたので推測しました。

body: { statuses: [{ id: 1229825058098466816,text: "@s4na_penguin ありがとうございます!" }] }.to_json,

完成したコード

test/models/tweet_repository_test.rb

# frozen_string_literal: true

require "test_helper"
require "webmock/minitest"

class TweetRepositoryTest < ActiveSupport::TestCase
  test "#search" do
    stub_request(:post, "https://api.twitter.com/oauth2/token").
      with(
        body: { "grant_type"=>"client_credentials" },
        headers: {
        "Accept"=>"*/*",
        "Connection"=>"close",
        "Content-Type"=>"application/x-www-form-urlencoded",
        "Host"=>"api.twitter.com",
        "User-Agent"=>"TwitterRubyGem/6.2.0"
        }).
      to_return(
        status: 200,
        body: { "token_type": "bearer", "access_token": "AAAA%2FAAA%3DAAAAAAAA" }.to_json,
        headers: { "Content-Type": "application/json; charset=utf-8" },
      )

    stub_request(:get, "https://api.twitter.com/1.1/search/tweets.json?count=2&exclude=retweets&q=s4na_penguin&result_type=recent&since_id&until=2020-02-19").
      with(
        headers: {
        "Connection"=>"close",
        "Host"=>"api.twitter.com",
        "User-Agent"=>"TwitterRubyGem/6.2.0"
        }).
      to_return(
        status: 200,
        body: { statuses: [{ id: 1229825058098466816, text: "@s4na_penguin ありがとうございます!" }] }.to_json,
        headers: { 'Content-Type': "application/json; charset=utf-8" }
      )

    tweets = TweetRepository.new("ID", "SECRET").search
    assert_equal "@s4na_penguin ありがとうございます!", tweets[:statuses].first[:text]
  end
end

test/test_helper.rb

# frozen_string_literal: true

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

app/models/tweet_repository.rb

# frozen_string_literal: true

class TweetRepository
  def initialize(twitter_app_id, twitter_app_secret)
    @twitter_app_id = twitter_app_id
    @twitter_app_secret = twitter_app_secret
  end

  def search(params)
    search_tweets
  end

  private
    def client
      @client ||= Twitter::REST::Client.new(
        consumer_key: @twitter_app_id,
        consumer_secret: @twitter_app_secret
      )
    end

    def search_tweets
      client.search(
        "s4na_penguin",
        count: 2,
        result_type: "recent",
        until: "2020-02-19",
        exclude: "retweets",
        since_id: nil).to_h
    end
end

備考

誤字脱字などがあればこちらにご連絡いただけると助かります🙇‍♂️

👉Twitter@s4na_penguin