TECH PLAY

株式会社スタメン

株式会社スタメン の技術ブログ

238

目次 はじめに Reactを使用したフォーム設計パターンについて React Hook Formとは ? React Hook Formの基本機能の紹介 React Hook Formのユースケース 最後に はじめに こんにちは、株式会社スタメンでエンジニアをしています、 ワカゾノ です。 Rails、Reactを使用して、弊社プロダクト TUNAG の機能開発を行っています。 直近のプロジェクトにおいて、Reactでフォームを実装する必要がありました。 要件としては、下記のようになります。 新規作成時、編集時のフォームをerbから、Reactへリプレイス 1画面毎に3 ~ 6つのフォームが存在、それを10数画面分実装 各フォームの入力値に応じて画面の表示を動的に変更する 例) 選択しているラジオボックスにより、フォーム要素の表示、非表示を切り替える 各フォームに細かいバリデーションが必要 例 ) セレクトボックスの組み合わせによっては、同時に選択できない プロジェクト自体はReduxを使用してstateの管理を行っているため、 onChangeイベントを用いて、フォームの入力を元にUIに関するstateを管理することも出来ます。 しかしフォームの規模が大きいほど、UIに関連するstate管理が煩雑になり、stateを管理するための定型コードの記述量が増えるという問題があります。 また入力の度にレンダリングが走り、パフォーマンスの問題も懸念されます。 上記のような問題点を解消するために、 React Hook Form というライブラリを使用してフォーム実装を行うことになりました。 今回は「React Hook Formとは」、「React Hook Formの実際の使い方」について紹介していこうと思います。 Reactを使用したフォーム設計のパターンについて React Hook Formの説明に入る前に、Reactを使用したフォーム設計パターンについて紹介します。 Reactではフォーム実装において、2つのパターンが存在します。 Controlled Component Reactのstateを唯一信頼出来る情報源(single source of truth)とし、フォームをレンダーしているReactコンポーネントが、後続のユーザー入力でフォームに起こるイベントを制御する Uncontrolled Component フォームデータをDOM(ブラウザ)自身が制御する そもそもHTMLでは input 、 textarea 、 select のようなフォーム要素は自身で状態を保持しています。 Uncontrolled Componentによるフォーム構築は、React固有の実装というよりは、ネイティブ(ブラウザ)の実装に近い形になります。 React公式 では、Controlled Componentの使用が推奨されています。 React + Reduxの環境下で、Controlled Componentのライブラリとして有名な redux-form では、Reduxでの状態管理を元にフォーム構築を行います。 しかし、 Redux公式 では、「経験則に基づくと、Reduxでフォームの状態を管理する必要はないと考えられる」という記載があります。 これらの主張に則れば、「フォームに関する状態をReduxで管理せず、useStateなどのローカルstateを使用して、Controlled Componentパターンでフォームコンポーネントを実装する」という手法がベストプラクティスのように思われます。 しかし、上述したような複雑なフォームのstate管理が必要かつ、大規模なフォームを実装する上で、ローカルstateだけでフォームを実装することは大変です。 そこでReact Hook Formが登場します。 React Hook Formとは ? 公式サイト では、「シンプルかつ、拡張性のある、使い勝手の良いフォームバリデーションライブラリ」という説明がされています。 Performant, flexible and extensible forms with easy-to-use validation. React Hook Formは、React16.8.0から導入されたhooksの仕組みを利用したフォームライブラリです。 Uncontrolled Componentsのパターンを採用しており、フォーム毎の参照( ref )をカスタムフックス( useForm )に登録することで、フォームの状態をコントロールします。 useForm が提供するAPIである、 register を使用して、各入力フォームの要素の参照を登録します。 React Hook Formの利点として、公式で紹介されているものとしては以下のような点が挙げられます。 state管理などのコードの記述量を減らすことが出来る パッケージが軽量 Unontrolled Componentsのパターンを採用しており、レンダリング回数を減らすことが出来る 今回の要件で言えば、特に下記のような問題点に対して、アプローチ出来るため、React Hook Formを選定するに至りました。 10数画面分のフォームを実装するにあたり、各フォームで入力変更を検知するstate、アクションなどを定義していくことが大変 単純なコードの記述量が増える メンテナンス性が低下する テキストエリアなど、長い文章を入力する際にレンダリング回数を減らすことが出来る それでは実際にコードを書いていきながら紹介していきます。 React Hook Formの基本機能の紹介 簡単なデモを作成して、基本機能を紹介していきます。 動作環境は下記になります。 node v12.16.2 yarn v1.22.5 react v17.0.2 typescript v4.1.2 react-hook-form v7.1.1 create-react-appにて新規Reactプロジェクトを作成し、 yarn、npmなどのパッケージマネージャを使用して、 React Hook Formをプロジェクトにインストールします。 npx create-react-app react-hook-form-sample --template typescript yarn add react-hook-form 画像のような簡単な入力フォームを実装し、React Hook Formについて説明していきます。 SampleForm.tsx import React from 'react' import { useForm, SubmitHandler, SubmitErrorHandler } from 'react-hook-form' type ValuesType = { name: string, introduction: string, department: 'product' | 'sales' | 'marketing' | '' programingLanguage: 'golang' | 'ruby' | 'javascript' | '' } const SampleForm: React.VFC = () => { const { register, watch, handleSubmit, formState } = useForm<valuesType>( { mode: 'onSubmit' , reValidateMode: 'onChange' , defaultValues: { name: '' , introduction: '' , department: '' , programingLanguage: '' } } ) const handleOnSubmit: SubmitHandler<valuesType> = (values) => { console.log(values) } const handleOnError: SubmitErrorHandler<valuesType> = (errors) => { console.log(errors) } return ( <wrapper> <form onSubmit= { handleSubmit(handleOnSubmit, handleOnError) } > // テキスト項目 <label htmlFor= 'name' >Name</label> { !!formState.errors.name && <p> { formState.errors.name.message } </p> } <input id= 'name' type= "text" isError= { !!formState.errors.name } // エラー時にborderの色を変更するためのprops { ...register( 'name' , { required: '* this is required filed' } ) } /> // テキストエリア項目 <label htmlFor= 'introduction' >Introduction</label> { !!formState.errors.introduction && <p> { formState.errors.introduction.message } </p> } <textarea id= 'introduction' isError= { !!formState.errors.introduction } { ...register( 'introduction' , { required: '* this is required filed' , minLength: { value: 10, message: '* please enter at least 10 characters' } } ) } /> // セレクトボックス <label htmlFor= 'department' >Department</label> { !!formState.errors.department && <p> { formState.errors.department.message } </p> } <select id= 'department' isError= { !!formState.errors.department } { ...register( 'department' , { required: '* this is required filed' } ) } > <option value= '' hidden>please selecting...</option> <option value= 'product' >Product</option> <option value= 'sales' >Sales</option> <option value= 'marketing' >Marketing</option> </select> // セレクトボックス { watch( 'department' ) === 'product' && <> <label htmlFor= 'programing-langage' >Programing Language</label> <select id= 'programing-language' { ...register( 'programingLanguage' ) } > <option value= '' hidden>please selecting...</option> <option value= 'golang' >Golang</option> <option value= 'ruby' >Ruby</option> <option value= 'javascript' >Javascript</option> </select> </> } // 送信ボタン <button type= "submit" disabled= { !formState.isDirty || formState.isSubmitting } > Click </button> </form> </wrapper> ) } useForm useForm ではオプショナルの引数を渡すことで、フォーム全体のバリデーションのタイミングを制御したり、フォームの初回レンダリング時のデフォルト値を設定することができます。 const { register, watch, handleSubmit, formState } = useForm<valuesType>( { mode: 'onSubmit' , // バリデーションが実行されるタイミング reValidateMode: 'onChange' , // 再度バリデーションを実行するタイミング、onChangeの場合は、入力の度にバリデーションが走る defaultValues: { // 初回レンダリング時のフォームのデフォルト値 name: '' , introduction: '' , department: '' , programingLanguage: '' } } ) mode で onChange を指定することは、 this often comes with a significant impact on performance と記載があるようにパフォーマンスへの懸念から推奨されていません。 register input や select 要素をReact Hook Formのバリデーションルールに適用するために、このメソッドを使用します。 第1引数に、登録する参照の名前を設定します。 設定方法によって入力結果をネストしたり、配列で渡すことができます。 register( "name" ) 👉 { name: 'value' } register( "name.firstName" ) 👉 { name: { firstName: 'value' } } register( "name.firstName.0" ) 👉 { name: { firstName: [ 'value' ] } } 第2引数にバリデーションのルールをオブジェクトの形式で渡します。 今回だと required(必須) 、 minLength(最小文字数) を使用しています。 複雑なバリデーション要件が必要になってくる場合などは、 validate オプションを使用すると良さそうです。 handleSubmit 第1引数に、バリデーション成功時のコールバック関数を、第2引数に、エラー時(バリデーションに引っかかった際)のコールバック関数を登録することができます。 今回は成功時のコールバック関数で、フォームからの入力値を受け取りコンソールに出力しています。渡ってくるデータは下記のようになります。 { name: "Takuya Wakazono" , introduction: "I Like React Hook Form So Much!!" , department: "product" , programingLanguage: "javascript" } 失敗時のコールバック関数ではエラーを内容を受け取ることができます。 渡ってくるデータとしては下記のようになります。(すべて未入力の場合) { name: { type: "required" , message: "* this is required field" , ref: "..." } , introduction: { type: "required" , message: "* this is required field" , ref: "..." } , department: { type: "required" , message: "* this is required field" , ref: "..." } , } formState フォーム全体に関するstate(状態)をオブジェクト形式で保持しています。 今回であれば、 isDirty や errors 、 isSubmitting 等が該当します。 isDirty: input要素に入力が合った場合はtrueを返す(ユーザーが何も入力していない場合はfalseのまま) errors: エラーオブジェクトを格納 isSubmitting: 送信中かどうかを判定 watch 入力値を監視し、その値を返します。 主に入力値に応じてフォームのUIを動的に変更する場合などに使用します。 今回はDepertmentを選択する際に、 product を選択した場合のみ、プログラミング言語を選択するフォームをレンダリングするようにしています。 React Hook Formのユースケース これまでにReact Hook Formの基本的な機能を紹介しましたが、 実際は一つのコンポーネント内にフォームをベタに書くことはほとんど無く、 テキストフォーム、テキストエリア、チェックボックスなどの汎用コンポーネントをimportして、フォームコンポーネントを構築することがほとんどであると思います。 そのような場合に、 FormProvider 、 useFormContext を使用して、registerをpropsとして汎用コンポーネントに渡すことで、フォームを構築することが可能です。 pages/components/common/index.tsx import React from 'react' import { UseFormRegisterReturn } from 'react-hook-form' type PropsType = { labelName: string register: UseFormRegisterReturn } export const TextInput: React.VFC = (props: PropsType) => { const { id, labelName, register } = props return ( <> <label htmlFor= { id } > { labelName } </label> <Input id= { id } type= "text" { ...register } /> </> ) } export const Textarea: React.VFC = (props: PropsType) => { const { id, labelName, register } = props return ( <> <label htmlFor= { id } > { labelName } </label> <textarea id= { id } { ...register } /> </> ) } export const SelectBox: React.VFC = (props: PropsType) => { const { id, labelName, register } = props return ( <> <label htmlFor= { id } > { labelName } </label> <select id= { id } { ...register } > <option>選択肢1</option> <option>選択肢2</option> </select> </> ) } export const SubmitButton: React.VFC = () => { return ( <input type= "submit" /> ) } SampleForm.tsx import React from 'react' // 汎用コンポーネントのimport =========================== import NestedSampleForm from './' import { FormProvider, useForm } from 'react-hook-form' type ValuesType = { // ... } const SampleForm: React.VFC = () => { const methods = useForm<ValuesType>() return ( <FormProvider { ...methods } > <Form onSubmit= { handleSubmit(handleOnSubmit) } > <NestedSampleForm /> </Form> <SubmitButton /> </FormProvider> ) } NestedSampleForm.tsx import React from 'react' import { useFormContext } from 'react-hook-form' // 汎用コンポーネントのimport =========================== import { TextInput, Textarea, SelectBox } 'pages/components/common' // ================================================= const NestedSampleForm: React.VFC = () => { const { register } = useFormContext<valuesType>() return ( <Wrapper> <TextInput labelName= 'テキスト項目' register= { register( 'text' ) } /> <TextareaForm labelName= 'テキストエリア項目' register= { register( 'textarea' ) } /> <SelectBox labelName= 'セレクトボックス' register= { register( 'selectbox' ) } /> </Wrapper> ) } 最後に フォームの要件が複雑になるほど、Reactが推奨しているControlled Componentのパターンでは、state管理が大変になり、辛さを感じていたので、シンプルで使いやすいというのはまさにその通りだなと思いました。 今回紹介した以外にも、たくさん機能があるので、今後React Hook Formを使用していく中で、応用的な使用方法など知見が溜まった際は、また紹介させて頂こうと思います! スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 エンジニア募集ページ
はじめに 背景 ActiveRecord::AttributeMethods::Dirtyとは メソッド一覧 メソッド名の変遷 活用に向けた検証 検証に使用したモデル Dirtyの活用例 実現したかったこと/実装例 Dirtyの活用したサンプルコード おわりに 参考 はじめに はじめまして、スタメンでエンジニアをしているショウゴです。普段は、バックエンドグループでRuby on Railsを用いてバックエンドの開発を主に担当しています。 今回の記事では、ActiveRecordのattributeの変更状況を確認できるRailsのActiveRecord::AttributeMethods::Dirtyモジュールの使い方の検証結果と活用例を紹介します。 背景 今回、特定のカラムの値を変化させて、ステータスの変更・管理を行っているモデルに対して新たなバリデーションを実装する作業の中で、特定のカラムの変化を察知し、特定のステータス変化が発生する時にだけバリデーションを実行するように実装する必要がありました。そのため、特定のカラムの変更状況の確認と変更前後の値の取得を行うために、ActiveRecord::AttributeMethods::Dirtyモジュールを活用しました。 ActiveRecord::AttributeMethods::Dirtyとは Dirtyは、オブジェクトに変更があった場合に検出ができ、変更前後の値を取得することができます。 使用できるメソッドは下記の通りです。 メソッド一覧 method一覧 用途 changed_attribute_names_to_save 保存予定の変更があるカラム名 has_changes_to_save? 保存予定の変更があるか判定 changes_to_save 保存予定の変更があるカラム名と変更前後の値 カラム名_change_to_be_saved 特定のカラムの保存予定の変更前後の値 will_save_change_to_attribute?(カラム名, from: "hoge", to: "fuga")(※2) 保存予定の変更があるか判定、変更前後の値を指定可 カラム名_in_database 特定のカラムのDBの値 attributes_in_database 全てのカラムの名前とそれらのDBの値 カラム名_before_last_save 特定のカラムの直近の保存前の値 saved_change_to_カラム名 直前に保存された変更内容 saved_change_to_カラム名?(from: "hoge", to: "fuga")(※1) 直前に保存された変更があるか判定、変更前後の値を指定可 saved_changes?() 直前に保存で値の変更があったか判定 saved_changes() 直前に保存した変更の変更前後の値 ※1 : saved_change_to_attribute?(:カラム名, from: "hoge", to: "fuga")とも書けます。 ※2 : will_save_change_to_カラム名?(from: "hoge", to: "fuga")でも書けます。 メソッド名の変遷 Railsの旧バージョンでは、下記のメソッドが用意されていましたが、現在ではそれらは非推奨となり、より分かりやすい表現に変わっています。メソッドの数が増えていますがafter_create/after_updateの前後のどちらかということを意識しながら過去形、現在形、未来形の時制に注目することがポイントです。 # 注)以下は現在非推奨です。 attribute_changed? attribute_change attribute_was changes changed? changed changed_attributes 活用に向けた検証 検証に使用したモデル Rails ver.6.0.3.5において、検証用に下記のモデルを準備しました。 DBのカラムに対するデフォルト値(以下、初期値)の設定の有無の影響を再現するため、初期値が無いnameカラムと初期値があるstatusカラムを用意しました。 # == Schema Information # # Table name: users # # id :bigint not null, primary key # name :string(255) # status :integer default("active"), not null # created_at :datetime not null # updated_at :datetime not null # class User < ApplicationRecord enum status : { active : 0 , inactive : 1 , inviting : 2 } end createとupdateの過程における挙動を確認した結果が下記になります。 > user1 = User .new( name : " Tom " ) => #<User id: nil, name: "Tom", status: "active", created_at: nil, updated_at: nil> > { changed_attribute_names_to_save : user1.changed_attribute_names_to_save, has_changes_to_save? : user1.has_changes_to_save?} => { :changed_attribute_names_to_save =>[ " name " ], :has_changes_to_save? => true } > { changes_to_save : user1.changes_to_save, name_change_to_be_saved : user1.name_change_to_be_saved} => { :changes_to_save =>{ " name " =>[ nil , " Tom " ]}, :name_change_to_be_saved =>[ nil , " Tom " ]} > " will_save_change_to_attribute?(:name, from: nil, to: 'Tom')=> #{ user1.will_save_change_to_attribute?( :name , from : nil , to : " Tom " ) }" => " will_save_change_to_attribute?(:name, from: nil, to: 'Tom')=>true " > user1.save => true > user1.name = " Jelly " => " Jelly " > { attributes_in_database : user1.attributes_in_database, name_in_database : user1.name_in_database} => { :attributes_in_database =>{ " name " => " Tom " }, :name_in_database => " Tom " } > user1.save => true > { name_before_last_save : user1.name_before_last_save, saved_change_to_name : user1.saved_change_to_name} => { :name_before_last_save => " Tom " , :saved_change_to_name =>[ " Tom " , " Jelly " ]} > " saved_change_to_name?(from: 'Tom', to: 'Jelly')=> #{ user1.saved_change_to_name?( from : " Tom " , to : " Jelly " ) }" => " saved_change_to_name?(from: 'Tom', to: 'Jelly')=>true " > { saved_changes? : user1.saved_changes?, saved_changes : user1.saved_changes} => { :saved_changes? => true , :saved_changes =>{ " name " =>[ " Tom " , " Jelly " ], " updated_at " =>[ Tue , 06 Apr 2021 11 : 45 : 23 UTC + 00 : 00 , Tue , 06 Apr 2021 11 : 45 : 47 UTC + 00 : 00 ]}} 上記の挙動確認の結果より、before_create, before_updateのタイミングで、特定のカラムの変更前後を確認するには、 カラム名_change_to_be_saved が最も良いのではないかと当初は考えました。 しかし、実装の過程で初期値がある場合と初期値が無い場合で、下記の様に少し挙動が異なることが分かりました。 > user1 = User .new( name : " Tom " ) # 初期値なしの場合 => #<User id: nil, name: "Tom", status: "hoge", created_at: nil, updated_at: nil> > user1.name_change_to_be_saved => [ nil , " Tom " ]  # nil -> "Tom" > user2 = User .new( status : 0 ) # 初期値あり、初期値に設定する場合 => #<User id: nil, name: nil, status: "active", created_at: nil, updated_at: nil> > user2.status_change_to_be_saved => nil   # nil -> "active"ではなく 変更なしと判断されnilが返る > user3 = User .new( status : 1 ) # 初期値あり、初期値以外に設定する場合 => #<User id: nil, name: nil, status: "inactive", created_at: nil, updated_at: nil> > user3.status_change_to_be_saved => [ " active " , " inactive " ]   # nil -> "inactive"ではなく "active" -> "inactive" > user4 = User .new( status : 2 ) # 初期値あり、初期値以外に設定する場合 => #<User id: nil, name: nil, status: "inviting", created_at: nil, updated_at: nil> > user4.status_change_to_be_saved => [ " active " , " inviting " ]   # nil -> "inviting"ではなく "active" -> "inviting" 初期値が無い場合は、nilから設定値に変化するのですが、初期値がある場合は、nilではなく初期値から設定値に変化するという挙動になることが分かりました。また、 初期値がある場合に カラム名_change_to_be_saved を使うと設定値が初期値と同等の場合はnilが返り、設定値が初期値以外の場合は配列が返るため、nilの場合と配列の場合を判定仕分ける必要が出てきました。 Dirtyの活用例 実現したかったこと/実装例 今回の実装で実現したかったことに対して実装した内容が下記の通りです。 バリデーションエラーのメッセージを分けるため、onオプションでバリデーションを分けたい # 抜粋 validate :validate_registable_user_condition , on : :create , if : -> { will_add_registered_user? } validate :validate_updatable_user_condition , on : :update , if : -> { will_add_registered_user? } def validate_registable_user_condition # createのバリデーション if can_not_registable? errors.add( :base , " ユーザー数が多すぎるため、ユーザーを新規登録できません。 " ) # エラーメッセージ 1 end end def validate_updatable_user_condition # updateのバリデーション if can_not_updatable? errors.add( :base , " ユーザー数が多すぎるため、ユーザーを更新できません。 " ) # エラーメッセージ 2 end end バリデーションが必要か否かを判定するメソッドはcreateとupdateで共通としたい。 # 抜粋 def will_add_registered_user? # create/update共通のバリデーション要否の判定メソッド if new_record? # createの場合 # ~中略~ else not_registerd_user? && will_save_change_to_registerd_user? # updateの場合 end end status: active, inviting で新規作成する場合は、バリデーション対象としたい。 status_change_to_be_saved メソッドを使わずにcreateのsave直前の値を確認したい。 # 抜粋 def will_add_registered_user? if new_record? # createの場合 self[:status] == "active" || self[:status] == "inviting" # save直前の値をチェック else # ~中略~ end end active→inviting, inviting→activeの更新はバリデーション対象から除外したい。 # 抜粋 def will_add_registered_user? if new_record? # ~中略~ else # active→inviting, inviting→activeの更新か否かを判定 not_registerd_user? && will_save_change_to_registerd_user? end end def registered_user_in_database # 更新前のstatusカラムのDBの値をチェック status_in_database == ' active ' || status_in_database == ' inviting ' end # statusがactive, inviting以外の場合か否か判定 def not_registerd_user? !registered_user_in_database end # statusがactiveもしくはinvitingへの更新か否か判定 def will_save_change_to_registerd_user? will_save_change_to_attribute?( :status , to : " active " ) || will_save_change_to_attribute?( :status , to : " inviting " ) end Dirtyの活用したサンプルコード 検証の結果を踏まえて、下記のサンプルコードのように実装することでバリデーション対象の状態変化か否かを判定できる様になりました。 class User < ApplicationRecord enum status : { active : 0 , inactive : 1 , inviting : 2 } validate :validate_registable_user_condition , on : :create , if : -> { will_add_registered_user } validate :validate_updatable_user_condition , on : :update , if : -> { will_add_registered_user } def validate_registable_user_condition if can_not_registable? errors.add( :base , " ユーザー数が多すぎるため、ユーザーを新規登録できません。 " ) end end def validate_updatable_user_condition if can_not_updatable? errors.add( :base , " ユーザー数が多すぎるため、ユーザーを更新できません。 " ) end end # status: active, invitingのユーザーはカウント対象となる。 def will_add_registered_user? if new_record? # status: active, inviting で新規作成する場合 self [ :status ] == " active " || self [ :status ] == " inviting " else # activeもしくはinvitingから登録対象にカウントされる状態へ更新する場合 not_registerd_user? && will_save_change_to_registerd_user? end end def registered_user_in_database status_in_database == ' active ' || status_in_database == ' inviting ' end def not_registerd_user? !registered_user_in_database end def will_save_change_to_registerd_user? will_save_change_to_attribute?( :status , to : " active " ) || will_save_change_to_attribute?( :status , to : " inviting " ) end # 〜中略〜 end おわりに 今回は、ActiveRecord::AttributeMethods::Dirtyモジュールの活用方法について紹介させていただきました。 今回の紹介した内容が少しでも参考になれば幸いです。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ 参考 ActiveRecord::AttributeMethods::Dirty
こんにちは。フロントエンドエンジニアの 渡邉 です。 普段はReactとTypeScriptを書いています。 今回は自分がコンポーネントを実装する際に意識していることについていくつか紹介できればなとおもいます。 ※ スタイリングに関して話すときはstyled-componentsを使用しています。 目次 はじめに 再利用性の高いコンポーネントを実装するために意識していること 共通のコンポーネントを作成する際は汎用性を意識する コンポーネントが知らなくてもいい情報を持たない(コンポーネント・構成編) コンポーネントが知らなくていい情報をもたない(コンポーネント・スタイル編) 無駄な描画を減らすために意識していること 状態に関係ないコンポーネントを混ぜない さいごに はじめに 今まで自分がReactを書いてきて、再利用性が低いコンポーネントを実装してしまったり、コンポーネントの設計自体が無駄な再描画を起こしてしまうことがあったので、その過ちを起こさないためにも実装する際に自分が意識していることを悪い例・良い例と比べながら紹介します。 この記事を読んだ後に得られる知見としては以下の2つです。 再利用性の高いコンポーネントが実装できる 無駄な再描画を可能な限り減らせたコンポーネントの実装(memo化などを使わずに) 再利用性の高いコンポーネントを実装するために意識していること 共通のコンポーネントを作成する際は汎用性を意識する 共通のコンポーネントの例としてButtonコンポーネントを作るとします。 ここで意識しているのは、共通のコンポーネント(子コンポーネント)に、呼び出し側のコンポーネント(親コンポーネント)を依存させることです。 悪い例 interface ButtonInterface { title: string getUserData: () => void } export const Button = ( { title , getUserData } : ButtonInterface ) => { return ( // getUserDataを実行するだけのボタンになってしまっている // ただ、getUserDataというpropsで違う振る舞い(会社情報を取得)をすることも可能だが、このような使い方は負債の原因となる < StyledButton onClick = { getUserData } > { title } < /StyledButton > ) } const StyledButton = styled.button ` background-color: red; ` //... // 親コンポーネント const User = () => { const getUserData = () => { //... } const getComapanyData = () => { //... } return ( < div > < Button title = 'ユーザー情報取得' getUserData= { getUserData } / > { /* これでもちゃんと動きますが、上記で説明したとおり負債の原因となる */ } < Button title = 'ユーザー情報取得' getUserData= { getComapanyData } / > < h1 > ユーザー情報 < /h1 > // ... < /div > ) } 上記だとユーザー情報を取得するためだけのボタンになってしまっています。 良い例 interface ButtonInterface { title: string onClick : ( e: React.MouseEvent < HTMLButtonElement >) => void } export const Button = ( { title , onClick } : ButtonInterface ) => { return ( < StyledButton onClick = { onClick } > { title } < /StyledButton > ) } const StyledButton = styled.button ` background-color: red; ` //... // 親コンポーネント const User = () => { const getUserData = () => { //... } const getComapanyData = () => { //... } return ( < div > < Button title = 'ユーザー情報取得' onClick = { getUserData } / > < Button title = 'ユーザー情報取得' onClick = { getComapanyData } / > < h1 > ユーザー情報 < /h1 > // ... < /div > ) } ボタンが押されたときの振る舞いを実行するだけです。 まとめ 共通のコンポーネントを作成する際は、親コンポーネントに依存したコンポーネントを作らないようにします 親コンポーネントに共通コンポーネントに依存させます 親に依存した時点で依存元のコンポーネントで作成します 何にも依存していない場合 : components/common/Button.tsx ユーザーに依存している場合: components/user/Button.tsx コンポーネントが知らなくてもいい情報を持たない(コンポーネント・構成編) 親コンポーネントは子コンポーネントのことをしるべきではないです。 逆も然りで子コンポーネントは親コンポーネントを知るべきではないです。 知ってしまった時点で再利用性は低くなります。 悪い例 const TodoList = () => { return ( < ul > { todo.map ( item => ( < Item key = { item. id } item = { item } / > )) } < /ul > ) } const Item = ( { item } ) => { return ( < li > < p > { item. title } < /p > < /li > ) } 最終的に表示されるのは ul の中にtodoの個数分 li が表示されます。 これの何が悪いのかというと、TodoListコンポーネントはItemコンポーネントがliを返すことをしっているから、ulの中に含めることができています。 つまり、ItemコンポーネントはTodoList専用のコンポーネントになります。 もしItemコンポーネントを他の場所かつ単体で使いたい場合は以下のようになり、 <div><li></li></div> というよろしくない構成になってしまいます。 const AnotherComponent = () => { return ( < div > < h1 > AnotherComponent < /h1 > < Item item = { item } / > < /div > ) } そのため、コンポーネントが知らなくてもいい情報を持たないのが大事です。 下記が適用したコードになります。 良い例 const TodoList = () => { return ( < ul > { todo.map ( item => ( < li key = { item. id } > < Item item = { item } / > < /li > )) } < /ul > ) } const Item = ( { item } ) => { return ( < div > < p > { item. title } < /p > < /div > ) } これで親コンポーネントと子コンポーネントはお互いのことを知らなくなりました。 コンポーネントが知らなくていい情報をもたない(コンポーネント・スタイル編) スタイルに関しても知らなくてもいい情報を知ってしまうと、再利用性が低くなってしまいます。 例えば、アイコンがいくつか並んだコンポーネントがあるとします。 悪い例 const Icon = ( { src } ) => { return ( < Img src = { src } / > ) } const Img = styled.img ` margin: 0 10px; ` アイコンのコンポーネントは上記のように定義されてあり、他の箇所でこのコンポーネントを使いたくなったとします。 今回は左右に20px必要です。このときにどのように解決すればよいでしょうか。 コンポーネント内に条件を追加してスタイリングをするなど様々な解決方法がありますが、、知らなくていい情報を持つことによって分岐が増えて可読性が下がります。 良い例 const Icon = ( { src } ) => { return ( < img src = { src } / > ) } const IconList = () => { return ( < IconWrapper > < Icon src = { //...} /> < Icon src = { //...} /> < Icon src = { //...} /> < /IconWrapper > ) } const IconWrapper = styled.div ` // アイコンのレイアウト記載 ` 子コンポーネントはどのように配置されるかを知らないようにします。 親がどのように配置するかを考えます。 まとめ 基本的にコンポーネントのトップでmarginを持たせないようにします 子コンポーネントは親のレイアウトを知るべきではないです 親も子の見た目について知らないようにします 無駄な描画を減らすために意識していること 状態に関係ないコンポーネントを混ぜない 状態に関係ないコンポーネントを混ぜてしまうことによって、無駄な再描画が起きてしまいます。 React.memo()でも防げますが、React.memo()をしないで防ぐのがベストだと思います。 状態の管理をReact.useStateを使っている場合と、Reduxで管理している場合の2つのパターンで紹介します。 悪い例 const Hoge = () => { const [ count , setCount ] = useState ( 0 ) return ( < div > < Counter count = { count } setCount = { setCount } / > < AnotherComponent / > < /div > ) } AnotherComponent コンポーネントは count という状態に関係ないのにも関わらずcountに変更があるたびに再描画されてします。 良い例 const Hoge = () => { return ( < div > < Counter / > < AnotherComponent / > < /div > ) } const Counter = () => { const [ count , setCount ] = useState ( 0 ) // ... } 正しい箇所で状態を管理します。 Reduxを使っている場合 悪い例 const Hoge = () => { const count = useSelector ( state => state.count ) return ( < div > < Counter count = { count } / > < AnotherComponent / > < /div > ) } countはCounterコンポーネントには必要だが、 AnotherComponent には関係のない状態です。 良い例 const Hoge = () => { return ( < div > < Counter / > < AnotherComponent / > < /div > ) } const Counter = () => { const count = useSelector ( state => state.count ) // ... } ただ、一つ問題点があり、このCounterコンポーネントがpropsのcountのみを表示する共通コンポーネントの場合です。 そのような場合は以下のようにしています。 良い例2 const Hoge = () => { return ( < div > < HogeCounter / > < AnotherComponent / > < /div > ) } // Hoge専用のCounter const HogeCounter = () => { const count = useSelector ( state => state , count ) return < Counter count = { count } / > } interface CounterInterface { count: number } const Counter = ( { count } : CounterInterface ) => { return ( < span > { count } < /span > ) } まとめ 状態に関係のないコンポーネントが見つかった場合は状態が使われているコンポーネントを新たに切り出します Reduxを使っている場合はより意識します storeで状態を管理しているため、コンポーネント外から対象の状態(上記でいうと、state.count)に変更を加える可能性があるため さいごに この記事で説明したことを少しでも意識し始めたことによって自分はかなり再利用性の高いコンポーネントが実装できたと感じているので、参考にしていただければなと思います。 時にはこのケースに当てはまらない場合もあるとは思いのますが、その時は新たな観点で考えて貰えれば幅もより良い実装になっていくと思います。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
https://www.cypress.io 目次 はじめに Cypress cypress-on-rails おわりに 1. はじめに はじめまして、株式会社スタメンでエンジニアをしています伊藤です。普段はRuby on Railsを使っているサーバー側の人間なのですが、重要な機能を守るためにE2Eテストを書くことになりました。Railsで単体テストを書く際はFactoryBotでテストデータを作り、RSpecで単体テストを行うというお決まりパターンでコードを書いていましたが、今回は Cypress と cypress-on-rails 使いE2Eテストを書いてみたのでその内容について紹介できればと思います。 Cypressとはですが、簡単に言ってしまえばE2Eテストを行うことができるOSSです。Cypressは導入がとても楽なので、触り始めたばかりの頃は「なんて便利なものなんだ!」と、なるのですが、ネイティブのJavaScriptにはない独特の仕様が多く一筋縄ではいきません。代表的なものだと、Promiseやasync/awaitは基本的には使えないです。非同期処理をどうするかはCypressを触る上でとても重要なポイントです。公式にドキュメントがしっかりとまとめられており、また多くのエンジニアがissueを立てているので、そのあたりをちゃんと読めば大体の問題は解決できると思います。ただし英語です。 RailsエンジニアがCypressを触るのであればcypress-on-railsの利用を考えてみても良いと思います。cypress-on-railsの利点として、FactoryBot経由でテストデータを作成できることがあげられます。これまでtraitなどで積み上げてきたテストケースの財産を再利用できるので、これまで頑張って単体テストを書いてきた人ほどハッピーになれます。ただし、実際に触ってみると非同期処理や変数の扱いが分かっていないと分からない難しさがあるので、Cypressの仕様に触れてからcypress-on-railsの話に入っていきたいと思います。 2. Cypress Cypressではテストランナーとダッシュボードが提供されており、テストランナーはGitHubでソースコードが公開されているOSSで、ダッシュボードでは一部機能を無料で利用することができます。SeleniumなどのようにWebDriverを入れたりする必要がないので、環境構築に対するコストが小さいことも魅力的です。Dockerを利用する場合、テスト用のサーバーの実行とCypressが実行できるコンテナが用意できればいいため、CircleCIとの連携も比較的簡単です。 Cypressでは公式HPで多くのBest Practiceが示されており、基本的にはそれに則るコーディングが推奨されています。Cypress自体はnode環境下で実行されるJavaScriptになりますが、実行のされ方が特殊です。書くコードがそのままコード通り同期的に評価されるのではなく、キューに蓄えられてから非同期的に実行されます。どういうことかというと、Cypressが用意しているAPIとネイティブのJavaScriptの書き方を組み合わせると意図しないタイミングで評価されてしまい、思い通りの処理が実現できないということになります。つまり、Cypressのコードを書く際は Cypressのガイド で示されている書き方に従いコーディングを行うことになります。その中でいくつか特徴的な仕様について紹介します。 Cypressの仕様で複雑なものとして非同期処理に関する部分があげられます。最近のモダンなJavaScriptの書き方に慣れている人からすれば、非同期といえばasync/await、少なくともPromiseの使用をイメージすると思います。しかし、 公式 で述べられているようにCypressではES7のasync/awaitはサポートしていません。Promiseは存在しますが、ネイティブのPromiseとは異なり Cypress.Promise で生成されたオブジェクトのみ挙動を保証しています。(内部モジュールとしては Bluebird を使っているようです。) Why can’t I use async / await? If you’re a modern JS programmer you might hear “asynchronous” and think: why can’t I just use async/await instead of learning some proprietary API? Cypress’s APIs are built very differently from what you’re likely used to: but these design patterns are incredibly intentional. We’ll go into more detail later in this guide. 公式の設計デザインとしてasync/awaitは使用しないとされており、ガイドに則った非同期処理のコーディングが求められます。例えば、Cypress公式の見解として非同期処理を行う場合は then() や intercept() といったCommandと呼ばれるAPIの利用や、 Chains of Commands に従ったコーディング、 Custom Command の利用を推奨しています。 Test Structure Cypressは Mocha と Chai をベースにしています。そのため、MochaやChaiのTDD/BDDの記法にしたがってコードを書くことになります。テストコード全体の構成としてはMochaをベースにしています。そのため、 describe() や context() 、 it() 、 specify() などRailsエンジニアであれば馴染みのあるBDDスタイルでコーディングすることが基本となります。Webページ上のDOMを参照する際はjQueryのエンジンを利用しています。そのためセレクターの書き方は古き良きjQueryの書き方に従うことになります。( https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Cypress-Can-Be-Simple-Sometimes ) describe( 'Post Resource' , () => { it( 'Creating a New Post' , () => { cy.visit( '/posts/new' ) // 1. cy.get( 'input.post-title' ) // 2. .type( 'My First Post' ) // 3. cy.get( 'input.post-body' ) // 4. .type( 'Hello, world!' ) // 5. cy.contains( 'Submit' ) // 6. .click() // 7. cy.url() // 8. .should( 'include' , '/posts/my-first-post' ) cy.get( 'h1' ) // 9. .should( 'contain' , 'My First Post' ) } ) } ) Cypressは実行の前後のhooksについてもMochaにおけるhooksの仕様が受け継がれています。( https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Hooks ) beforeEach(() => { // root-level hook // runs before every test } ) describe( 'Hooks' , () => { before(() => { // runs once before all tests in the block } ) beforeEach(() => { // runs before each test in the block } ) afterEach(() => { // runs after each test in the block } ) after(() => { // runs once after all tests in the block } ) } ) before でテストデータの準備などを行い、 beforeEach でログインやCookie周りなどテスト毎のステート管理を行うことが基本となります。素直に考えれば after や afterEach は before や beforeEach のステートを綺麗にする処理を書きたくなると思いますが、それは アンチパターン のようなので、正直使い所が難しいです。 Chains of Commands Chains of Commands とは、基本的にはJavaScriptのメソッドチェーンになります。Cypressでは実行が非同期的に行われます。そのためネイティブのJavaScriptではなくCypressで用意しているAPIを使い実行順序を制御します。 各コマンドはキューに一度蓄えるため、ネストが同じ高さのコマンドは同期的に実行されることになります。しかし、実行結果の内容を受けて処理を変化させたい場合や、DOM要素の属性値を参照したい場合などは各コマンドの実行結果を受け取りたいはずです。実行結果を他のコマンドに確実に渡す方法としてコマンドのチェーンがあります。 Cypressのコマンドは必ず返り値が存在します。前回実行したコマンドの結果のことをCypressでは subject と呼びます。subjectはDOM要素や数値、文字列、オブジェクトなど様々な型になりますが、 この設計 はChaiおよびChai-jQueryから組み込まれているそうです。コマンドをチェーンしていくことでこのsubjectが次のコマンドへと渡されていくため、アサーションを実行することができます。チェーンの途中で get() などを挟むことでsubjectを変えることもできるため、全てのコマンドをチェーンさせたコードを書くこともできます。ただし、コマンドによってはsubjectとして何も渡さないものも存在します。コードの可読性を考えて、一連のまとまったテスト内容はチェーンさせて、テストしたい内容が変わるタイミングでわざとチェーンを一度外すといった書き方もできます。 Commands Cypressの実行が非同期的です。そのためネイティブのJavaScriptではなくCypressで用意しているAPIを使い実行順序を制御します。その中で非同期処理に関連する get() 、 then() 、 as() 、 wait() 、 intercept() の4つを紹介します。これらのコマンドは実際に使う中で何度も悩まされたコマンドです。 get get() はおそらくCypressでコードを書く中で一番使用頻度が高いコマンドです。使用方法としては大きくわけて2つあり、DOM要素の取得とエイリアスの参照です。DOM要素を参照する方法はjQueryのセレクターの記法に準拠します。また、DOM要素が実行したタイミングですぐに見つからなかった場合でも、自動的にリトライ処理が走り、遅延してレンダリングされた場合でも取得することが可能です。 get() での検索自体がアサーションとして働くため。時間をおいても対象が見つからずタイムアウトした場合は、テスト項目の失敗となります。そのため、DOM要素の存在有無を繰り返し判定するような処理を行いたい場合には使えません。エイリアスについては as() の説明で述べます。 cy.get( 'button' ) // button要素 cy.get( 'div.test' ) // div要素でクラス名がtest cy.get( '[data-cy=hoge]' ) // data-cy属性値がhoge then then() は直前のコマンドの結果をうけてコールバック関数を実行するコマンドです。 then() を使用するタイミングとしては、大きく分けて2つの状況があげられます。1つ目は変数の扱う状況です。公式のガイドラインによると、Cypessコマンドの実行結果を変数に格納する方法はアンチパターンとされています。理由としては、呼び出すタイミングで対象となるオブジェクトが存在している保証がないからです。 then() を使って呼び出すことで、コールバックのスコープ内であれば確実にオブジェクトを参照することができます。 cy.wrap( 'hoge' ).then(text => { const result = text + 'fuga' cy.wrap(result).should( 'eq' , 'hogefuga' ) } ) 2つ目はDOM要素を参照する場合です。Cypressの場合DOM要素を参照する場合 get() を使います。参照したDOM要素を検証する場合には続けて should() などを使いますが、変数化して扱いたい場合は then() を使わなければなりません。 get() の返り値としてjQueryセレクターを返しますが、そのまま変数化しても変数化の処理の部分だけが同期的な処理になってしまうので、思い通りの挙動をしない恐れがあります。 const $elem = cy.get( 'button' ) // ここは同期的 $btn.click() // ここは非同期的、いつ実行されるか分からない DOM要素を確実に変数化したい場合は then() を使い、引数から参照するようにします。 cy.get( 'button' ).then($btn => { // $btnはjQueryオブジェクト const text = $btn.text() // Cypressのコマンドを実行できる形にするには一度wrapで変換する必要がある cy.wrap($btn).click() } ) as thenを使用せずにオブジェクトを別のコマンドに渡す方法として、 as コマンドの使用があげられます。 then() は非同期的な実行が行われるCypressの中で変数などを扱う際に欠かせないものですが、使用するたびにネストが下がるため、jQueryのコールバック地獄の時のような深いネストが生まれてしまうことがあります。そこで、 as コマンドを使用することで、ネストを回避することができます。 as() はsubjectに対してエイリアスをはるコマンドです。ここでいうエイリアスとは、直前に実行したコマンドのsubjectを参照するためのkeyとなる文字列のことで、チェーンしていなくても get() から値を参照することができます。 get() は対象が見つかるまで処理が繰り返されるので、評価が終わるまでは次のコマンドに移ることはありません。なので、直前に非同期的な処理の結果を as() で持つようにし、 get() を使い処理が完了するのを確実に待つことができます。 cy.get( 'button' ).as( 'btn' ) cy.get( '@btn' ).click() then() のネストを避けて変数化の代わりを行う方法として有用ですが、落とし穴があります。 as() ではられたエイリアスは get() で参照されるとライフサイクルを終えてしまうので、再度参照することができなくなります。なので、繰り返し参照する可能性がある結果に対しては不向きです。 wait 非同期的な処理を待つ方法として as() と get() を使用するパターンを述べましたが、通信処理を待つ場合には get() ではなく wait() を使う方が良いです。 get() はそれ自体にアサーションを含むためタイムアウトしてしまった場合はテストの失敗となってしまいます。しかし、通信結果がなかなか返ってこず、タイムアウトした後に再度処理を繰り返したい場合は wait() が適しています。 wait() の使い方は、引数に与えられた一定時間を待つという使い方と、エイリアスを待つという使い方の2つがあります。前者の使い方はsetTimeoutなどと同じように馴染みのある使い方ですが、テストの不安定さに繋がるためCypressではアンチパターンとされています。一定時間ではなく結果を待つ方法としてエイリアスを使う方法があり、こちらの使用が推奨されます。 intercept 非同期処理の定番として通信処理があげられます。例えば、APIを投げる処理が走った場合に、レスポンスが返ってくるまで処理を待ちたい状況が考えられます。 intercept() はクライアント側から送られるリクエストを監視できるコマンドです。以前は route() というコマンドが使われることが多かったようですが、Fetch APIへの対応など様々なネットワーク層の仕様に対応したコマンドになっています。 intercept() は単体で使うことはなく、基本的には as() と wait() とセットで使います。 cy.intercept( '/results' ).as( '@results' ) // リクエストの内容を記述、asでエイリアスを作る cy.get( 'button' ).click() // リクエストが飛ぶ処理 cy,wait( '@results' ) // エイリアスの内容が得られるまで待つ cy,get( 'li' ).should( 'have.length' . 10) // 結果をアサーション Custom Command Cypressの標準で実装されているコマンドについていくつか紹介しましたが、ユーザーがコマンドの組み合わせで独自で定義する Custom Commands と呼ばれるものがあります。 Custom Commandのベストプラクティス で内容について書かれていますが、ログイン処理や通信処理などよく使われ関数化したい処理をCustom Commandsにするのが良いとされています。JavaScriptのコードなのでもちろんネイティブの関数定義で複数のコマンドをまとめることもできますが、Custom Commandで定義された処理はチェーンすることで非同期的な処理内容でも確実に制御することができるので、なるべく関数ではなくCustom Commandで定義する方が良いです。特にPromiseが必要になるような処理を書きたい場合はCustom CommandsでCypress.Promissを返す必要があります。( https://qiita.com/murata0705/items/100ef8300caeeaa7d409 ) Cypress.Commands.add( 'hoge' , () => { // cyコマンドの処理 } ) Sharing Context Cypressで変数を扱う方法として then() と as() を紹介しましたが、複数のものを何度も参照したいケースでは使い辛いです。そこで、一部のケースにおいてこの問題を解決する方法として sharing context というものがあります。Mochaの仕様として、 before などのhookではられたエイリアスは this.* で参照することができます。これを用いることで、beforeで行った処理結果をitで参照することができます。また、複数のデータを渡す場合でもネストを下げることなく繰り返し参照することができます。ただし、 before から this のスコープが渡されることが必要となるため、arrow式でitにコールバックを渡した場合には利用することができません。shared contextを利用する場合は必ずfunction式で渡します。渡したいデータが少ない場合は then() や get() で参照し、渡すデータが多い場合はshared contextを利用するなどの使い分けができると思います。shared contextを利用する場合、function式とarrow式が同じファイル内で混在しがちになりますが、shared contextを使う場合だけコールバックをfunction式で書くと言ったルールにすれば、書き方から意図を伝えることができます。 before(() => { cy.fixture( 'users.json' ).as( 'users' ) // jsonファイルの読み込み結果に対してエイリアスをはる } ) // shared contextを使う場合はコールバックをfunction式にする it( 'utilize users in some way' , function () { const user = this .users [ 0 ] cy.get( 'header' ).should( 'contain' , user.name) } ) 3. cypress-on-rails CypressはWebブラウザでの挙動を自動的にテストしてくれるツールです。そのため、サーバーサイドで準備するテストデータはCypress外部で用意をしておく必要があります。そこで今回はRails環境下でCypressを使用する際に便利な cypress-on-rails というgemについて紹介します。 cypress-on-railsの最大の特徴は、CypressからのRubyファイルを実行できる点にあります。FactoryBotによるテストデータの作成やtest fixturesの利用が可能です。これによりこれまで培ってきた既存のテストデータの作成が再利用できます。FactoryBotを使う場合であればtraitやtransientを使い、簡潔にコードを記述することも可能です。 インストール gemをインストールするためにGemfileに次の記述を追加します。 group :test, :development do gem 'cypress-on-rails', '~> 1.0' end gemのインストールの次は、cypress-on-rails用のボイラープレートが用意されているのでそれも合わせて実行します。 bin/rails g cypress_on_rails:install 実行すると以下のようなディレクトリとファイルが生成されます。 config/environments/test.rb config/initializers/cypress_on_rails spec/cypress/integrations/ Cypressのテストファイルを格納する spec/cypress/support/on-rails.js cypress-on-railsに必要なCustom Commandsの定義 spec/cypress/app_commands/scenarios/ テストデータなどを作成するシナリオファイルを格納する spec/cypress/cypress_helper.rb コマンドが実行される前に評価されるファイル 自動的に追加されるものではないですが、FactoryBotの利用やデータベースのクリーンアップ、静的なテストデータの読み込み、Cypress外でのNodeプロセスの実行などを行う際には、加えて以下のディレクトリやファイルが必要になります。 spec/cypress/fixtures/ Cypress内で読み込むテストデータを格納する spec/cypress/plugins/ Cypress外のNode.jsのイベントを登録する spec/cypress/app_commands/clean.rb データベースのクリーンアップ spec/cypress/app_commands/factory_bot.rb FactoryBotの設定 cypress-on-railsではCypressのコマンドがフックでRubyファイルが実行されます。仕組みとしては、Cypressの cy.request コマンドを用いてサーバーへリクエストを送り、送られてきたリクエストの内容に従い実行するRubyファイルを見つけ実行し、実行結果をレスポンスとして返すことでファイルの実行と実行結果の取得を行います。app_commandsで定義したRubyファイルは Kernel.eval で評価されます。そのため、DRYなコードを実現するためにはFactoryBotのtraitを最大限に使用するなどの工夫が必要になります。 使い方 FactoryBotを使用したデータの作成は以下のような形で記述することができます。 bot = CypressOnRails :: SmartFactoryWrapper params = command_options.symbolize_keys user = bot.create( :user , name : params[ :name ], password : ' password ' ) article = bot.create( :article , :only_text , user : user, title : ' 素敵なタイトル ' , value : params[ :value ]) return { id : user.id, password : user.id, } Cypressのテストコードで実際に使用する場合は次のように cy.app もしくは cy.appScenario で実行することができます。 const data = { name: 'テスト太郎' , value: '素敵な文章' , } // cy.appを使ったパターン cy.app( 'scenarios/create_data' , data).then(res => { cy.login(res) } ) // cy.appScenarioを使ったパターン cy.appScenario( 'create_data' , data).then(res => { cy.login(res) } ) cypress-on-railsではrubyファイルの評価結果をsubjectとして渡すことができるので、shared contextと組み合わせればサーバー側から複数の情報を簡単に参照することができます。また、DBに対する操作も間接的に可能であるため、テスト用に追加でサーバー側にAPIを定義せずに様々なテストケースを再現することができます。追加の設定でDBのクリーンアップも行えるので、テストごとに独立したテストデータを用意することもできます。 cypress-on-rails はテストデータを用意する際にとても便利なのですが、ruby側の処理でエラーが合った場合、Cypress側ではエラーコードが500のレスポンスでタイムアウトしたことしか分かりません。実際のエラー内容を確認するには実行ログを見るしかありません。さらに、FactoryBotのtraitの定義に問題が合った場合はログにも現れないことがあります。デバックの面では使いにくさが残ります。 4. おわりに Rails環境下におけるE2EテストとしてCypressとcypress-on-railsを用いた方法について紹介しました。Cypressは導入が簡単でCIとのシナジーも高い部分がメリットとしてあげられますが、独特な仕様や非同期処理の扱いづらさがデメリットとしてあげられます。cypress-on-railsを使うことでCypressでもFactoryBotなどのRubyの財産を使ったテストデータの作成ができ、素早く様々なシナリオでのE2Eテストを作ることができます。重要な機能はこうしたE2Eテストなどでこれからもしっかりと守っていきます。 スタメンでは一緒に働くエンジニアを募集しています。興味がある方は、ぜひ 採用サイト からご連絡ください!
はじめに こんにちは!スタメンでエンジニアインターンをしている松山です。約半年間インターンをしてきました。今回はインターンの振り返りを書いていこうと思います。 自己紹介 私は現在、愛知県の大学2年生です。大学では社会福祉を専攻していてその中でも特に社会福祉事業の最適化について研究しています。 スタメンには2020年の8月からインターンとして参画しました。業務では主にiOSアプリの開発を行っています。 スタメンでのインターン以外に実務経験はなく、文系の学部に通っていたので、参画時は正直右も左も分からないというような状態でした。しかし半年間のインターンを経てTUNAGの新機能開発・機能改善を行えるまで成長しました。 下記ではインターンとしてどのような業務をしていったのかを書いていこうと思います。 これまでの業務内容 最初の一ヶ月間 まず初めの1ヶ月は小さな不具合修正や細かいタスクをこなしながらTUNAGのコードを理解していきました。ちょうど自分がインターンを始めるときにコロナウイルスの影響でリモートワークが進み仕事のやりづらさがありました。しかし、開発チームのみなさんがDiscordの通話を常時繋げっぱなしにし、わからないところを気軽に聞ける環境を作ってくださったので、なんとかタスクを潰していくことができました。インターンでの初めてのタスクは、ボタンの文字を「完了」⇨「閉じる」に変更するものでした。簡単なタスクでしたが、初めてPRを提出するときは緊張しました。 2ヶ月目〜 それからしばらくして、TUNAGのコードにも慣れてきた頃少し複雑な機能の開発を任されました。タイムラインのコメント入力画面をリニューアルするというタスクでした。これまでの業務ではピンポイントで特定の箇所を修正すれば済むものでした。しかし今回は自分の書いたコードが今後どのように使われるのかなどを想定しなければいけなかったため、アーキテクチャの理解やオブジェクト指向の理解に苦しみました。しかし開発を進めていくうちに自分の中で段々苦しんでいたことが腑に落ちるようになってきたためここで大きく成長できたと思います。 現在 それからはOSのアップデートの対応やそれに付随して必要になったライブラリの導入などを行い現在は比較的大規模なプロジェクトに携わっています。インターンではありながらも、社員と同じ業務を任せてもらっています。 なぜ未経験の文系大学生が社員と同じような業務をできるまで成長できたのか 半年間のインターンを経て見違えるほど成長することができました。一番僕の成長を後押ししてくれたのはTUNAGが急成長中のプロダクトであるということだと思います。成長中のプロダクトは、課題が多くあり、その分プロジェクトも多く用意されます。そのどれもが重厚な開発経験を積めるものばかりであったため、自分の成長に大きく起因したと思います。 また、スタメンではセキュリティ勉強会や、コンピューターサイエンス勉強会など、初心者と上級者の知識の溝を埋めてくれるような勉強会が開催されていたので、それに積極的に参加していったのも自分の力を底上げする要因になったのではないかと思います。 おわりに スタメンは周りに優秀なエンジニアが多く非常に切磋琢磨できる環境です。さらに若手にも多くのチャンスが降ってきます。自分はこの半年間で多くを任せていただき成長することができました。今後も良いプロダクトを作る過程で自身が成長していけると思うとワクワクします。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニアサイト をご覧ください。
スタメンエンジニアの井本です。 普段の業務ではRuby on Railsを用いた機能開発を担当しています。 前職である電気回路の設計エンジニアからWebエンジニアに転身し、11月から働いています。 スタメンでは、エンジニアの技術力向上に力を入れており、社内勉強会を積極的に実施しています。 今回は、私が11月〜12月に参加した「みんなのコンピュータ・サイエンス勉強会」についての記事です。 当記事のトピックは大きく2つ。 1. 勉強会のレポート 2. 勉強会の中でRubyで実装したアルゴリズムについて 1においては、エンジニアとしてスタメンで働くことで、どのような環境でどう成長できるか?イメージする一助となれば幸いです。 2は、Rubyならこう書く、といった技術寄りのコンテンツです。 勉強会レポート テキスト みんなのコンピュータサイエンス(翔泳社) 内容 計算量 データ構造 アルゴリズム(ソート、探索) データベース(リレーショナル、非リレーショナル、分散データベース、データの一貫性) コンピュータ(アーキテクチャ、コンパイラ) 所感 大学時代の専攻や前職で、アルゴリズムやデータベースについては、学ぶ機会がありましたが、あくまで知識として持っているに過ぎませんでした。 今回の勉強会で何より良かったことは、先輩エンジニアに質問しながら理解を深めることができた点です。 このため、今回の勉強会を通して、Webエンジニアの立場でどう実現するのか、選択していくのか等、以前よりも実務をイメージしながら学ぶことができました。 業務に活かせていること 計算量を意識できるようになった 明らかに計算量が増えるような構造に注意が向くようになりました。 例えば、 each などリストを扱うメソッドをネストする、といったコードを、Webエンジニアデビューする前には気にせず書いていたものです…。 Rubyならではの書き方に関心が向くきっかけとなった 教材の書籍においては、例で掲載されているコードは、擬似コードを用いているため、経験の浅い私には少々イメージがしづらいところがありました。 そこでアルゴリズムの章では、自らRubyで実装しました。 実際に動かすことでアルゴリズムの動きを感覚的に理解ができただけでなく、コードレビューをもらうことで、Rubyらしいシンプルな実装を学ぶことができました。 Rubyで書くコードの明快さと、自身が書くコードの不明瞭さに気づくことができました。 この時に書いたコードに関するコードについては、次のトピックで実際に触れて参ります。 Rubyによる各種アルゴリズムの実装 すべて載せてしまうと、相当な文量となってしまうので、Rubyで実装することで違いが顕著だったコードのうち2つをピックアップします。 挿入ソート テキストのコード function insertion_sort(list) for i ← 2 ... list.length j ← i while j and list[j- 1 ] > list[j] list.swap_items(j, j- 1 ) j ← j - 1 Rubyで動作のみを再現したコード def swap (ary, x, y) ary[x], ary[y] = ary[y], ary[x] end def insert_sort (ary) for i in 1 ..(ary.length - 1 ) j = i while j > 0 && ary[j- 1 ] > ary[j] swap(ary, j- 1 , j) j = j - 1 end end end リファクタリング後 ary = # ランダム数列 module Sortable def swap! (i, j) self [i], self [j] = self [j], self [i] end def insert_sort 0 .upto self .size- 1 do | i | i.downto 1 do | j | break if self [j- 1 ] < self [j] swap!(j- 1 , j) end end end end ary.extend Sortable ary.insert_sort Rubyでは for 文を使わない、ということで upto メソッドで代替しました。 合わせて while 文についても down_to で置き換えています。 イテレータの制御をメソッド自身に任せる点でRubyらしいといえます。 他にはArrayオブジェクトにて extend して用いることで、関数ではなくメソッドとして swap や insert_sort を実施できるようにしました。 DFSとBFS DFSとBFSとは DFS(Depth First Search)=深さ優先探索、 BFS(Breadth First Search)=広さ優先探索 と呼ばれるアルゴリズムのことです。 グラフを探索するにあたって、どのような順序でノードを巡回していくか?を指すものと思っていただければ、差し支えございません。 具体的には次の2つの図のような順番で探索を進めます。 DFSの場合 数字の順番に探索が行われます。 ノード0から探索を開始する ノード1, 5, 6を発見する ノード1が条件に合致するか確認する ノード1に接続されたノードを探す ノード2を発見する ノード2が条件に合致するか確認する ノード2に接続されたノードを探す ノード3, 4を発見する ノード3が条件に合致するか確認する (以下、同様) このように、新しく発見したノードから先に探索を進めていく方式がBFS(広さ優先探索)です。 BFSの場合 ノード0から探索を開始する ノード1, 2, 3を発見する ノード1が条件に合致するか確認 ノード1に接続されたノードを探す ノード4を発見する ノード2, 3も1と同様に、条件を確認した上で接続ノードを見つける ノード1, 2, 3の探索を完了する 新しく発見したノード4, 5の条件を確認する (以下、同様) このように、先に発見したノードから先に探索を進めていく方式がBFS(広さ優先探索)です。 早速、コードを見ていきましょう。 テキストのコード function DFS(start_node, key) next_nodes <- Stack. new () seen_nodes <- Set. new () next_nodes.push(start_node) seen_nodes.add(start_node) while not next_nodes.empty node <- next_nodes.pop() if node.key = key return node for n in node.connected_nodes if not n in seen_nodes next_nodes.push(n) seen_nodes.add(n) return null function BFS next_nodes <- Queue. new () seen_nodes <- Set. new () next_nodes.enqueue(start_node) seen_nodes.add(start_node) while not next_nodes.empty node <- next_nodes.dequeue() if node.key = key: return node for n in node.connected_nodes if not n in seen_nodes next_nodes.enqueue(n) seen_nodes.add(n) return null Rubyによる実装 クラス実装 class Graph attr_accessor :nodes def initialize (nodes = []) @nodes = nodes end def initialize_search_memory @next_nodes = [] @seen_nodes = [] end def push_memory (node) @next_nodes .push(node) @seen_nodes .push(node) end alias :queue_memory :push_memory def pop_next_nodes @next_nodes .pop end def dequeue_next_nodes @next_nodes .shift end def saw? (node) @seen_nodes .include?(node) end def next_nodes_exist? @next_nodes .any? end def connect (key1, key2) if (v1 = find_node(key1)) && (v2 = find_node(key2)) v1.connect(key2) v2.connect(key1) else false end end def find_node (key) nodes.find{| v | v.key == key } end def get_connected_nodes (node) keys = node.connected_nodes nodes = keys.map{| k | find_node(k)} end end class Node attr_accessor :key , :value , :connected_nodes def initialize (key, value) @key = key @value = value @connected_nodes = [] # Nodeオブジェクトのkeyを格納する end def connect (key) connected_nodes.push(key) end end DFS class Graph def dfs (start_key, key) initialize_search_memory start_node = find_node start_key push_memory start_node while next_nodes_exist? node = pop_next_nodes return node if node.key == key get_connected_nodes(node).each do | n | push_memory n unless saw? n end end end end BFS class Gragh def bfs (start_key, key) initialize_search_memory start_node = find_node start_key queue_memory start_node while next_nodes_exist? node = dequeue_next_nodes return node if node.key == key get_connected_nodes(node).each do | n | queue_memory n unless saw? n end end end end 実行結果 @graph = ' グラフの生成 ' @gragh .dfs( 0 , 3 ) => #<Node:0x00007fd68e85a7e8 key: 3, value: 35, connected_nodes: [4, 5, 8, 9]> @gragh .dfs( 0 , 3 ) => #<Node:0x00007fd68e85a7e8 key: 3, value: 35, connected_nodes: [4, 5, 8, 9]> テキストのコードでは、 stack や set など一般的なデータ構造を用いて書かれていました。 Rubyによる実装ではクラス定義を用いることで、引数を最小限に抑えることができたため、シンプルなコードで書くことができました。 おわりに 前半では、勉強会がどのように進められていたか?勉強会で何を得て、業務に活かすことができているか?について述べました。 自己研鑽して食らいついていくことは前提ではありますが、スタメンには、それをサポートする環境があります。 後半では、やや技術的な内容としてテキストの疑似コードをRubyで実装したコードを紹介しました。 バリエーション豊かなイテーレーションメソッドや、クラス定義を用いることで、よりシンプルに書けるRubyの良さを再確認しました。 今回は以上です。 最後までご覧いただき誠にありがとうございました。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
目次 はじめに HTTPヘッダーとは Content-Typeの概要 検証内容 おわりに はじめに こんにちは、スタメンでエンジニアをしている手嶋です。普段はReact+TypeScriptでフロントエンドを開発したり、RailsでAPIを作成しています。クライアントサイドからサーバーサイドへリクエストするに当たり、HTTPヘッダーのContent-Typeを柔軟に変える事でリクエストの記述をシンプルに出来たので、今回紹介したいと思います。 HTTPヘッダーとは まずHTTPヘッダーについてですが、以下のように定義されています。 HTTPヘッダーは、要求または応答に関する追加のコンテキスト及びメタデータを渡すHTTP要求または応答のフィールド HTTPヘッダーは以下3つにカテゴライズされる リクエストヘッダー:フェッチするリソースまたはクライアント自体に関する詳細情報を含むヘッダー。 応答ヘッダー:場所やサーバー自体(名前、バージョンなど)など、応答に関する追加情報を含むヘッダー。 表現メタデータヘッダー:メッセージ本文のリソースに関するメタデータ(言語、長さ、メディアタイプなど) Content-Typeは リクエストボディのメディアタイプを指定 する役割を持つので、表現メタデータヘッダーに該当します。 Content-Typeの概要 上述の通り、Content-Typeはリクエスト時にメディアタイプを指定する役割を果たします。 メディアタイプは、MIMEタイプや要素タイプとも言われ、インターネット上で転送される コンテンツの形式を表現する識別子 を表します。 具体的な種類の例として以下が挙げられます。ファイルは「形式」と適宜読み換えてください。 MIMEタイプ 文書の種類 text/plain テキストファイル text/csv CSVファイル text/html HTMLファイル text/css CSSファイル text/javascript JavaScriptファイル application/json JSONファイル application/pdf PDFファイル image/jpeg JPEGファイル(.jpg, .jpeg) image/png PNGファイル image/gif GIFファイル image/svg+xml SVGファイル application/zip Zipファイル video/mpeg MPEGファイル(動画) 検証内容 今回検証した内容は以下です。 前提としてReactアプリケーションはPOST(PATCH)パラメータ(params)をオブジェクトとして管理しています。そしてAPIにリクエストするファイルでは以下のようにパラメータを展開します。 パラメータは全てが必須項目ではなく、存在する場合のみリクエストに含める想定です。 改善前 // type type UserParamsType = { name: string email: string address: string phone: number gender: string } // user更新用の関数。 別関数にエンドポイントurlとリクエストbodyを渡す export const requestUpdateUser = async ( userId: number , params: UserParamsType ) => { // エンドポイント const url = `api/v1/user/${userId}` // パラメータ生成 const { name , email , address , phone , gender , } = params let body = '' if ( name ) body += `&[name]=${encodeURIComponent(name)}` if ( email ) body += `&[email]=${encodeURIComponent(email)}` if ( address ) body += `&[address]=${encodeURIComponent(address)}` if ( phone ) body += `&[phone]=${encodeURIComponent(phone)}` if ( gender ) body += `&[gender]=${encodeURIComponent(gender)}` const response = await fetchPatchTemplate ( url , body ) return response } // HEADERS // Content-Typeにはapplication/x-www-form-urlencoded; charset=utf-8を指定 const HEADERS = { Accept: 'application/json' , 'Content-Type' : 'application/x-www-form-urlencoded; charset=utf-8' , } // apiを叩く関数 export const fetchPatchTemplate = async ( url: string , body: string ) => { try { const response = await fetch ( url , { credentials: 'same-origin' , method: 'PATCH' , headers: HEADERS , body , } ) if ( !response.ok ) { throw Error ( response. statusText ) } const resJson = await response.json () return { payload: resJson } } catch ( error ) { return { error: 'エラーメッセージ' } } } 上記の通りContent-Typeには application/x-www-form-urlencoded; charset=utf-8 を指定しています。 このTypeは、 「キーと値が '=' を挟んで組になり、 '&' で区切られてエンコードされる」 という特徴を持ちます。 よってこのTypeを指定した場合は、上記の記述でparamsを生成し、リクエストbodyに含める事ができます。 改善案 しかし、上記の記述ではparamsの数だけ展開の記述をする回数が増えてしまいます。 その場合は、以下のように params展開部分 と ContentType を書き換える事で記述量を減らす事が可能です。 params展開部分 => JSON.stringify(params) JSON.stringify() メソッドは、あるJavaScript のオブジェクトや値をJSON文字列に変換するメソッドです。 ContentType => 'application/json' application/jsonに変更しJSON形式を扱えるよう変更します。 // type type UserParamsType = { name: string email: string address: string phone: number gender: string } // user更新用の関数。 別関数にエンドポイントurlとリクエストbodyを渡す export const requestUpdateUser = async ( userId: number , params: UserParamsType ) => { // エンドポイント const url = `api/v1/user/${userId}` // オブジェクト形式のparamsをJSON.stringifyの引数に渡しパラメータ生成 const body = JSON.stringify ( params ) const response = await fetchPatchTemplate ( url , body ) return response } // HEADERS // Content-Typeにはapplication/jsonを指定 const HEADERS = { Accept: 'application/json' , 'Content-Type' : 'application/json' } // apiを叩く関数 export const fetchPatchTemplate = async ( url: string , body: string ) => { try { const response = await fetch ( url , { credentials: 'same-origin' , method: 'PATCH' , headers: HEADERS , body , } ) if ( !response.ok ) { throw Error ( response. statusText ) } const resJson = await response.json () return { payload: resJson } } catch ( error ) { return { error: 'エラーメッセージ' } } } おわりに 今回はHTTP通信におけるヘッダー及びContent-Typeについて紹介させていただきました。 paramsの量にもよりますが、クライアントの記述を大幅に減らすことができる場合もあるので、これからもContent-Typeを柔軟に扱っていければと思います。 今回の内容が少しでも参考となれば幸いです。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ 参考 HTTPヘッダー Content-Type MIME タイプ
はじめに こんにちは、スタメンでエンジニアをしている梅村です。20卒として昨年の4月から正式に入社し、もうすぐ1年が経とうとしています。そんな自分が「 達人プログラマー 」を読んでみた感想と、自分にどの部分が活用できるかを紹介していこうと思います。 新装版 達人プログラマー 職人から名匠への道 作者: AndrewHunt , DavidThomas 発売日: 2017/07/14 メディア: Kindle版 なぜ読んだか プログラマーとしての心得を学び、自分自身を1段レベルアップさせたいと思ったからです。 上記にも書きましたが、スタメンに入社して1年弱が経ちました。業務では主に Ruby on Rails で FANTS の開発を行っており、ある程度の知識は身についてきました。ただ、技術は身についてきたものの、その他のことに関してはまだまだ未熟です。そこで、社内の推薦図書にも上がっていた「達人プログラマー」を読み、実践することで成長したいと考えました。 達人プログラマーとはどんな本か ざっくりと説明すると、「より良いプログラマーになるため・より良い仕事を行えるようお手伝いをする本」です。短いセクションを集めたかたちで構成されており、各セクションで特定の話題を扱っています。技術だけでなく、プログラマーとしての仕事の進め方なども話題にし、効率的・生産的に行動するにはどうしたらいいかが記述されている本です。 ここで、いくつかのセクションを抜粋して、内容や感想・自分に活かすにはどうしたらいいかを紹介していきます。 第1章:達人の哲学 ~ソフトウェアのエントロピー~ エントロピーとは物理学の用語で、無秩序な度合いを表す指標のことです。このセクションでは全宇宙のエントロピーが増加していくのと同様に、ソフトウェアも時間とともに無秩序になっていくと説明しています。 無秩序になる理由として「割れ窓理論」を上げており、悪い設計や質の悪いコードを残しておくと、「自分も適当に作業するだけだ」という考え方が忍び込みやすくなる、と記載されています。 自分自身も、進捗によっては深く考えずにコードを書いてしまったことがあり、それが結果として悪いコードとなり、その後の作業に悪影響を及ぼす、という経験をしたことがあります。 まずは自分の手をつける部分から、割れた窓ではない(適切なメソッド名を記述・責任を明確にした設計)コードを記述し、エントロピーを抑えていきたいと思います。 第2章:達人のアプローチ ~二重化の過ち~ このセクションでは二重化が起きる原因、二重化による問題を説明しています。 これを読んでいる皆さんも、二重化に悩まされた経験があるのではないでしょうか。コード上で同じ知識を2箇所以上に記述しているせいで、複数箇所を修正しなければいけなかったり、仕様が複数箇所にまとまっていることで、どれを正としたらいいかわからない、などはやりがちな問題だと思います。様々な二重化の解決方法が示されていますが、中でも「再利用しやすいようにしておくこと」は一つの解決方法です。コード上の話では、DRYを心がけどこからでも利用できるメソッドにしておく、仕様上の話では、仕様をまとめる一つのドキュメントを作成し逐一履歴を追えるようにして更新していく、などをして、二重化から開放されましょう。 第6章:コーディング段階 ~リファクタリング~ このセクションはリファクタリングとは何か、どうやってリファクタリングをするかを説明しています。 この本では「コードの記述のやり直し、再作業、再設計」を総称して「リファクタリング」と呼んでいます。また、リファクタリングを行うタイミングは、コードがなじんでいないと感じたり、まとめるべき2つの事柄を見つけた場合、としています。まさにコードを書いている際に、上記のことを感じる場面は多いですよね。リリースや納期のことを考えると、安易にリファクタリングに着手できないですが、放置していると、将来問題が発生した場合、余計に修正のために大量の時間が必要になります。なので、気づいたタイミングでリファクタリング・こまめなリファクタリングを行い、将来の問題に対処していきたいですね。 第8章:達人のプロジェクト ~どこでも自動化~ このセクションはビルドやリリース手続き、テストなどの作業の自動化について説明しています。 人間はコンピュータほど繰り返し作業が得意ではないので、ヒューマンエラーによる問題や手作業により工数も多くなります。そこで作業を自動化することで、ヒューマンエラーが無くなり、手作業をなくして他の作業を行い生産性を上げることができます。また、この概念はプログラマー以外でも重要な考えですね。日々の業務を振り返り、繰り返し作業があるなら、それを自動化してみる、ということを実践していこうと思いました。 おわりに 今回は「達人プログラマー」を読み、本の内容・感想・自分に活かすには、ということを中心に紹介しました。本の中で紹介されているツールなどは少し古いので、利用は難しいかもしれませんが、概念としてはとても重要なこと記述されていました。ただ、自分は2016年に発売されたものを読みましたが、去年の11月に 第2版として新しいバージョン が発売されているようなので、こちらでは今に即したツールが紹介されているのではないかと思います。 達人プログラマー ―熟達に向けたあなたの旅― 第2版 作者: デイビット・トーマス , アンドリュー・ハント 発売日: 2021/01/18 メディア: Kindle版 各セクションで完結しているとはいえ、分厚い本なので、自分自身まだまだ理解できていない部分もあります。プログラマーとして迷ったときに読み返すなどして、理解を深めていきたいです。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
モバイルアプリグループでおもにAndroidアプリの開発を行っている @sokume です。 Android開発者の方であれば興味関心の尽きない、 Android OS 12 Developer Preview 1 が2/18日に公開されましたね。 毎年の事ではありますが、2021年の新OS Android 12への対応にむけて少しずつ検討をすすめていかないとならない時期がやってきました。 この記事では、新OS Android 12への対応や、2021年平行にして気になる更新などをピックアップしていこうと思います。 Android12 公式情報 以下に公式情報が記載されています。 Android 12 Developer Preview Android 12 Behavior changes: all apps Android 12 Features and APIs Overview スケジュール Android 12 のリリーススケジュールは以下のようになるそうです。 https://developer.android.com/about/versions/12 より 一昨年までは 5月の Google I/O や 10月の made by Google といった大きめなオフラインを軸としたイベントがありましたので、リリース時期がイベントと連動していく感がありました。 昨年と同様のスケジュールになりそうというだと感じたので、今年も8月末〜9月にリリースされる流れになりそうです。 アプリへの変更点 OSが進化するので、その環境を利用するアプリも進化を促されます。 targetSDK Update to Android 12 アプリの targetSdkVersion を Android12用に変更した際の変更点については以下の記事になります。 https://developer.android.com/about/versions/12/behavior-changes-12 キーとなるのは以下の点になりそうです。 Foreground service launch restrictions App components containing intent filters must declare exported attribute Unsafe launches of nested intents 内容としては、アプリのLaunch部分に関するセキュリティーやプライバシーの変更がはいるようです。リンク先に詳細がありますので、開発中のアプリがこの変更点の対象となるアプリかどうかチェックしておきましょう。 Update all Apps Android OS 12 上で動作するすべてのアプリに対しての記事は以下になります。 https://developer.android.com/about/versions/12/behavior-changes-all UXに関する点や、フォアグラウンドPushに関する点の変更など、OS全体での変更があるようです。この点もしっかり把握しないとですね。 Android 12 デバイス 今回の発表にあわせて、Android 12の開発者向けプレビュー版がリリースされています。 https://developer.android.com/about/versions/12/download#flash 更新できる機種は以下になります。 Pixel 3 and 3 XL Pixel 3a and 3a XL Pixel 4 and 4 XL Pixel 4a and 4a (5G) Pixel 5 更新方法も Android Flash Tool を利用した更新と、自分のadb環境を利用した更新の2パターンが用意されています。 https://developer.android.com/about/versions/12/download 注意です!いつものことですが、自身の判断でデバイスのバックアップを取った後に更新を行うようにしましょう。クリーンインストールから実行されます。 私もPixel 4 XL を Android 12開発者プレビュー版 に更新してみました。利用した感じは大きくAndroid 11からの大きな変更はそこまで感じませんでしたが。今後いろいろと使って何らかの違いがわかってくるのかなと思っています。 直近の問題は、やはり動かなくなった一部のアプリをどうするかーという点で悩んでおります。 Jetpack Compose は? Android 環境の宣言型UI開発フレームワークとして、Jetpack Compose が昨年は話題になりました。 Jetpack Composeのロードマップ的には今年がリリースの年となる予定です。 正式にリリースとなることで、Androidの開発フレームワークとしてまた大きな変化をもたらす事が考えられますね。 早い段階から技術的なキャッチアップをすすめて置く必要があるでしょう。 Jetpack Compose https://www.youtube.com/watch?v=U5BwfqBpiWU&feature=youtu.be&t=1324 最後に 2月になり、新OS Android 12情報も出てきたので、これまで以上にアンテナ高く、情報のキャッチアップをしていきたいと思っております。 昨年同様 Android 11 Meetup などを通しての技術情報の共有もあるんじゃないかと思います。 昨年のAndroid 11の更新については こちら にまとまっております。ご興味のあるかたはどうぞ。 株式会社スタメンでは一緒に働くエンジニアを募集しています。 ご興味のある方はぜひ エンジニア採用サイト をご覧ください。 Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、 Creative Commons 3.0 Attribution ライセンスに記載された条件に従って使用しています。
はじめに はじめまして。株式会社スタメンでエンジニアをしております 永井 です。 今回の記事ではReactでメモ化によるパフォーマンスを意識した実装方法について書きたいと思います。 なぜパフォーマンスを意識した実装が大切なのでしょうか。 なぜなら、ユーザーのある操作に対するレスポンスの速度を高めることは、UXの文脈において非常に重要な要素だからです。例えば、100ms未満のレスポンスに関してはユーザーは瞬時に感じられますが、100ms ~ 300msではすでに遅いと感じてしまいます。遅いことにストレスを感じたユーザーは、別のサービスにリプレイスしてしまうかもしれません。 Reactでパフォーマンスを出すには、バンドルサイズを減らすなど、いくつか方法はありますが、基本的な戦略としては不要なレンダリングを抑えることだと思います。 この不要なレンダリングを抑制するためには、Reactがどのように機能するかを理解する必要があります。理解しないまま改善を行うと、却ってパフォーマンスに悪影響が出る可能性もあります。 そのため、パフォーマンス改善に繋がるメモ化等のメソッドを説明する前に、このReactがレンダリングにおいてどのように機能しているかを説明したいと思います。 はじめに 仮想DOMによるReactの更新処理 Reactのメモ化 React.memo useCallback useMemo 最後に 仮想DOMによるReactの更新処理 ReactではDOMの更新処理を、仮想DOMによる差分更新処理に任せることで、パフォーマンスを高めています。 具体的に言うと、実際のDOMをJavascriptオブジェクトの形式に変換したツリーデータをメモリ上に作成し、コンポーネントの状態に変更がある度に、実際のDOMを更新するのではなく仮想DOMを更新します。 更新された仮想DOMと古い仮想DOMを比較し差分を検出することで、実際のDOMにレンダリングを行います。 こうすることで実際のDOMでは必要最低限の箇所のみレンダリングを行うことが可能になります。 例えば以下のようなコードがあるとします。 const Page = () => { return ( < div > < p > Counter App < /p > < Counter / > < /div > ) } const Counter = () => { const [ count , setCount ] = useState ( 0 ) return ( < div > < span > { count } < /span > < button onClick = { () => setCount ( count + 1 ) } > + < /button > < /div > ) } 仮想DOMとしては、初回レンダリング時にPageとCounterコンポーネントで記述されたJSXを元に、Javascriptで構築した仮想DOMツリーを生成します。そして、 + を押すと、 count のstateが更新されてレンダリングが走り、 count の箇所が 2 となった新しい仮想DOMツリーを生成します。そして新旧2つの仮想DOMツリーを比較して差分を検出し、差分があった箇所である <span>{count}</span> だけを実際のDOMに反映します。 このように、仮想DOMの概念によって必要最低限の箇所のみDOMを変更することができます。 Reactのメモ化 仮想DOMによる差分更新によって、変更箇所のみをレンダリングすることができます。しかし、配下のコンポーネントが再描画されるため、不必要な箇所まで再レンダリングされてしまいます。 そこで、Reactによるメモ化によってコンポーネントに変更がない場合はレンダリングされないようにしましょう。 React.memo Reactの高階関数である React.memo はコンポーネントをメモ化する上でよく使われる手法です。 例えば以下のようなコードがあるとします。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child / > < /div > ) } const Child = () => { return ( < div > < span > I am Child < /span > < /div > ) } この時、 <input //... /> に文字を入力すると setParentName が発火して name が更新されます。stateが更新されるのでParentコンポーネントはレンダリングされますが、Childコンポーネントはどうでしょうか? 試しに console.log('child') を仕込んでみましょう。 const Child = () => { console.log ( 'child' ) return ( < div > < span > I am Child < /span > < /div > ) } 再び <input //... /> に文字を入力してみましょう。すると... => child 「child」と表示されてしまいました。つまり、文字を入力する度に発生するParentコンポーネントのレンダリングに付随して、Childコンポーネントも毎回レンダリングされてしまっているのです。 本来であればこのレンダリングは不要なので、パフォーマンスを考慮するのであれば防ぎたいレンダリングです。 このレンダリングを抑えるために、 React.memo を使います。 const Child = React.memo (() => { console.log ( 'child' ) const return ( < div > < span > I am Child < /span > < /div > ) } ) このコードでは React.memo でコンポーネントをラップすることで、Childコンポーネントに渡すpropsに変更がない場合に、レンダリングをスキップしています。Parentコンポーネントにある <input //... /> で再び文字を入力してみると、コンソールには何も出力されていないことが確認できると思います。 この React.memo でpropsの前後の値を比較してレンダリングするかを決定している訳ですが、この比較は浅い比較で行われます。所謂、オブジェクトのインスタンスにおける参照が異なるかどうかを見ています。 ※ React.memo は第2引数に何も指定しないと、デフォルトでは浅い比較で行われます。第2引数に比較関数を渡すことでレンダリングをカスタムで制御することができますが、基本的には等価性のチェックにはコストが掛かるので避けたいです。 このように、 React.memo を使用することで、本来変更されていないコンポーネントのレンダリングを抑えることができますが、一つ落とし穴があります。 次のコードを見てみてください。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) const [ childName , setChildName ] = useState ( '' ) const childNameHandler = ( e: React.ChangeEvent < HTMLInputElement >) => { setChildName ( e. target .value ) } return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child name = { childName } onChange = { childNameHandler } / > < /div > ) } const Child = React.memo (( { name , onChange } ) => { console.log ( 'child' ) return ( < div > < span > child is { name } < /span > < input type= "text" onChange = { onChange } / > < /div > ) } ) 変更した箇所としては、Childコンポーネントにstateの name と関数の childNameHandler propsで渡しています。 このコードで再びParentコンポーネントにある <input //... /> に文字を入力してみましょう。すると... => child React.memo でメモ化しているのにも関わらず、再びレンダリングされてしまいました。この原因は、 childNameHandler 関数にあります。 アロー関数はレンダリングの度に新しい関数オブジェクトを生成しますが、この関数オブジェクトの再生成によって、propsとして渡している childNameHandler の参照が変更されてしまい、Childコンポーネントがレンダリングされてしまうのです。( (() => {}) !== (() => {}) であるため) これを防ぐ方法として useCallback があります。 useCallback useCallback を用いて改善したコードは以下のようになります。 const Parent = () => { const [ parentName , setParentName ] = useState ( '' ) const [ childName , setChildName ] = useState ( '' ) const childNameHandler = useCallback (( e: React.ChangeEvent < HTMLInputElement >) => { setChildName ( e. target .value ) } , [] ) return ( < div > < span > parent is { parentName } < /span > < input type= "text" onChange = { e => setParentName ( e. target .value ) } / > < Child name = { childName } onChange = { childNameHandler } / > < /div > ) } useCallback は、メモ化された関数オブジェクトを返すhooks APIです。第2引数にしている配列は依存配列で、配列内のいずれかの値が変更されると、新しく関数オブジェクトを生成します。 useCallback を使用する場合は、基本的に React.memo によって最適化されたコンポーネントにpropsとして渡す場合に限定するべきです。 React.memo を使用していないコンポーネントに useCallback によってメモ化した関数を渡したとしても、親コンポーネントがレンダリングされると子コンポーネントはレンダリングされてしまうからです。 また、 React.memo によってメモ化されたコンポーネントに渡さない場合にも useCallback でメモ化するのは避けた方が良いと言われています。これは useCallback の実行コストは関数オブジェクトの再生成のコストよりも高いと言われているからです。 useMemo useMemo は関数の返り値をメモ化する際に使用します( useCallback は関数自体をメモ化します) 例えば、以下のような関数があるとします。(こんなコードは現実には存在しないと思いますがあくまでサンプルということで) const someCalculate = () => { let number = 0 ; while( number <= 1000 ) { console.log ( number ) number ++ } return count * number } 非常にシンプルなコードですが、変数 number が1000になるまで1ずつ足していき、最終的に変数 count と乗算するというものです。 useMemo ではこの計算結果をメモ化することができ、計算自体をスキップすることができます。(以下のコード) const memorized = useMemo (() => { let number = 0 ; while( number < 1000 ) { console.log ( number ) number ++ } return count * number } , [ count ] ) useMemo の依存配列には count を入れています。countが変化すると再計算する必要がありますが、 count が不変の場合は計算結果をメモ化して利用することができます。 最後に Reactでのメモ化について今回書きました。普通にReactで実装していても画面自体は出来てしまうのですが、Reactのレンダリングやメモ化について知らないと、実はかなりパフォーマンスが悪い実装になってしまうことは往々にしてあると思います。 弊社のプロダクトであるTUNAGでも、フロントエンド領域においてパフォーマンスを最適化しきれていない部分がまだまだあるので、徐々に最適化できればと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
こんにちは。スタメンでTUNAGやFANTSのモバイルアプリ開発を担当している @temoki です。 先週、Twitter iOSアプリで使用されているテキストエディタが TwitterTextEditor というOSSとして公開され、iOSアプリエンジニアの間で話題になりましたね。以下がTwitter公式のエンジニアリングブログによる紹介記事です。 blog.twitter.com 私はTUNAG iOS/Androidアプリの開発の中で、メンションや絵文字のショートコード入力を備えたテキスト入力機能の実装に苦労してきていることもあり、このOSSの公開はとても興味をそそられるものでしたので、早速試してみることにしました。今回のブログでは、実際にこの TwitterTextEditor を利用するという観点で書きたいと思います。 以降の内容は TwitterTextEditor v1.0.0 時点での内容となります。TwitterTextEditor の概要につきましては、上記の公式ブログやそれを日本語で紹介されている以下の記事をご覧ください。 TwitterがiOSアプリ向けに新しいオープンソースのテキストエディタAPI「Twitter Text Editor」を発表 - GIGAZINE TwitterTextEditorの機能 公式ブログでは TwitterTextEditor の機能として以下の5つについて挙げられていますので、1つ1つ実際のコードなども交えながら紹介していきます。 Easy delegate-based APIs Robust text-attribute update logic Additional text editing events Safe event handling for text input Support for recent versions of iOS Easy delegate-based APIs TwitterTextEditor は UIKit と同じようなデリゲートベースのAPIが提供されています。例えばテキスト入力の開始・終了のイベントは TextEditorViewEditingDelegate というプロトコルを実装することでハンドルすることができますが、これは UITextViewDelegate のそれとほぼ同じになっていることがわかります。 public protocol TextEditorViewEditingDelegate : AnyObject { func textEditorViewShouldBeginEditing (_ textEditorView : TextEditorView ) -> Bool func textEditorViewDidBeginEditing (_ textEditorView : TextEditorView ) func textEditorViewDidEndEditing (_ textEditorView : TextEditorView ) } というのも TwitterTextEditor は UITextView を内包しているため UITextView で提供されている機能はほぼ網羅されているのです。そのため、既存のプロジェクトですでに UITextView を組み込んでテキスト入力を実装している箇所も容易に置き換えることができそうです。 TwitterTextEditor はイベントの種類に応じて複数のプロトコルに分割されています。 textEditorView.font = UIFont.systemFont(ofSize : 15 ) textEditorView.keyboardType = . default textEditorView.textContentInsets = . init (top : 10 , left : 10 , bottom : 10 , right : 10 ) textEditorView.placeholderText = "メッセージを入力" // プレースホルダー対応😂 textEditorView.editingDelegate = self そして個人的に嬉しいのは、プレースホルダーの表示に対応している点です。同じ UIKit の UITextField はプレースホルダーの表示に対応しているのに、なぜか UITextView は対応していなくて、泣く泣く実装する...ということもなくなりますね。 Robust text-attribute update logic ここからが TwitterTextEditor が本領発揮する部分となります。TwitterTextEditor はテキスト属性を更新するためのAPIを提供しており、例えばシンタックスハイライトのような機能を実装しやすくなっています。 Twitterアプリでいうとメンションやハッシュタグのハイライト表示ですね。例えば Markdown テキストの入力に対する簡単なプレビュー機能なんかにも応用できそうです。試しに二つのアスタリスクで囲んで強調表示する記法( **text strong emphasis** ) で実装してみます。 テキスト属性を更新できるタイミングで TextEditorViewTextAttributesDelegate プロトコルのメソッドが呼び出されますので、ここで属性を更新して返します。更新結果は completion ハンドラを介して返すことになるので、バックグラウンドでテキストの解析と属性更新を行えるのがポイントです。 extension EditorViewController : TextEditorViewTextAttributesDelegate { func textEditorView (_ textEditorView : TextEditorView , updateAttributedString attributedString : NSAttributedString , completion : @escaping (NSAttributedString?) -> Void ) { // バックグラウンドでテキストを解析して属性を更新する DispatchQueue.global().async { // テキストの解析 let regex = try ! NSRegularExpression(pattern : "(\\*+)(\\s*\\b)([^\\*]*)(\\b\\s*)(\\*+)" , options : [] ) let stringRange = NSRange(location : 0 , length : attributedString.length ) let matches = regex.matches( in : attributedString.string , options : [] , range : stringRange ) // テキスト属性の更新 let newAttributedString = NSMutableAttributedString(attributedString : attributedString ) newAttributedString.removeAttribute(.font, range : stringRange ) newAttributedString.addAttribute(.font, value : UIFont.systemFont (ofSize : 15 ), range : stringRange ) for match in matches { newAttributedString.addAttribute(.font, value : UIFont.boldSystemFont (ofSize : 15 ), range : match.range ) } // メインスレッドで更新結果を返す DispatchQueue.main.async { completion(newAttributedString) } } } } 実行結果の動画です。 いい感じですね。ただ、Markdown は多くの記法があり、編集するテキストのサイズも大きくなる可能性が高いです。実際に Markdown エディタを実装する場合、このTwitterTextEditorの機能がすべてを解決するものではないことを理解しておく必要があります。開発者の @niw さんもTwitterで次のようなことをおっしゃっています。 Nothing prevents to support it! (or probably Markdown grammar itself is, tho.) However, a partial attribute update logic may be needed, depends on the expected size of editing text. — Yoshimasa Niwa (@niw) 2021年1月26日 Additional text editing events Twitterは世界中で利用されているアプリですので、あらゆる言語での入力に対応しなければなりません。そのため、TwitterTextEditor には多言語への対応を考慮したテキスト入力イベントが追加されています。以下がそのイベントです。 入力中の言語が切り替わった時のイベント テキスト入力の方向が切り替わった時のイベント(アラビア語などの Right-to-left writing な言語への考慮) これは TextEditorViewTextInputObserver プロトコルとして提供されています。このイベントをトリガーに、入力のためのUIの切り替えなどに使われることを想定されているようです。 public protocol TextEditorViewTextInputObserver : AnyObject { func textEditorView (_ textEditorView : TextEditorView , didChangeInputPrimaryLanguage inputPrimaryLanguage : String ?) func textEditorView (_ textEditorView : TextEditorView , didChangeBaseWritingDirection writingDirection : NSWritingDirection ) } Safe event handling for text input 例えば、入力済の文字数を表示したり、入力中のテキスト内容に応じた入力サジェストなどを行うためには、ユーザーのテキスト入力イベントを使用します。 UITextView でこのイベントをハンドリングするには、 UITextViewDelegate の以下のデリゲートメソッドを使うことになります。 func textView (UITextView, shouldChangeTextIn : NSRange , replacementText : String ) -> Bool func textViewDidChange (UITextView) TwitterTextEditor ではこれらよりも安全にテキスト入力イベントをハンドリングするための TextEditorViewChangeObserver プロトコルが提供されています。 TwitterTextEditor のソースコードの中には // UIKit behavior ~ というコメントで、UIKit の動作の問題点とそのワークアラウンドといった情報がたくさん記述されており、これらをふまえた上で安全なイベントを提供してくれるというわけですね。 public protocol TextEditorViewChangeObserver : AnyObject { func textEditorView (_ textEditorView : TextEditorView , didChangeWithChangeResult changeResult : TextEditorViewChangeResult ) } iOSアプリでのテキスト入力の難しさやその解決方法については、作者の @niw さんが昨年の iOSDC Japan 2020 でも発表されていますので、ぜひご覧いただきたいです。 www.youtube.com このプレゼンテーションの中では、テキスト入力を伴うアプリを開発する場合、ソフトウェアキーボードの表示状態の変更についても気にしながら実装する必要があるという点についても言及されており、これについては同じく @niw さんが公開されている KeyboardGuide というライブラリでスマートに解決することができるので、こちらもオススメです。 GitHub - niw/KeyboardGuide: A modern, real iOS keyboard system notifications handler framework that Just Works. Support for recent versions of iOS TwitterTextEditor はサポートバージョンとして iOS 11 以降という良心的な設定になっています(Twitter iOS アプリの最小バージョンが iOS 12 なのに!)。 そして、サポートされているパッケージマネージャーも CocoaPods、Carthage、Swift Package Manager と一般的なものは全て揃っているので導入で困るということはなさそうですね。 おわりに 世界中で利用される Twitter iOS アプリ。そのテキスト入力を支えるOSS、 TwitterTextEditor についてご紹介いたしましたが、いかがでしたでしょうか。 このブログを書くにあたって TwitterTextEditor のソースコードを眺めてみましたが、とても勉強になる部分が多かったり、作者の実装の苦労が垣間見えたりしました。また、自分が TUNAG アプリの開発で実装したコードに類似しているところをいくつか見つけて親近感が沸いたりもしたので、改めて人のソースコードを読むのは重要なことだなと感じました。 最後になりましたがスタメンでは TUNAG や FANTS そして新しい事業におけるプロダクト開発を一緒に牽引してくれる仲間を募集しています。エンジニアに限らず、デザイナーやプロダクトマネージャー職も募集中ですので、興味のある方は下記の応募からご連絡ください! エンジニア採用サイト インハウスデザイナーWanted!名古屋で注目のベンチャーで活躍しませんか 急成長する大規模 SaaSプロダクトのプロダクトマネージャー募集!!
スタメンでエンジニアをしている 田中 です。 今回は決済プラットフォームであるStripeのサブスクリプションを扱う際に遭遇した問題について、発生した事象とその原因、および対策方法についてご紹介します。 なお、本記事ではStripeのサブスクリプションについての詳細は説明いたしません。また、対策方法についてはRubyのコードで記載します。RubyでStripeのサブスクリプションを扱う場合については、以下の記事にて紹介しているのでよろしければご参照ください。 【Ruby on Rails】Stripeのサブスクリプションで試したことをまとめてみた また、以前にも同様のタイトルで記事を投稿しましたが、今回は内容が若干異なります。ただ、前回の記事の知識があると理解が早くなると思うので、この機会に読んでいただければと思います。(タイトルの括弧の中身が異なります) 【Stripe】サブスクリプションの支払いタイミングが特定日時においてズレる問題について(月末版) 前提 本記事で扱うサブスクリプションは請求期間が月次のものです サブスクリプションの支払い日について、通常、翌月に同じ日が存在しない場合は自動的にその前の日を指定してくれます 例 5/31 → 6/30 8/31 → 9/30 参考 https://stripe.com/docs/billing/subscriptions/billing-cycle 発生した事象 以下の画像のように、同じ日(1日)にサブスクリプションを開始しましたが、2回目の支払いのタイミングがズレてしまうということがありました。 2回目の支払いが初回支払いと同じ日(1日)に行われているケース 2回目の支払いが初回支払いと異なる日に行われているケース そのため、例えば支払い成功時のWebhookにて何かしらの処理をする場合に、このズレによって影響が発生する可能性が大いに考えられます。 発生原因 先日ご紹介した記事と同様、 billing_cycle_anchor とタイムゾーンの関係によるものだと思われます。 以下は、過去記事の引用です。 ここで、 billing_cycle_anchor について説明します。 billing_cycle_anchor とは支払い開始の起点となる日時のことです。たとえば、毎月1日に決済したい場合、サブスクリプション作成時に billing_cycle_anchor に翌月の1日を指定することで、毎月1日払いを実現することが出来ます。特に指定をしなければ、サブスクリプション作成時刻 = billing_cycle_anchor となります。 参考 https://stripe.com/docs/billing/subscriptions/billing-cycle 発生原因についての詳細は下記の通りです。 Stripeのシステムは、UTC基準で動作する 日本時間(JST)でサブスクリプションを作成する場合に、UTCの時刻から9時間の差がある そのため、UTC基準では月末だが、日本時間だと翌月と判定されてしまうため今回の問題が発生する これだけだとよく分からないため、具体例を挙げて説明します。 具体例 (1)12/1 午前0時にサブスクリプションを作成した場合 ・日本時間「2020-12-01 00:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-12-01 00:00:00 2020-11-30 16:00:00 2回目 2020-12-31 00:00:00 2020-12-30 16:00:00 3回目 2020-01-31 00:00:00 2021-01-30 16:00:00 4回目 2020-03-01 00:00:00 2021-02-28 16:00:00 (2)11/1 午前0時にサブスクリプションを作成した場合 ・日本時間「2020-11-01 00:00:00」にbilling_cycle_anchorを指定 支払回数 ダッシュボード上の挙動(JST) 実際の挙動(UTC) 1回目 2020-11-01 00:00:00 2020-10-31 16:00:00 2回目 2020-12-01 00:00:00 2020-11-30 16:00:00 3回目 2021-01-01 00:00:00 2020-12-31 16:00:00 4回目 2021-02-01 00:00:00 2020-01-31 16:00:00 (1)に関してはUTC基準の場合にbilling_cycle_anchorが2020-11-30 16:00:00で設定されてしまうため、次回以降の支払いサイクルが以下の通りになってしまいます。 31日がある月は31日に支払い 31日がない月は1日に支払い (2)に関してはUTC基準の場合にbilling_cycle_anchorが2020-10-31 16:00:00で設定されるので、次回以降の支払いサイクルは毎月1日支払いとなります。 上記のことから、日本時間において以下の日時にサブスクリプションが作成されると今回の問題が発生すると考えられます。 前月が31日までない月の1日(3月1日, 5月1日, 7月1日, 10月1日, 12月1日) 午前0時から午前9時の間 対策方法 特定日時でサブスクリプションを作成した場合、 trial_end を用いて次回以降の支払い日時をずらすことで対応します。 詳細については 前回の記事 をご確認ください。 おわりに 今回はStripeのサブスクリプションを扱う際に遭遇した問題についてご紹介しました。 時差および月によって日数が異なることを考慮しないと想定外の挙動が発生する可能性があるので、取り扱う際にはその点を頭に入れて実装していきましょう。 最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。
こんにちは、スタメンでVPoE兼プロダクト部の部長 をしている小林です。 1月も前半が終わり、スタメンでは新しいチームやプロジェクトが立ち上がり、本格的に2021年がスタートしています。 昨年末に、CTOの松谷が技術を中心に「 スタメン開発チーム 2020年の振り返りと2021年の展望 」を投稿しましたので、私は組織面を中心に2021年の方向性をご紹介しようと思います。 現在のプロダクト部について プロダクト部は、2021年1月18日時点で、総勢26名となり、エンジニア、デザイナー、プロダクトマネージャーが在籍する「ものづくり」の部門となっています。 2016年、オフィスに一人で rails new から始めたところから、Rails, React, Swift, Kotlin, AWS/GCP と多岐にわたる技術を用いて、デザイナーとプロダクトマネージャー(PdM)も加わって、TUNAG と FANTS の2つの事業を創る部門に成長しました。 組織(会社)で仕事をする意味は一人ではできない大きな仕事をチームで成し遂げるためにあると言われます。そして、スタメンの経営理念は、「一人でも多くの人に、感動を届け、幸せを広める。」です。 私たちプロダクト部にとって、大きな仕事とは、TUNAGやFANTSなど、スタメンが提供するプロダクト(≒事業)によって、より多くの人に感動を届け、幸せを広めることです。 まさに言葉通り、このチームでなければできないことがたくさんあると思えるほど、たくさんのことが実現できるチームになってきました。集ってくれた皆さんに感謝です。 そんなプロダクト部の内訳としては下記になります。 プロダクト部の内訳 このグラフのとおり、スタメンのプロダクトチームの特徴は下記になります。 エンジニア、デザイナー、プロダクトマネージャーで構成されるフルセットのプロダクトチーム。 役員/社員は計24名で、エンジニア20名、デザイナー2名、プロダクトマネージャー2名。 加えて、学生インターン(アルバイト)のエンジニアが2名。 若手と経験者が混ざって、即戦力と育成の両立を重視しているチーム。 若手は、経験を問わずポテシャルの高い人材を採用し、事業とともに成長している。 経験者は、その経験を活かして即戦力として専門性をもたらし、チームを底上げしている。 ただし、まだまだ満足できる専門性や世の中への影響力ではありません。企画、デザイン、実装において、もっと良いチームになり、良いプロダクトを世に提供する必要があります。 以下では、そのためにVPoEとして考えていることをご紹介したいと思います。 機能を増やすのでなくUXを上げる SoE (System of Engagement) と SoR (System of Record) という言葉がありますが、事業として TUNAG は、機能を増やす SoR から UXを向上する SoE の段階に来ており、機能を追加するよりも、各機能の使い勝手や効果を最大化することを意識して施策を実施しています。 ただし、BtoB の SaaS である TUNAG は、対象とする顧客の業態や規模が多様で、HR Tech領域は法律等で決まった形が無いため、解決すべき課題や要望も多岐に渡ります。 また、TUNAG は、300社以上のお客さまに導入いただき、10人程度から1万人以上の規模の組織で利用されています。全体的に、要件定義が難しく、機能も多く複雑で、仕様、デザイン、実装も複雑になりがちで、プロダクトマネジメントが難しいプロダクトです。 そんな中、今年からプロダクトマネージャーが増えて二人となり、プロジェクトの運営に加えて、カスマーサクセスやセールスとの連携、数値分析や深い議論に時間を割くことができるようになってきました。デザイナーの増員も進めており、プロジェクトの早い段階からデザイナーを巻き込んで、仮説検証やUI/UXの向上に取り組んでいます。 まだまだ向上余地がたくさーんありますが、次のような取り組みにより、プロダクトの作り込みが深くなってきています。 カスマーサクセスとセールスとの連携による課題や顧客ニーズの把握 データを用いた定量的な分析に基づく意思決定 デザイナーとプロダクトマネージャーの連携による企画とプロトタイプによる検証 エンジニアや社員も巻き込んだレビューをプロジェクトの節目で複数回開催 使い勝手の良いUIと開発効率を両立するためのデザインシステムの構築 技術的課題と事業推進のバランス 2016にTUNAGを創り始めてから4年半経過しました。途中事業の方針転換(ピボット)もしていますし、相次ぐ拡張によって、システムは複雑となり、技術的負債も蓄積しています。 これまでいくつかのプロジェクトで負債の解消を行っていますが、まだまだたくさんの負債が残っており、障害や不具合の発生や、開発速度の低下など弊害も見え始めています。同時にシステム全体のスケーラビリティやセキュリティを向上するような取り組みも議論されており、近くプロジェクトも立ち上がる予定です。 ただしスタメンのような小さなチームでは、開発リソースにも限りがありますし、技術的な依存関係や各エンジニアの得意な技術も異なり、同時に実施可能なプロジェクトは限られています。 そのため、定期的にロードマップ会議を開催し、この先半年から1年先ぐらいの長期的な視点での議論を行い、機能の改善などの事業を発展させる施策と、技術的負債の解消やアーキテクチャー変更などの技術的な施策の優先順位を役員が集まって意思決定しています。 最近の例だと、TUNAGのメイン画面であるタイムライン(React)を、今年の夏に大きく機能を追加(変更)する前に刷新するプロジェクトを進めることになり、昨年末から着手しています。 このように、技術的課題と事業推進のバランスを取りながら、プロダクト部全体を運営していく必要があります。 プロダクト部の行動指針である Star Code にあるように、様々な「問題を見極め」、事業の成長のために「ユーザー目線で考え」ながら事業を進める。障害や不具合などが発生したら「失敗に向き合い」、技術的負債の解消やアーキテクチャー変更を行う。こんな、チーム運営をしています。 小さなチームと権限移譲 組織面でも発展を続けています。2021年に入り、デザイナーの増員に伴いデザインチームができ、プロダクトマネージャーも増えて、チームとしてプロジェクト運営を行うようになりました。エンジニアは20名となり6つのチームに所属しています。 全体として8チームが部長の下にフラットに配置された「文鎮型組織」になっており、部長の直下にすべてのチームを配置することで、迅速な意思疎通と意思決定を行うようにしています。 チーム編成としては、専門的な小さなチームの連携により、効率と成果の最大化を意識しています。また、小さなチームにすることで、マネージャーがプレイヤーとしても活躍&成長できるようにしています。 フラットな組織にすることで、意思疎通はしやすい反面、部長(小林)が多くのチームを管轄することになりマネジメントが疎かになる懸念がありますが、技術面を CTOの松谷に移譲し 、プロジェクト運営をプロダクトマネージャーと各チームのマネージャーに委ねることで、小林は組織(採用と人事)と事業の意思決定に専念するようにしています。 今後は、さらなる組織拡大に備えて各分野での権限委譲を進め、小林は全体最適にフォーカスしていく予定です。 全員が成長し貢献する組織へ 主に、組織と事業について、課題と感じていることを書いてみました。各分野で課題がたくさんですが、逆に言えば伸びしろに溢れているとも考えています。 前述したようにスタメンのプロダクト部は、若くて経験の浅いメンバーが多い割に、事業とシステムが複雑な大規模プロダクトを作っています。 エンジニアは、大規模SaaSプロダクトの開発を少人数のチームで主力メンバーとして担当することができますし、難易度の高い負荷対策や大規模リファクタリングといった貴重な経験を得られます。 デザイナーは、インハウスデザイナーとしてのPCとモバイルでのUIデザインはもちろん、紙媒体やプロモーションサイト、デザインシステムの構築など様々なデザインと共に、プロジェクト初期からの参加による上流工程に参加することができます。 プロダクトマネージャーは、HR Tech BtoB SaaS という難しい分野のエンゲージメント経営という前例の無いプロダクトに対して、プロダクトマネジメントのすべてを思う存分することができます。 これらの各専門分野とともに、組織面では20代のマネージャーがたくさん誕生し、小林と一緒になって組織(採用、育成、アサイン、評価)と事業のマネジメントを経験しています。 このように、事業も技術も組織も伸びしろがたくさんあり、まだ組織が小さいこともあって、非常に裁量と経験値が大きな環境となっています。良いチームで良いプロダクトを作っており、きっと皆さんにとって人生の代表作となるプロダクトを作っていけると思っています。 責任が大きい分、大変なときもあるかと思いますが、引き続き全員で創意工夫し、ひたむきに良いプロダクトを作っていきましょう! 新しい仲間も募集中です スタメンでは、引き続き、エンジニア、デザイナー、プロダクトマネージャーを採用しています。これまでも、少年マンガの新キャラクターのように、新しい仲間がピンチを救い、チームに新しい力をもたらしてくれました。 今年もたくさんの仲間が加わって、更に強いチームになる予定です。こんな環境に魅力を感じてくださった方は、ぜひ下記から応募していただけないでしょうか。 株式会社スタメン エンジニア採用サイト インハウスデザイナーWanted!名古屋で注目のベンチャーで活躍しませんか 急成長する大規模 SaaSプロダクトのプロダクトマネージャー募集!! お待ちしております!!!
はじめに はじめまして。株式会社スタメンでエンジニアをしております、永井( @0906koki )です。 以前の記事 では、筋トレを週5でしていると書いていましたが、今は週2に減らして体をメンテナンスしています。 今回の記事ではRailsとWebpack、そしてReactを使って、webpack_dev_serverによるHot Module Replacement(以下 HMR)を実装する方法について書きたいと思います。 軽くwebpack_dev_serverとHMRの説明をすると、 webpack_dev_server とはWebpackを利用した開発環境向けWebサーバーで、Webpack管理内の静的アセットを配信することができます。また、 HMR とはWebpackの提供する仕組みで、ブラウザのリロードをせずにJavascriptの変更内容を画面に反映するツールです。 弊社のプロダクトである TUNAG ではサーバーサイドをRails、フロントエンドをReactとTypeScriptで実装しており、フロントエンドのビルドファイルを、Railsのsprocketsでコンパイルしてerbで読み込ませていました。 プロダクトの成長に比例してWebpackのbundleサイズも肥大化していき、それに起因してsprocketsのアセットコンパイルに掛かる時間も増加し、フロントエンド開発環境化においてスピード感を持って開発することが難しくなってきました。 このままでは、プロジェクトの進行に大きな悪影響を及ぼすことが目に見えてきたので、問題を解消するために、 sprocketsによる無駄なコンパイルをなくし、webpack_dev_serverによるコンパイルのみにする HMRを導入し、リロードせずとも変更内容が反映されるようにする この2つを軸として、フロントエンド開発環境改善プロジェクトをスタートしました。 ※ この改善プロジェクトの内、webpack_dev_serverとRailsの連携部分に関しては、スタディストさんの 「フロントエンド原理主義者が目論んだ脱webpacker」 が非常に参考になりました。 実装の手順 webpack_dev_serverを導入して、HMRを適用する手順は以下の通りです。 webpack_dev_serverのインストール webpack_dev_serverとmanifestPluginの設定 webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装 Railsのプロキシ設定 react-hot-loaderの導入と実装 ※ TUNAGではRailsのWebpackerを使わず純粋なWebpackを元々使用していたため、Webpackで実装する前提で話を進めます。 webpack_dev_serverのインストール webpack_dev_serverに必要なpackageを追加します。 $ yarn add -D webpack_dev_server webpack-manifest-plugin ※ webpack-manifest-pluginは、生成したビルドファイルパスの管理ファイルとして使用します。 webpack_dev_serverとmanifestPluginの設定 設定は以下のようにしています。(loader等の設定は省略しているので、適宜追加してください) const path = require( 'path' ); const WebpackManifestPlugin = require( 'webpack-manifest-plugin' ) const outputPath = path.resolve( '../../public/packs' ) module.exports = (env, argv) => { return ( { entry: { bundle: [ 'webpack-dev-server/client?http://localhost:8080' , './src/index.tsx' ] } , output: { path: outputPath, publicPath: 'http://localhost:8080/packs' , filename: '[name].js' , } , plugins: [ new WebpackManifestPlugin( { fileName: 'manifest.json' , publicPath: '/packs/' } ) ] , devServer: { contentBase: 'http://localhost:8080/packs' , port: 8080, hot: true , headers: { 'Access-Control-Allow-Origin' : '*' , } } , } ); } ; ここでは、ビルドファイルの出力先をpublicディレクトリ配下のpacksディレクトリに指定しています。そしてwebpack_dev_serverのcontentBaseに /packs を指定することで、 http://localhost:8080/packs で出力先されたビルドファイルを取得することができます。 また、Webpackのプラグインである ManifestPlugin を使用して、ファイル名と実際に配置されるファイルパスが記述されたマニフェストファイルを生成します。 devServerとmanifestPluginの各プロパティの説明は以下の通りです。 devServer contentBase: 静的ファイルを配置するパスの指定 port: ポート番号の指定(rails serverが3000を使用するので、8080に) hot: HMRの利用 headers: webpack_dev_serverからのレスポンスに任意のヘッダー情報を含める manifestPlugin filename: 生成されるマニフェストファイル名の指定 publicPath: valueにprefixを付与する これでwebpack_dev_serverからアセットを配信する設定が完了したので、早速webpack_dev_serverを立ち上げてみたいと思います。 立ち上げ方は、以下のコマンドを実行するだけです。(package.jsonのscriptsに設定しておくことをオススメします) $ webpack-dev-server --progress --color これで、 http://localhost:8080/packs/◯◯.js にアクセスすると、出力先されたビルドファイルを取得することができます。 ちなみに、 http://localhost:8080/packs/manifest.json で、以下のようなマニフェストファイルも取得できると思います。 { "bundle.js" : "/packs/bundle.js" , } webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装 次に行いたいことは、上記で配信されたアセットをRails側で読み込むヘルパーメソッドの実装です。 コードは以下のようになります。(jsファイルのみを読み込む設定になっていますが、cssファイルも読み込みたい場合は、専用のメソッドを追加してください) module WebpackBundleHelper class BundleNotFound < StandardError ; end def javascript_bundle_tag (entry, **options) return javascript_include_tag entry unless Rails .env.development? path = asset_bundle_path( "#{ entry } .js " ) options = { src : path, defer : true }.merge(options) options.delete( :defer ) if options[ :async ] javascript_include_tag '' , **options end private def asset_host Rails .application.config.asset_host || '' end def dev_server_host " http://localhost:8080 " end def dev_manifest # webpack-dev-serverから直接取得する OpenURI .open_uri( "#{ dev_server_host } /manifest.json " ).read end def manifest @manifest ||= JSON .parse(dev_manifest) end def valid_entry? (entry) return true if manifest.key?(entry) raise BundleNotFound , " Could not find bundle with name #{ entry }" end def asset_bundle_path (entry, **options) valid_entry?(entry) asset_path(asset_host + manifest.fetch(entry), **options) end end javascript_bundle_tagでは、webpack_dev_serverから配信されているmanifest.jsonを取得し、manifest.jsonの中で引数に合致するファイルパスを取得します。 例えば、javascript_bundle_tagの引数に bundle を指定すると、manifest.jsonで bundle に合致するkeyを見つけて、そのvalue( /packs/bundle.js )を取得します。そして、 localhost:3000/packs/bundle.js へリクエストを送る流れです。 しかし、 localhost:3000 ではなく localhost:8080 でwebpack_dev_serverを立ち上げているので、当然のことながら、この段階ではアセットを取得できません。 なので、プロキシをしてRails側がwebpack_dev_serverからアセットを取得できるように設定してあげます。 Railsのプロキシ設定 プロキシの処理は、rack-proxyというGemをRailsに追加して実装しました。 require ' rack/proxy ' class DevServerProxy < Rack :: Proxy def perform_request (env) if env[ ' PATH_INFO ' ].start_with?( ' /packs/ ' ) env[ ' HTTP_HOST ' ] = dev_server_host env[ ' HTTP_X_FORWARDED_HOST ' ] = dev_server_host env[ ' HTTP_X_FORWARDED_SERVER ' ] = dev_server_host super else @app .call(env) end end private def dev_server_host " localhost:8080 " end end ここで行っていることは単純に localhost:3000/packs/ で来たリクエストを localhost:8080/packs/ へプロキシしているだけとなっています。 開発環境下のみでプロキシを行いたいので、developmentのconfigファイルに以下の設定を追加します。 config.middleware.use DevServerProxy , ssl_verify_none : true これでRails側からフロントエンドのアセットを取得することができるようになったので、 http://localhost:3000/packs/manifest.json にアクセスすると、webpack_dev_serverから配信されているマニフェストファイルを取得することができるはずです。 react-hot-loaderの導入と実装 ここまでで、Rails側がwebpack_dev_serverから配信されるアセットを取得できるようになったので、今まで通り、フロントエンドの開発を進めることができるようになったと思います。 ここからは、ReactでHMRを行う方法について解説します。 まず、HMRを行うために react-hot-loader というpackgaeを追加します。 $ yarn add react-hot-loader そして、 .babelrc にも以下の設定を追記します。 { "plugins" : [ "react-hot-loader/babel" ] } 次に、Reactコンポーネントの実装に移ります。 react-hot-loader にあるhot関数に、Reactプロジェクトのトップコンポーネントを引数として渡します。 import React from 'react' ; import { hot } from 'react-hot-loader' import { Todo } from './todo' const App = () => { return ( <> < Todo / > < / > ) } export default hot ( App ) HMRの確認 上記のReactコンポーネントを管理しているWebpackから出力先されるビルドファイルがbundle.jsとすると、先程定義したRailsのヘルパーメソッドの引数にbundleを指定します。 <%= javascript_bundle_tag( ' bundle ' ) %> これでwebpack_dev_serverを立ち上げて、Chromeのコンソールに以下の内容が出ていれば、HMRが有効になっています。 試しに、先程指定したトップコンポーネント配下のコンポーネントをいじってみてください。HMRによって即時に変更内容が反映されるはずです! まとめ webpack_dev_serverとReactにおけるHMRの導入について解説しました。 RailsのAssets Pipelineの仕組みや無数にあるWebpackの設定など、技術的に理解するべき範囲は広く、難しい部分はありましたが、今回のプロジェクトを通じてフロントエンドのコードを触るエンジニアの生産性改善に貢献できたのは良かったです。 弊社CTOの記事 にあるように、事業の成長に伴いエンジニアの人数も増えていくなかで、メンバー全体に関わる開発環境の問題は、プロジェクトを進めていく上で非常に大きな問題です。 すでに顕在化している問題や今後起きそうな課題に対して、エンジニアがその都度問題を提起し、解決に向けて行動することはとても大切なので、これからもそうした意識を持ち続けたいと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
スタメンの松谷( @uuushiro )です。2020年3月末にスタメンのCTOに就任し、約9ヶ月ほどが経ちました。色々と変化の大きかったスタメン開発チームの2020年を、私の目線で振り返ります。そして期待も込めて来年の展望を共有したいと思います! どんな2020年だったか 事業について TUNAG まず、創業事業である TUNAG はリリースして今年で4年目でした。TUNAG は、2017年~2019年までは、「エンゲージメント経営プラットフォーム」として必要な一連の機能を揃えてきました。プロダクトの機能がカバーできる範囲を増やすことに注力してきたこともあり、その一つひとつの機能に関しては、価値提供ができる最小限のものでした。 2020年は、これまで揃えてきた主要な機能の価値・体験を引き上げるような改善や、新機能の追加においてもリリース時点のプロダクトの作り込みレベルを上げることができるようになってきたことで、開発組織として提供できる機能価値が大きくなってきたと感じています(もちろん改善余地は沢山です)。特に、フロントエンドエンジニア・ネイティブアプリエンジニアは、スキルアップと組織化により TUNAG のユーザー体験を向上させることに非常に大きな貢献をしてくれました。 また、TUNAG のメイン機能である、自社に合わせた社内制度を運用できる機能だけでなく、チャット機能やワークフロー機能といった通常業務を行う上で必須のツールを多くのユーザー様に使っていただくようになった年でもあったので、TUNAG へのアクセス数も負荷の特性も変化してきました。開発チームとしてもユーザー様の業務を止めてしまうことがないように、不具合が無いか、ストレスのない応答速度かなど「当たり前品質」を追求する意識が大きく高まっていきました。そして、エンタープライズ企業様の導入において、これまで経験したことのない規模のユーザー数の利用シーンを想定した負荷対策、及びセキュリティ対応を実施し、今後 TUNAG の導入を検討していただける企業様の幅も大きく広がりました。これらのシステムの信頼性向上の取り組みには、インフラチームがプロダクト開発全体をリードしてくれました。 一方で、プロダクトの作り込みレベルの向上と比例して、機能の複雑度も上がってきました。リリース当初は想像もしていなかったようなプロダクトの進化が何度も発生したので、その変化になんとか合わせてきた分の負債が無視できなくなってきています。その結果、アプリケーションコードが複雑になり、それが結果として不具合やサーバー負荷につながることが予測しづらいということも多々ありました。こちらについては、来年時間を確保し、リファクタリング及び、自動テストの拡充を進めていく予定です。 また、TUNAG事業において TERAS という組織診断ツールの提供を開始し、TUNAG と別のシステムとして0から構築しました。TUNAG本体とはアーキテクチャが異なり、SPA(React)とAPIサーバ(Rails)で作られ、サーバーもコンテナを利用したり、デプロイも Blue Green Deployment を利用したりなど、スタメンの近い将来の技術スタックの検証も兼ねて構築しました( TERASのアーキテクチャ )。ここで得られた知見の一部を来年TUNAG本体に適用することで、システムの安定性・開発・運用効率向上などを獲得していきたいと考えています。 FANTS そして今年は新規事業 FANTS をスタートしました。必要な機能が TUNAG と近いので、ソースコードの転用ができた箇所は多かったのですが、ライブ配信機能、課金機能、サロン管理機能、集客機能などオンラインファンコミュニティサービス固有の機能開発を多く行いました。特に課金機能に関しては、システムがお金を扱う初めての事例だったこともあり(クレカ情報は保持していません)、今までにない緊張感の中で開発になりました。二重決済や誤課金、そして決済プラットフォーム側とFANTS側でデータの不整合が発生しないような仕組みをしっかりと時間を掛けて(冷や汗をかきながら)構築した甲斐があったと思っています。責任重大な仕事だったと思いますが、FANTSチームが責任持ってやりきってくれたおかげで、今は FANTS の事業理念達成に向けた多くの機能をスピーディにリリース出来ています。 その他取り組みについて 2020年の、プロダクトロードマップ以外の取り組みについて、特に印象に残っている4つを紹介します。 アラート管理改善 アプリケーションのアラート通知の整理をしました。整理する前は、一日あたりの通知数が多いため、通知に対する集中力が削がれ重要なアラートを見逃しやすくなっていました。その結果、不具合・障害対応の初動の遅れや漏れが発生してしまう可能性が高くなっていました。この問題を私たちは「オオカミ少年アラート」と呼び、問題解決のために「システム思考」のフレームワークを活用しました( システム思考でアラート運用に関する問題を考える )。これにより、アラートの責任範囲及びアクションが明確化し、新しいメンバーも迷わずに監視をすることができるようになったと思います。そして、半年経った今も新しいエラーに対して反応が早くなったと実感しています。ただ最近は、たまにあの「少年」が遊びにきている気がしないでもないので、また問題になる前に継続的に見直していきたいと思います。 開発環境改善 TUNAG の開発環境において、Railsのアセットコンパイルが遅い問題があったのですが、JavascriptなどのAssetの配信の仕組みを、RailsとWebpackで分離させたことで、sprocketsの無駄なコンパイルを排除し、待ち時間を大きく減らすことができました。またフロントエンド開発において、Webpackが提供するHot Module Replacement (HMR)についても適用することができるようになり、今後開発者体験も向上していきそうです。来年以降、フロントエンド開発はますます加速していくのでこの改善は非常にありがたいです。 一方で課題に感じるのは、ずっと同じ環境で開発していると緩やかに遅くなっていく開発環境に対して鈍感になってしまうということ。そして、RailsとWebpackが依存し合っている仕組みだと、両方の技術を理解していないと問題提起しにくいという難しさも感じました。しかし「開発環境が遅い」という事象に対しては、誰でも多少ストレスを感じるはずです。そこに慣れるのではなくプログラマーの三大美徳の一つ、「短気」なマインドを持ち、目の前の開発環境の怠慢さに怒りを感じ、問題を提起し、解決に導くことは非常に重要です。そういったエンジニアがどれだけ組織にいるか?という指標は中長期的にプロダクト部としてのアウトプット量を大きく左右します。大きなことを成し遂げる上で、斧を研ぐことがどれだけ大事なのか。開発環境を1%でも良くすることが、今後人数が増えていく開発組織においてどれだけ大事なのか。ということをチームカルチャーとして浸透させていき、来年はエンジニア全員が「短気」になっていければと思います。 APIドキュメンテーション標準化 APIドキュメンテーション標準化がされる以前は、APIドキュメントは社内wikiに蓄積されていましたが、機能が増えてきたことによってドキュメントの数も増えて管理が難しくなったり、 フォーマットが明確でないので書く人によってばらつきがあるという問題がありました。そこでAPIを提供する側・使う側の両方の立場のエンジニアが協力して、ツールの選定、運用方法の確立及び浸透を推進してくれました。既存資産であるRSpecというテストフレームワークをそのまま活かすことで実装者にほぼ負担のない形で導入でき、Swagger UIをホスティングすることで簡単にドキュメントにアクセスできようにしてくれました。そして、運用する中で出てきた課題も適宜解決してくれました。 RSpec から API ドキュメントを生成する「rspec-openapi」を試してみた JSON:APIのRequestSpecに、jsonapi-rspecを導入する - stmn tech blog このプロジェクトは、技術領域やチームを横断する良い例だったと思います。ドキュメントのズレをいちいち手動で修正するのがめんどくさいと、プログラマーの三大美徳の一つ「怠惰」なマインドで課題に向き合ってくれたおかげだと思います。このような、技術領域やチーム領域の間を埋めるエンジニア、越境するエンジニアといった存在は事業を推進する上で非常に重要になってくるので、今後も働きかけをフォローしていきたいと思います。 失敗から学ぶ取り組み スタメンのプロダクト部には「失敗に向き合う」という バリュー が定められています。2020年も不具合や障害など失敗をしてきましたが、今年を振り返ると組織全体で失敗に向き合う姿勢のレベルが一段上がったなと感じています。ただ失敗を反省するだけでなく、個々人の原因追求・再発防止・未然防止の質が高まり、同じ失敗をしないよう改善できるようになってきていると感じています。障害振り返り会での議論も活発になってきて、本音でオープンに原因を追求し、組織全体が失敗から学ぶカルチャーが浸透してきているなと感じています。 2021年にやること 私個人として、組織全体として2021年を実行したいことは2点です。 技術戦略を描き実行する 全体最適・基準決めに注力していく 技術戦略を描き実行する 2020年を振り返ると、事業の要求に応えながら多くの開発でプロダクトの成長を支え、進化させてくることができました。 一方で私自身、開発組織における「技術戦略」について言語化・発信が足りなかったなという反省があります。「技術戦略」というものは、事業上の要望に120%応えつつ、プロダクトの品質、改善スピード、低コスト実現、採用力、育成力...など事業戦略を加速させる強みとして、開発組織・アーキテクチャ・カルチャーをどの時間軸で達成したいのか、そのために限られたリソースをどう配分したらよいのか、ということを示したものだと考えていますが、それに取り組む動きが弱かったと感じています。「やるべきリスト」は常に目の前にありますがこれは戦略ではないです。事業を牽引するスタメンならではの「技術戦略」の描き筋があるはずで、今後はより具体的に考え、言語化し、発信していきます。そこで必要になってくることは、目に見える課題だけではなく、まだ目に見えないより重要な課題を発見しアプローチすることです。何かをやったことのコストとリターンは見えますが、「見えていない」「やっていない」ことによる機会損失はそれ以上に重く捉えるべきと思っています。見逃しているチャンスが無いように今一度システム・組織・未来の事業の姿を考えて、引き続きプロダクト部のビジョンである「プロダクトで事業を牽引する」の実現を追求していきます。 全体最適・基準決めに注力していく 2020年は、バックエンド領域に関しては私も中心となって技術的な意思決定をしてきましたが、その他フロントエンドやネイティブアプリ領域に関しては、技術選定やその実行タイミングは各技術領域のチームごとに意思決定をお任せしてきました。2021年も同じく、信頼してお願いしていきたい一方で、意思決定のフォローや全体最適となるようにバランスをとることはCTOとしての重要な責任です。今後、人・チーム・事業・技術領域が増える中で、技術的意思決定の基準・価値観を揃えることは重要になってきます。 正直、TUNAG は自分でも全体の技術を把握しきれない規模に成長しているのですが、例えば、プロダクト開発を一時的に止めてでも投資すべき技術的な意思決定の判断を迫られた時に「分かりません」じゃ話になりませんし、理解して投資が必要と判断できれば、事業責任者に説明して納得してもらえるように働きかける責任がCTOにはあります。実際、私が全ての領域を理解し判断していくことは難しいですが、問題提起や議論のファシリテートをしながら、各意思決定をオープンに残し、エンジニアチーム全体でベストな意思決定に導くフォローはできるはずです。考えてみれば当たり前なのですが、自社のシステムを十分に理解していないことによって起こる機会損失は、最新技術のトレンドや他社の事例知らない以上に大きいと捉えています。なので2021年は全体最適のリードができるように、自分の技術領域を増やす方向でインプットし、まずはシステムの理解を深めていきたいと思います。 また、事業を複数展開し組織化が進む中で、同じような課題や意思決定が増えてくるはずです。組織内での車輪の再発明を防ぎ、組織化していくからこその強みを生かしていくために、横展開することを想定した設計や基盤の整備、及びナレッジの水平展開が可能な情報共有・展開の仕組みも考えてきます。 そして、社内のエンジニア全体へこれらのメッセージを定期的に発信できるように、月一くらいでCTO通信的なものを始めてみようと思います。 振り返ってみると、2020年は組織や技術の変化が沢山ありました。まだ書き足りないですが、ここで終わりにします。年始に良いスタートダッシュが切れるように、年末は来年のイメージを膨らませながらしっかりリフレッシュをします! 皆さん1年間お疲れさまでした! 最後に 株式会社スタメンは、2020年12月15日をもちまして東京証券取引所マザーズへ新規上場いたしました。これまで支えてくださった方々への感謝の気持ちと、またここからリスタートし、次の高い山を目指してより一層プロダクト開発に励んでいきたいという思いです。 来年は、既存システムのアーキテクチャを事業成長の方向に合わせて大きく変えていくアーキテクトや、大規模なシステムの運用やソフトウェア開発をリードするエンジニアなど、幅広く仲間を見つけていきたいと思っています。B2Bの TUNAG だけでなく、オンラインファンサロン事業 FANTS も展開しており、今後も多角的に事業を展開していきたいと思っているので、TUNAG や FANTS に興味を持ってくれた方も、そうでない方も一緒に作っていくエンジニアを募集しています!興味を持ってくれた方は、 Wantedly で応募いただければと思います!技術・会社・事業についてもっと詳しく聞いてみたい方は、松谷( @uuushiro )まで気軽にDMいただければ喜んで返事します!というわけでここまで読んでいただきありがとうございました。
目次 はじめに ジェネリック型とは 汎用性の高いコンポーネントを作成 おわりに はじめに こんにちは、スタメンでエンジニアをしている手嶋です。普段は、React+TypeScriptでフロントエンドメインで開発をしています。開発の中でReactコンポーネント(以下コンポーネント)を共通的に使いたいことが多々あるのですが、その手法の一つとしてジェネリック型が有効だったので、今回紹介したいと思います。 ジェネリック型とは ジェネリック型は一言で表すと、「型を抽象化したもの」です。 定義だけでは分かりにくいので、理解を早めるために例を挙げます。 以下のようにtextとindexをそれぞれstring型、number型で受け取って、返す関数があったとします。 const showText = ( text: string ) : string => { return text } const showIndex = ( index: number ) : number => { return index } この似たような処理を共通化する際に、ジェネリック型が役立ちます。 以下のように変更することで、関数を呼び出す際に型を指定すれば処理をまとめることができます。 「型のみ異なる」コードを抽象化することで、汎用性を高くすることが出来ました。 const genericFunction < T > = ( arg: T ) : T => { return arg } genericFunction < string >( "hoge" ) // showText()と同じ genericFunction < number >( 10 ) // showIndex()と同じ この考えを応用し、Reactで汎用性の高いコンポーネントを作成します。 汎用性の高いコンポーネントを作成 例として以下コードを挙げます。 DropDownListコンポーネントは、アプリケーション内で汎用的に使うものだと想定してください。 役割としては、親コンポーネントから受け取ったusersをmapで回して子コンポーネント(DropDownItemコンポーネント)に渡すことです。 親コンポーネントではDropDownで選択した単一のuser名をStateとして管理しています。 (スタイルやDropDownItemコンポーネントの中身は割愛しています) ジェネリック型使用前 //types export type UserType = { key: number ; name: string ; } ; // 親コンポーネント import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import UserType from 'types/user' ; interface Props { users: UserType [] ; } const User = ( props: Props ) => { const { users } = props ; // DropDownで選択した単一のユーザーの名前をStateで管理 const [ userName , setUserName ] = useState (); const handleSetUserName = useCallback (( name: string ) => { setUserName ( name ) } , [] ); return ( < div > < span > ユーザー一覧 < span > < DropDownList users = { users } setUserName = { handleSetUserName } / > < div > ); } ; export default User ; // DropDownListコンポーネント import React from 'react' ; import DropDownItem from 'components/commmon/DropDownItem' ; import UserType from 'types/user' interface Props { users: UserType [] ; setUserName: ( name: string ) => void ; //親コンポーネントで管理するStateをセットするAction } const DropDownList = ( props: Props ) => { const { users , setUserName } = props ; return ( < div > { users.map ( user => { return ( < DropdownItem key = { user.key } value = { user.name } setValue = { setUserName } / > ); } ) } < div > ); } ; export default DropDownList ; この状態でも問題なくコードは動作し、ユーザーの名前をDropDownListとして表示できます。 しかし、ユーザーの名前以外(例えばNumber型のリスト)を表示したい場合はどうでしょうか。 DropDownListコンポーネントのinterfaceが 具体的 すぎる(users,setUserNameしか許可していない)ので、再利用することができません。 アプリケーション内で同じ見た目を実現する際に、コンポーネントを使い回せないのは勿体ないです。 解決策として、以下のようにジェネリック型を使用します。 ジェネリック型使用後 // 親コンポーネント① import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import UserType from 'types/user' ; interface Props { users: UserType [] ; } const User = ( props: Props ) => { const { users } = props ; // DropDownで選択した単一のユーザーの名前をStateで管理 const [ userName , setUserName ] = useState (); const handleSetUserName = useCallback (( name: string ) => { setUserName ( name ) } , [] ); return ( < div > < span > ユーザー一覧 < span > // DropDownListに対してstring型を指定して呼び出す < DropDownList < string > users = { users } setListItem = { handleSetUserName } / > < div > ); } ; export default User ; // 親コンポーネント② import React , { useState , useCallback } from 'react' ; import DropDownList from 'components/common/DropDownList' ; import PriceType from 'types/price' ; interface Props { price: PriceType [] ; } const Price = ( props: Props ) => { const { price } = props ; // DropDownで選択した単一の価格をStateで管理 const [ price , setPrice ] = useState (); const handleSetPrice = useCallback (( price: number ) => { setPrice ( price ) } , [] ); return ( < div > < span > 価格一覧 < span > // DropDownListに対してnumber型を指定して呼び出す < DropDownList < number > listItems = { price } setListItem = { handleSetPrice } / > < div > ); } ; export default Price ; //types export type ListItemType < T > = { key: number ; value: T ; //keyを汎用的なvalueという名前に、valueの型はジェネリックに } ; // DropDownListコンポーネント import React from 'react' ; import DropDownItem from 'components/commmon/DropDownItem' ; import ListItemType from 'types/common' //汎用的な型に変更 //親コンポーネントから渡した型がTに interface Props < T > { listItems: ListItemType < T > [] ; setListItem: ( value: T ) => void ; //親コンポーネントで管理するStateをセットするAction } //親コンポーネントから渡した型がTに const DropDownList = < T ,>( props: Props < T >) => { const { listItems , setListItem } = props ; return ( < div > { listItems.map ( listItem => { return ( < DropdownItem < T > key = { listItem.key } value = { listItem.value } setValue = { setListItem } / > ); } ) } < div > ); } ; export default DropDownList ; 上記のようにDropDownListコンポーネントの interfaceを抽象的 にしてあげることで、複数の(型が違う)親コンポーネントから呼び出す事が可能になりました。 DropDownListコンポーネント内で扱うpropsも汎用性の高い名前にすることで、アプリケーション全体で共通利用しやすくなると思います。 おわりに 今回はジェネリック型を用いて、汎用性の高いReactコンポーネントを作成する方法を紹介しました。 是非、コンポーネントを共通化する際の選択肢の一つとして考慮してみてください。 スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
目次 はじめに jsonapi-rspecのinstall 既存のRequestSpecによく見られるテストケースの例 jsonapi-rspecで置き換えてみる さいごに はじめに こんにちは、株式会社スタメンでエンジニアをしている ワカゾノ です。 4月からサーバーサイドエンジニアとして、弊社プロダクト TUNAG の開発を行っております。 TUNAGでは、ユーザビリティの向上を目的に、既存機能のReact化、Native化が進められています。 その際に、 jsonapi_serializer というGemを使用してAPI実装を行っています。responseはJSON:API形式で取得することが出来ます。 また、弊社では APIドキュメント化 を進めており、RequestSpecによる結合テストを必ず追加し、APIインターフェースに対してドキュメントを作成、追加するようにしています。 TUNAGの既存のRequestSpecには下記のような問題点があります。 responseのstatusやID値のみのテストケースが多く、APIドキュメントに記載されている各属性値に関するテストケースが少ない responseのネストが深くなる場合に、 response['data'][0]['a']['b'] のように対象のテストデータを取得する必要があるため、テストを作成した人以外が見た場合に、直感的に分かりにくいテストケースになってしまう 今後、APIを実装する機会が増加する、APIドキュメントの各属性値に関して、正しさを担保する為に、より細かく、そして直感的にRequestSpecを書きたいというニーズがありました。 そのため、これらの問題点を解消するために、今回は jsonapi-rspec というGemを試してみました。 その使用感や感想についてまとめてみようと思います。 jsonapi-rspecのinstall 公式のREADME 通りですが、下記の手順でinstall、設定をします。 Gemfileに追記します gem ' jsonapi-rspec ' プロジェクトにinstallします bundle install spec/spec_helpers.rbに設定を追記します テストケースの中でkeyをstring型、symbol型のどちらも使用したい場合は config.jsonapi_indifferent_hash = true と設定します。 # spec/spec_helpers.rb require ' jsonapi/rspec ' RSpec .configure do | config | config.include JSONAPI :: RSpec # Support for documents with mixed string/symbol keys. Disabled by default. config.jsonapi_indifferent_hash = true end 既存のRequestSpecによく見られるテストケースの例 下記のようなTODOアプリケーションを例として、テストケースを作成しました /api/v1/tasksにアクセスした際に、TODOリスト一覧を取得することが出来る TODOリストの中には、「完了」と「未完了」のタスクがあり、/api/v1/tasksのエンドポイントにパラメータを付与することで、絞り込みを行うことが可能 require ' rails_helper ' RSpec .describe ' Api::V1::Tasks ' , type : :request do describe ' GET /api/v1/tasks ' do # 完了しているTODOタスクデータを作成 let!( :complete_todo_task ) # 詳細は割愛 # 未完了のTODOタスクデータを作成 let!( :incomplete_todo_task ) # 詳細は割愛 # responseから、取得したデータIDを配列へ格納 let( :json ) { JSON .parse(response.body, symbolize_names : true ) let( :response_todo_tasks ) { json[ :data ].map { | task | task[ :id ].to_i } } context ' 正常系 ' do context ' パラメーターが存在しない場合 ' do it ' TODOリストを取得する ' do get ' api/v1/tasks ' expect(response_todo_tasks).to eq [complete_todo_task.id, incomplete_todo_task.id] end end context ' パラメーターが存在する場合 ' do context ' 完了のパラメーターを付与した場合 ' do it ' 完了しているTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' complete ' } expect(response_todo_tasks).to eq [complete_todo_task.id] end end context ' 未完了のパラメーターを付与した場合 ' do it ' 未完了のTODOデータのみ取得する ' do get ' api/v1/○○○ ' , params : { status : ' incomplete ' } expect(response_todo_tasks).to eq [incomplete_todo_task.id] end end end end end end TUNAGの既存のRequestSpecではこのように、レスポンスから取得したデータのIDによりIN、OUT値をテストするテストケースが多く存在します。 今回のAPIのresponse例は下記のような構造となりますが、ID値以外の各属性値をテストしたい場合に、対象のデータを取得するまでが大変であり、またAPIのインターフェースが変更された際の、テストの修正点が多くなってしまいます。 { :data => [{ :id => " ○○○ " , :type => " tasks " , :attributes => { :title => " 宿題を終わらせる! " :status => " 完了 " , } }, { :id => " ○○○ " , :type => " tasks " , :attributes => { :title => " 買い物に行く! " :status => " 未完了 " , } } ], :meta => { :total_todo_num => 2 } } jsonapi-rspecで置き換えてみる 先程のテストケースを jsonapi-rspec を使用して置き換えたものが下記になります。 require ' rails_helper ' RSpec .describe ' Api::V1::Tasks ' , type : :request do describe ' GET /api/v1/tasks ' do # データを作成する箇所は割愛 # 属性毎のテストをするため変更 let( :json ) { JSON .parse(response.body, symbolize_names : true ) let( :response_todo_tasks ) { json[ :data ] } context ' 正常系 ' do context ' パラメーターが存在しない場合 ' do it ' TODOリストを取得する ' do get ' api/v1/tasks ' expect(response_todo_tasks[ 0 ]).to have_id(complete_todo_task.id) expect(response_todo_tasks[ 1 ]).to have_id(incomplete_todo_task.id) end # 新規にmetaのテストケースを追加 it ' metaデータを取得することが出来る ' do get ' api/v1/tasks ' expect(json).to have_meta( total_todo_num : 2 ) end end context ' パラメーターが存在する場合 ' do context ' 完了のパラメーターを付与した場 ' do it ' 完了しているTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' complete ' } expect(response_todo_tasks[ 0 ]).to have_type( ' tasks ' ) expect(response_todo_tasks[ 0 ]).to have_id(complete_todo_task.id) expect(response_todo_tasks[ 0 ]).to have_attribute( :title ).with_value( ' 宿題を終わらせる! ' ) expect(response_data[ 0 ]).to have_attribute( :status ).with_value( ' 完了 ' ) end end context ' 未完了のパラメーターを付与した場合 ' do it ' 未完了のTODOデータのみ取得する ' do get ' api/v1/tasks ' , params : { status : ' incomplete ' } expect(response_todo_tasks[ 0 ]).to have_type( ' tasks ' ) expect(response_todo_tasks[ 0 ]).to have_id(incomplete_todo_task.id) expect(response_todo_tasks[ 0 ]).to have_attribute( :title ).with_value( ' 買い物に行く! ' ) expect(response_todo_tasks[ 0 ]).to have_attribute( :status ).with_value( ' 未完了 ' ) end end end end end end マッチャとして下記を使用しています。 have_type have_id JSON:APIでリソースの判別に使用される type と id をテストするmatcher have_attribute(key).with_value(value) attributes配下にkeyが含まれるかとその値をテストするmatcher have_meta metaデータに関してkeyと値をテストするmatcher その他にもリソース間の関連をテストする必要がある場合に have_relationship().with_data() などのmatcherも用意されています。 APIのインターフェースに沿った形でテストケースを書くことが出来るため、より直感的にテストを書くことが出来るようになりました。 また、テスト作成者以外が見てもテストの意図が明確であり、インターフェースの変更に伴うテストの修正が容易になると思います。 さいごに APIドキュメントは既存機能をReact化、Native化するにあたり、多くのチームが参照するため、APIのインターフェースの正確性を担保することが重要であり、なるべく詳細にテストを書く必要があると考えています。 JSON:APIのRequestSpecをより書きやすく、分かりやすくするために有用なGemだと思いますので、是非試してみてください! スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。 Webアプリケーションエンジニア募集ページ
目次 はじめに アップロードの流れ Google Cloud Storage の準備 実装 おわりに はじめに こんにちは、スタメンのミツモトです。 スタメンでは TUNAG 、 FANTS というサービスを提供しており、画像の保存先として Amazon Web Services のS3(Simple Storage Service)を採用しています。 Google Cloud Storage(以下、GCS)の場合、どういう流れで画像を保存するか知りたいと思い、自分の学習として GCS を用いた署名付きURLによる画像のアップロードを実装したので紹介させていただきます。 アップロードの流れ 今回はクライアントから直接GCSへ画像をアップロードします。 通信の流れは以下になります。 Google Cloud Storage の準備 サービスアカウント 最初に、GCSのバケットにアクセスするためのサービスアカウントを作成します。 Google Cloud Platformにアクセスし、サイドバーから「IAMと管理 > サービスアカウント」を選択してください。 「サービスアカウントの作成」をクリックします。 サービスアカウント名を入力し、サービスアカウントの権限として「Cloud Storage > Storageオブジェクト管理者」を選択します。 アプリケーションで作成したサービスアカウントを利用するためキーを追加します。サービスアカウントの一覧で対象アカウントの「編集」をクリックしてください。 サービスアカウントの詳細から「鍵を追加」をクリックし、JSON形式で鍵を作成してください。ここで作成した鍵をサーバーサイドの実装時に利用します。 バケット 続いてバケットの作成をします。 サイドバーから「Storage > ブラウザ」を選択してください。 バケット名の入力、データの保存場所やストレージクラス等を選択し、バケットを作成します。 以上で Google Cloud Storage の準備は完了です。 実装 サーバーサイド 署名付きURLを発行するためのモジュールを作成します。 先程作成したサービスアカウントを用いてクレデンシャルを生成し、それを用いてバケットに対する署名付きURLを発行します。 content_typeを指定しないとクライアントからのアップロードが上手くいかないため、署名付きURLを発行する時点でcontent_typeを指定しておきます。 require ' google/cloud/storage ' module Utils :: Gcp :: Storage GCP_SA_CREDENTIALS = { private_key : ' サービスアカウントのprivate_key ' , client_email : ' サービスアカウントのclient_email ' } GCS_PROJECT_ID = ' GCPのプロジェクトID ' GCS_BUCKET_NAME = ' GCPのバケット名 ' class << self def pre_signed_url (path, content_type, expires) @storage = Google :: Cloud :: Storage .new( project_id : GCP_PROJECT_ID , credentials : GC_SA_CREDENTIALS ) expires = expires.to_i @storage .signed_url( GCS_BUCKET_NAME , path, method : ' PUT ' , content_type : content_type, expires : expires) end end end Resourceであるインスタンスからモジュールの pre_signed_url メソッドを呼び出し、レスポンスとして返却します。 class Resource < ApplicationRecord def pre_signed_url (filename【 GCS上のファイル名 】, content_type) Utils :: Gcp :: Storage .pre_signed_url( " resouces/ #{ id } /images/ " + filename, content_type, 5 .minutes.from_now) end end クライアントサイド 最初のシーケンス図通り、以下の順番でリクエストを送ります。(エラーハンドリングは省略しています。) 署名付きURLの取得 GCSへの画像アップロード アップロードされた画像の情報をDBへ保存 実装としては以下のようになります。 const uploadImage = async ( file , resourceId ) => { // 署名付きURLの取得 const res = await fetch ( "署名付きURL取得エンドポイント" + `?content_type= ${ file . type } ` ) const resJson = await res . json () ; // 署名付きURLを用いて Google Cloud Storage へアップロード await fetch ( resJson . preSignedUrl , { method : "PUT" , headers : { "Content-Type" : file . type } , body : file }) // アップロードされた画像の情報をDBへ保存 fetch ( "画像の情報をDBへ保存するエンドポイント" , { method : "POST" , headers : { "Content-Type" : "application/x-www-form-urlencoded; charset=utf-8" } , body : `uniq_filename= ${ resJson . filename } &filename= ${ file . name } &content_type= ${ file . type } &byte_size= ${ file . size } ` } ) } おわりに GCS を用いた署名付きURLによる画像のアップロードについて紹介させていただきました。想定していたより簡単に画像のアップロードを実現できたので良かったです。今回はアップロードの流れを書きましたが、機会があれば画像配信についても記事にできたらと思います。 スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ 採用サイト からご連絡ください!
はじめに 本記事では RSpec の request spec から OpenAPI 仕様のドキュメントを出力する Gem、rspec-openapi を紹介します。 ドキュメンテーションツール導入にあたっての負担を少なくしたい、実装とドキュメントが乖離しないようにしたい、という場合に参考になるかもしれません。 背景 これまで弊社では、 API ドキュメントは社内 wiki に蓄積されていました。 最初はこれでも問題にならなかったのですが、機能が増えてきたことによってドキュメントの数も増えて管理が難しくなったり、 フォーマットが明確でないので書く人によってばらつきがあるという問題がちらほら出てくるようになります。 API ドキュメント標準化といえば OpenAPI が思いつくものの、記述のために DSL を理解する必要があるとか追加で何か学習が必要となると、全員に浸透させるのはハードルが高そうです。 もちろん OpenAPI 仕様に則ることの価値をチームとして合意できていたら話は別だと思います。今回はこれまでまったく使っていない状況から導入する、という前提なので少しでも簡単に導入できるほうが望ましいです。 rspec-openapi はアウトプットが OpenAPI フォーマットかつ既存の request spec がそのまま利用できるということで、上記の条件に適しているのではないかと思い試してみました。 使い方 Gemfile に以下のように追記して Gem をインストールします。 gem ' rpsec-openapi ' , group : :test OPENAPI=1 と環境変数をセットして request spec を実行すると、doc 配下に openapi.yaml というファイルが生成されます。 $ OPENAPI = 1 rspec path/to/request_spec_file Rails でなくても使えますが、内部で Rails かどうかの判定で処理を分岐している箇所がいくつかあるので、細かい部分など違いがあるかもしれません。 例を見てましょう。 以下のようなコントローラーとテストケースを用意しました。 class Api :: PostsController < ApplicationController def index posts = [ { title : ' foo ' } ] render json : { posts : posts } end end require ' rails_helper ' RSpec .describe ' Api::Posts ' , type : :request do describe ' GET /api/posts ' do it ' get posts ' do get api_posts_path, params : { title : ' f ' } expect(response).to have_http_status( :ok ) end end end テストを実行すると OPENAPI = 1 rspec spec/requests/api/posts_spec.rb doc/openapi.yaml に以下のような内容が生成されます。 --- openapi : 3.0.3 info : title : rspec-openapi-sample paths : "/api/posts" : get : summary : index tags : - Api::Post parameters : - name : title in : query schema : type : string example : f responses : '200' : description : get posts content : application/json : schema : type : object properties : posts : type : array items : type : object properties : title : type : string example : posts : - title : foo このファイルを Swagger Editor などに読み込ませると、問題なく生成できているのがわかるかと思います。(version だけ手動で追記しました) 実行結果から生成するという仕様上、実行結果に含まれない情報は取得できないので、その点については手直しが必要です。 例えば必須パラメータやレスポンスのフィールドが nullable なのかどうかなどの厳密な型情報は、テストケースだけだと判別がつきません。 出力ファイルには追記はされても上書きはされないので、手で直した箇所を保持しながらドキュメントを拡充していくことができます。 まとめ RSpec からドキュメントを生成する rspec-openapi を紹介しました。 既存資産(request spec)をそのまま活かすことで実装者にほぼ負担のない形で導入できるという点は、類似のライブラリと比べても優れているところだなと感じます。 良ければ試してみてください。 参考 rspec-openapi OpenAPI Specification
こんにちは。フロントエンドエンジニアの 渡邉 です。 普段はReactとTypeScriptを書いています。 今回の内容はタイトルにも記載されていますが、Next.jsを使った画像の最適化です。 画像の最適化でやるべきことは多々あります。 例えば、画像のサイズ・重さ調整や、フォーマット、遅延読み込みなどあります。 Next.js 10で発表された next/image を使えば誰でも簡単に画像の最適化が行えるようになりました。 この記事では以下のような方が対象者となります。 - 画像の最適化?したことないや - 自動でやってくれるなら試してみたい ただし、Next.jsの使い方などは今回は記載していないので、ご了承ください。 目次 画像最適化するとなにがいいの? 実際にnext/image使ってみる まとめ 画像最適化するとなにがいいの? 以下のようなメリットがあります。 - 画像を最適化することによりページの読み込み速度を改善できる - SEOの改善 画像を最適化することによりページの読み込み速度を改善できる 画像を多用したページでは最適化してあるか、していないかで大きく読み込み速度に差が出てきます。 例えば、ECサイトであったり、ランディングページに置いて読み込み速度が遅いとユーザーが離脱して売上にもかなり影響出てきます。 以下の画像はGoogleがページ表示速度がユーザーにどのくらい影響するかを調査した内容です。 引用元: Find Out How You Stack Up to New Industry Benchmarks for Mobile Page Speed 1秒から3秒で32% 1秒から5秒で90% 1秒から6秒で106% 1秒から10秒で123% 直帰率 が増加してしまいます。 SEOの改善 SEOの改善?と思われた方もいると思います。 どういうことかというと、Googleが、Webページの表示速度が遅いと検索のランキングに影響が出るよと言っています。 詳細は こちら ということもあり画像の最適化がされていない表示速度が遅くなり検索ランキングが低下し、そもそもユーザーに見に来てもらえない可能性もでてきます。 上記の他にも最適化することによってのメリットは多々あります。 ただ、もちろん最適化するには工数もかかるうえに大変です。 そこで、Next.jsを使えば画像の最適化を自動で行ってくれるということです。 実際にnext/image使ってみる nextのサンプルアプリケーションを作成し起動させます。 npx create-next-app image-pra cd image-pra yarn dev アプリケーションが起動したら public 配下に今回お試しで使用する画像を配置するのと、 page/index.js を以下のように書き換えます。 import Image from 'next/image' function Home() { return ( <> <h1>Image optimisation</h1> <Image src= "/sample.jpg" // publicに配置した画像 alt= "sample picture" width= { 500 } height= { 300 } /> </> ) } export default Home これだけで最適化された画像が表示されます。 基本的に使い方は既存の <img> と同じです。 ただ、 height と width を設定しないとエラーが出ます。 ここでは、アスペクト比にあった数値をいれてもらえれば、あとはImageコンポーネントがレスポンシブに画像サイズを調整してくれます。 実際にImageコンポーネントは以下のように変換されています。 変換された <img> でsrc属性の /_next/image というエンドポイントが気になったので説明していきます。 今回srcで指定されている /_next/image はNext.js 10〜から予め用意されているエンドポイントです。 _next/image のリクエストを受けると next-server はimageOptimizerを実行しています。 https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/next-server.ts 抜粋 { match: route( '/_next/image' ), type: 'route' , name: '_next/image catchall' , fn: (req, res, _params, parsedUrl) => imageOptimizer(server, req, res, parsedUrl), } , image-optimizer.ts その呼び出されている image-optimizer.ts で様々な最適化処理が行わています。 例えば、ブラウザがWebPに対応している場合にはWebPに変換するなどの対応がされています。 このように、コードの中を読むと実際にどのように最適化しているかがわかって面白いので是非お時間ある方は読んでみてください。 話がすこしそれてしまいましたが、 Vercel にデプロイすれば特に設定する必要なく使えます。とても楽ですよね。 外部サービスで最適化する場合 imgixやCloudinaryの外部のサービスで画像の最適化を行う場合は next.config.js で設定します。 例えばimgix使う場合は以下のようになります。 module.exports = { images: { loader: 'imgix' , path: 'https://example.com/myaccount/' , } , } 詳しく知りたい方は image-optimization#loader を御覧ください。 <img> とImageコンポーネントの比較 上の sample.jpg が <img> で表示している画像で、下の image?url=~~ のほうが Image コンポーネントで表示した画像です。 画像の読み込み時の詳細を見てみると、読み込み速度とサイズが大幅に改善されています。 デフォルトでここまで改善できるが素晴らしいです。 例えば、ある画像だけ品質を下げてもいいなというときには、 quality に数値を渡してあげるだけで調整ができます。 <Image src= "/sample.jpg" alt= "sample picture" width= { 500 } height= { 300 } quality= { 50 } // ここで調整する(デフォルトは75) /> こうすることで、よりSizeを抑え込めます。 その他propsについては next/image#usage を御覧ください。 画像最適化の全体設定 画像最適化の全体での設定は next.config.js で行えます。 module.exports = { images: { // ここに記載 } , } こちらも詳細は image-optimization#configuration を御覧ください まとめ Next.jsのImageコンポーネントを使うだけで画像を簡単に最適化することができます。 今回紹介しきれなかったんですが、初期表示で表示される画像が画面の外にある場合は、viewportを計算して遅延読み込みまでしてくれます。最近のNext.jsは成長がとてつもなく速く、質が高いので今後も楽しみです。 株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひ エンジニア採用サイト をご覧ください。