行動すれば次の現実

ほどよくモダンなシステム開発を目指しています。メインテーマは生産性、Ruby、Javascriptです。

Rails7 APIモードの認証機能をdevise_token_authで実装する

Rails7 APIモードの認証機能を実装するにあたり、Deviseのトークン認証を可能にするdevise_token_authというライブラリを使用することにしました。

導入に少しハマったところなどもありましたので記事にまとめました。同じような構成を検討している方の参考になれば幸いです。

環境構成

フロントエンド:Next.js 12.1
バックエンド:Rails 7.0
開発環境:Docker、docker-compose

devise_token_authの準備

Gemのインストール

Gemfileに以下を記載してbundle installします。

**Gemfile**

gem 'devise'
gem 'devise_token_auth'
gem 'devise-i18n'
gem 'rack-cors'

rack-corsはCORS(Cross-Origin Resource Sharing)を実現させるために必要となりますので、一緒にインストールします。 devise-i18nはメッセージの日本語化のために必要となるパッケージです。

インストールタスクの実行

deviseとdevise_token_authのインストールタスクを実行します。 今回はUserモデルに対してDeviseを適用したいと思います。

rails g devise:install
rails g devise_token_auth:install User auth

作成されるconfig/initializers/devise_token_auth.rbというファイルに各種設定を記述します。

私の場合config.change_headers_on_each_request = falseのみ変更しました。 これ設定をすることでリクエストの都度トークンが変更されなくなるので、トークンの管理が楽になります。

routesとmigrationファイルの修正

今回は/api/v1/authというパス上に認証用のエンドポイントを作成したいと思うので以下のようにroutes.rbを修正します。

**config/routes.rb**

namespace :api do
  namespace :v1 do
    mount_devise_token_auth_for 'User', at: 'auth'
  end
end

また、devise_token_auth:installではログイン日時などの情報をトラッキングするためのマイグレーション情報が作成されないようなので、以下のように手動で修正を加えます。

**app/models/user.rb**

# :trackableを追加します
:recoverable, :rememberable, :validatable, :trackable
**db/migrate/xxxxx_devise_token_auth_create_users.rb**

# Trackable用のカラムを追加します
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string  :current_sign_in_ip
t.string  :last_sign_in_ip

メッセージの日本語化

devise-i18nを使用してデフォルト英語のメッセージを日本語に変換させます。

rails g devise:i18n:locale ja

を実行すると/config/locales/devise.views.ja.ymlというファイルが作成されます。

このままでは日本語化されませんので、application_controller.rbに以下の定義を記載します。

**app/controllers/application_controller.rb**

before_action do
  I18n.locale = :ja
end

CORSの設定

originsにフロントのホスト(localhost:8000)を記載して以下のようにCORSの設定ファイルを作成します。

exposeには%w[access-token uid client]を記述しています。これによりクロスドメインでもヘッダー情報が公開されるようになります。

**config/initializers/cors.rb**

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8000"

    resource "*",
             headers: :any,
             expose: %w[access-token uid client],
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

各メソッドの動作検証

ここまで準備が完了しましたらrails db:migrateを実行してdeviseの動作検証をしてきます。

devise_token_authはHTTPリクエストのヘッダーにuid、access-token、clientを付与することで認証が行われます。 ちなみにcurlに-iオプションをつけるとHTTPレスポンスのヘッダー情報が出力されますので便利です。

ユーザ登録

  • / にPOSTでアクセスします
  • email、password、password_confirmationをparamsに設定して送信することでユーザが作成されます
# コマンド実行例
curl localhost:3000/api/v1/auth -X POST -d '{"email":"test@example.com", "password":"password", "password_confirmation": "password"}' -H "content-type:application/json" -i

ユーザ削除

  • /にDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与することで、該当のユーザが削除されます
# コマンド実行例
curl http://localhost:3000/api/v1/auth -H "uid: test@example.com" -H "client: hpHyxPAgFmVxFA75uJOhYA" -H "access-token: glXm3iysPovJSi4q_ffpwQ" -X DELETE
ActionDispatch::Request::Session::DisabledSessionErrorについて

ちなみにRails7の場合、そのままの状態だと以下のエラーが発生します

ActionDispatch::Request::Session::DisabledSessionError (Your application has sessions disabled. To write to the session you must first configure a session store):

Rails7より、APIモードのようにセッションを使用しない場合にセッションへのアクセスがあるとエラーを発生させる機構が備わったことによるエラーです。

deviseでは内部でセッションにアクセスしている箇所があるので当該エラーが発生する模様です。

私の場合、ワークアラウンドとして以下の処理を入れることで一旦回避させました。 根本対応されましたら、きちんと修正しておくことにします。

**app/controllers/application_controller.rb**

 class ApplicationController < ActionController::API
    include DeviseTokenAuth::Concerns::SetUserByToken
    include DeviseHackFakeSession # この部分を追加します
 end
**app/controllers/concerns/devise_hack_fake_session.rb**

module DeviseHackFakeSession
  extend ActiveSupport::Concern

  class FakeSession < Hash
    def enabled?
      false
    end

    def destroy
    end
  end

  included do
    before_action :set_fake_session

    private

    def set_fake_session
      if Rails.configuration.respond_to?(:api_only) && Rails.configuration.api_only
        request.env['rack.session'] ||= ::DeviseHackFakeSession::FakeSession.new
      end
    end
  end
end

参考: https://github.com/heartcombo/devise/issues/5443

ユーザ変更

  • /にPUTでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザを変更します
  • デフォルトはemailとpasswordのみ変更可能なので、必要に応じてconfigure_permitted_parametersにパラメータを追記します
# コマンド実行例
curl http://localhost:3000/api/v1/auth -d '{"password":"password", "name":"taro"}'  -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X PUT
**app/controllers/application_controller.rb**

  before_action :configure_permitted_parameters, if: :devise_controller?

  # nameを変更可能にする例
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end

ログイン

  • /sign_inにPOSTでアクセスします
  • 認証が成功するとHTTPレスポンスヘッダーにuid、access-token、clientが付与されてきます。認証が必要なアクションに対してリクエストヘッダーそれらを付与することでアクセス可能になります
curl localhost:3000/api/v1/auth/sign_in -X POST -d '{"email":"test@example.com", "password":"password"}' -H "content-type:application/json" -i

# レスポンス一部抜粋
access-token: OtzRYGvcy-W3zvXzQijB4Q
token-type: Bearer
client: r7zZ0MBaFSnNcGsiRZQetQ
expiry: 1657875966
uid: test@example.com

ログアウト

  • /sing_outにDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザがログアウトされます
curl http://localhost:3000/api/v1/auth/sign_out -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X DELETE

パスワードリセット

  • /auth/passwordにPOSTでアクセスします
  • emailとredirect_urlをパラメータに付与することで、該当のアドレスに対してパスワードリセットメールが送信されます。メール内に記載されたパスワードリセットフォームのURLにredirect_urlが設定されます
curl http://localhost:3000/api/v1/auth/password -H "content-type:application/json" -d '{"email":"hello@example.com", "redirect_url":"http://localhost:3000/pages"}' -X POST

パスワードリセットメール

疎通確認

では実際に、認証が必要なエンドポイントを設けて実際に疎通確認をしてみます。

/api/v1/homeというエンドポイントを設けます。

ログインしている場合のみアクセスできるようにbefore_action :authenticate_api_v1_user!を定義しています。authenticate_api_v1_userのメソッド名はdeviseがマウントされているpathによって動的に命名されますので、api/v1という構成でない場合は読み替えてください。

Rails.application.routes.draw do
  resources :pages, only: [:index]
  namespace :api do
    namespace :v1 do
      resources :home, only: [:index]

      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end
end
class Api::V1::HomeController < ApplicationController
  before_action :authenticate_api_v1_user!

  def index
    render json: { message: 'hello' }
  end
end

tokenが正しい場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: Zv5thbTxDEWEyn7Nhl_SLg" -X GET

{"message":"hello"}

tokenが正しくない場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: hote" -X GET

{"errors":["ログインもしくはアカウント登録してください。"]}

終わりに

今回の記事ではdevise_token_authのインストール方法と使い方をメインに説明しました。 次回以降では、Next.jsとの繋ぎ込み部分に関してのセッション管理をどうするか、認証によるページ制御をどうするかについて記事を書いてこうと思います。

Rails+Postgresql+Dockerで発生する「There is an issue connecting to your database with your username/password, username xxx」でハマった件

Rails、Postgresqlの構成でdocker composeによる開発環境を構築していたところ、RailsからDBに接続する際に、以下のエラーが発生しました。

There is an issue connecting to your database with your username/password, username: app.

Please check your database configuration to ensure the username/password are valid.
Couldn't create 'app_development' database. Please check your configuration.
rails aborted!
ActiveRecord::DatabaseConnectionError: There is an issue connecting to your database with your username/password, username: app.

Please check your database configuration to ensure the username/password are valid.

このエラーが発生する場合、database.ymlにusernameやpasswordを設定し忘れているケースがほとんどです。

実際に私の環境では以下のような設定になっておりました。

# database.yml

default: &default
  adapter: postgresql
  encoding: utf8
  host: db
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: app
  password: password

development:
  <<: *default
  database: app_development
# docker-compose.yml(dbの部分のみ抜粋)
  db:
    image: postgres:13.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_development
      TZ: "Asia/Tokyo"
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"

volumeを初期化したら解決

設定自体は正しそうでしたので、DBが記録されているvolumeを削除してみることにしました。

$ docker volume ls
・・省略
local     app_postgres
・・省略

$ docker volume rm app_postgres
$ docker-compose up -d -build

volumeを削除後にもう一度docker-composeで起動してみたところ、エラーが発生せずに正常にアプリとDBが接続することができました。

なぜvolumeを初期化することで解決できたかというと、database.ymlのusernameをdocker-composeの初期起動時から変更してしまったためです。

docker-compose.yml(dbの部分のみ抜粋)

  db:
    image: postgres:13.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: sample # ← sampleという名前で構築して後からappに変更した

そのため、実質的には、docker-composeでの初期起動時に記載されたusernameとdatabase.ymlに記載されたusernameと不一致の状態で接続を試みていたことになります。

小一時間ほどハマってしまったので、同じ境遇の方のお役に立てば幸いです。

それでも自社サービスを開発する理由

個人法人問わず、独立したエンジニアであれば、一度は自社サービスの立ち上げを検討したのではないでしょうか。

弊社も今まで何度か自社サービスの立ち上げを行ってきました。 自社サービスはマネタイズするまでに時間が掛かることがほとんどです。

一朝一夕で出来るようなことでは有りませんので、かなり根気のいる仕事であり、長い目で見ていく必要があります。

それでも私は「独立したのならば自社サービスを開発すべき」だと考えます。なぜそう思うのかについて持論を展開していこうと思います。

目に見えるポートフォリオとしての役割

ビジネスマッチングサイト等を利用して開発案件を獲得する際は、必ずと言っていいほど実績(ポートフォリオ)を提示するよう求められます。

しかし、SESや業務委託契約の案件ばかりやっていると、実績を目に見える形で証明できないことがほとんどです。

例えば「私がこのサービスを作りました!」と言ったとしても、サービスの運営会社は別に存在するので「本当にあなたが作ったのか?」「何人もいる開発者の内の一人なんじゃないか?」と勘ぐられ、説得力や信憑性に欠けてしまうこともあります。

また、NDAでサービス名を明かしては行けないという契約の場合も有り、「某プロジェクトの某システム」というように曖昧な実績を提示することになります。

しかし、自社サービスがあればそのようなことはなくなります。気兼ね無くポートフォリオとして実績を提示することができます。 また、自社サービスがあれば企業のブランディングとしての効果もあります。広報やメディア取材の対象にもなる可能性もありますので、自社の広告塔としての役割も担うことも期待できます。収益化まで時間が掛かるとしても、ブランディングとしての効果もありますので決して無駄にはならないと私は考えます。

収益の分散化としての備え

業務委託やSESの場合、基本的には満期日を迎えると契約はそこで終了しますので、収益が途絶えてしまいます。継続的な収益を得るためには業務委託契約を続けるほかありません。

受託開発の場合も納品して終わりというケースがほとんどです。保守契約で継続するといこともありますが、必ずとは言えません。

自社サービスの場合は、利用してもらい続ければ右肩上がりで収益を上げることができます。 新規顧客の開発と既存顧客フォロー(カスタマーサクセス)のサイクルを実施することで、解約率が低い状態を維持できれば基本的にはサービス利用者は伸び続けます。

自社サービスが軌道に乗るまでは、業務委託や受託等で食いつなぎながら、余剰金を使って自社サービスを育てていくことで、最終的には収益を分散化することが可能になります。

新しいテクノロジーの実験場所として

業務委託では使用するプログラミング言語やフレームワークは基本的に縛りがあるので、新しい技術を取り入れたいとしても簡単にはいきません。これは自己成長の機会を逃していると捉えることもできます。

受託でもやはりスピード重視になりますので、あえて不慣れな新しい技術を導入しようとはならないことがほとんどです。

自社サービスの場合は、完全に自社の独断で納期をコントロールできますので、あえて新しい技術を取り入れるという選択も可能になります。 新しいをテクノロジーを試験に取り入れられる実験場として絶好の場であると私は考えます。

また、自社サービスを売り込むためにはWebマーケティング知識も必要ですので、マーケティング関連の知識習得もでき、自己成長につながるという副次的な効果も期待できます。

Rails7+esbuild+TypeScriptで開発環境を構築する

Rails7のフロントエンド開発環境としてjsbundlingのesbuildを採用した際にTypeScriptの導入に少し手こずりましたので備忘録として記事にしました。参考になれば幸いです。

esbuildではTypeScriptの型チェックは行ってくれない

esbuildはデフォルトでTypeScriptのコンパイルをサポートしています。

そのため、特段何かインストールしたりや設定する必要はありません。 ただしサポートしているのはコンパイルのみで、型チェックまでは行ってくれません。そのため別途tsc -noEmit等のコマンドを並行して実行する必要があります。

それを踏まえると以下のようなnpm scriptsになるかと思います。

package.json(抜粋)

"scripts": {
    "build:js": "esbuild ./app/javascript/application.ts --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules",
    "type-check": "tsc -w --noEmit"
}

これでも良いのですが、build:jsとtype-checkがそれぞれ別で動いてしまいますので、型チェック(type-check)が通ったらコンパイル(build:js)を実行するようなことができません。

そのため今回はtsc-watchという型チェックの監視プロセスの結果によって挙動を制御するnpmパッケージがありましたので、それを使用します。

 yarn add -D tsc-watch

tsc-watchをインストールしましたら、package.jsonとProcfile.devを以下のように書き換えます。

package.json(抜粋)

"scripts": {
  "build:js": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
  "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/tailwind.css",
  "failure:js": "rm ./app/assets/builds/application.js && rm ./app/assets/builds/application.js.map",
  "dev:js": "tsc-watch --noClear -p tsconfig.json --onSuccess \"yarn build:js\" --onFailure \"yarn failure:js\""
}

また、Procfile.devでのjsコマンドもyarn dev:jsに変更します。

Procfile.dev

web: bin/rails server -b 0.0.0.0 -p 3000
js: yarn dev:js
css: yarn build:css --watch

このようにすることでTypeScriptの型チェックが成功すればコンパイルを実行し、失敗すれば前回のコンパイルファイルを削除させるような分岐をすることができます。

ちなみに、tsc-watchは内部的にtscコマンドを使用していますので、開発環境にTypeScriptがインストールされていない状態だとcannot find module 'typescript/bin/tsc'というエラーが出てしまいます。そのため、別途TypeScriptをインストールする必要がありますのでご注意ください。

 yarn add -D typescript

参考

https://esbuild.github.io/content-types/#typescript

現場主義の企業がどのようにDX化を進めていけばよいか

コロナ禍によって企業のDX化(デジタル・トランスフォーメーション)推進の動きが加速しております。 弊社でも今までシステム導入を見送っていた企業のシステム開発を請け負う機会が多くなりました。

DX化の案件を進めていく上で、どのようにDX化を進めていけばよいのかというのは永遠の課題だと思います。 今回は現場主義の企業がどのようにDX化を進めていけばよいのか弊社のナレッジを含めて考えを展開したいと思います。

エクセル管理の限界がDX化のサイン

DX化されていない企業の多くはエクセルを使用して日々の業務を管理しているケースがとても多いです。

簡易なエクセルマクロで業務の補助ツールとして利用している企業もあれば、コアなデータをシートに貼り付けて、そのデータを自動的に計算するような作り込みがされている職人芸のようなエクセルマクロを作って作業の効率化を図っている企業もあります。

エクセルマクロはシステマチックなことも実装できますので、非エンジニアであってもプログラミング思考を持っていれば複雑で高機能なツールを作ることが可能です。

ただし、このような高機能なエクセルマクロは作りが複雑であるがゆえに、特定の担当者でなければ修正できない状態(属人化)してしまったり、一つの修正により他の箇所が連鎖的に壊れてしまったりと様々な問題が生じてきます。

このような状態になることは考え方によっては、DX化を進める上でのサインであると捉えることもできます。 ツールで扱おうとする業務範囲が拡大し、エクセルのような簡易システムで管理できる範疇を超えてしまった状態です。

エクセル管理での限界を感じ始めたら、それがDX化を考えるスタートラインです。 まずはどのような解決策があるのか、インターネットや他企業から情報収集から始めるのが良いでしょう。

いきなり100%を目指すのではなく、1歩ずつDX化を進める

DX化する上で重要な考え方があります。

それはマネジメント層と現場スタッフとではDXに対する考え方が全く異なるという点です。 マネジメント層はすぐにでもDX化を推し進めようと考えますが、現場スタッフはそれに対して反発する傾向が強いように感じます。

人間というのは変化を恐れて現状維持のバイアスがかかります。現場スタッフもそれは例外では有りません。

日々ルーチンワークとしてある居心地良く回している業務が、新たなシステムの導入により抜本的に変化してしまうと、スタッフにとって大きなストレス要因となります。 また、一時的に作業効率が大きく落ちたり、新しいシステムの操作を覚えるためにスタッフへの負荷が高まり、他作業に影響が派生してしまうことも少なくありません。

DX化が重要だとは分かっていても最初からそれを理解していくれるスタッフは多くはありません。

そのため、いきなり0から100へのDX化を進めるではなく、0から1を目指すように、まずは一部の業務から移行したり、一部のスタッフから導入を促してシステムに慣れてもらうような考慮が大切になります。

PCやインターネットに対して苦手意識が低いスタッフや、比較的年齢層の若いスタッフを中心に広めていくと良いかと思います。

良いシステムであれば、使い慣れ始めると必ず賛同してくれます。賛同者が増えれば、また新しいスタッフへ伝播してくれる役割にもなりますので、ゆっくりでも1歩ずつ確実に浸透させていく心掛けが重要です。

Railsを5.2から6.0にバージョンアップする方法

この記事では、Railsをバージョンアップ(アップグレード)する方法を説明します。 バージョン5.2から6.0にアップグレードする手順を例にとって説明しています。

テストや確認の工程などは省き、なるべくシンプルな内容にしています。

Gemを最新状態にアップグレードする

Railsのアップグレードの前に、使用しているGemを最新状態にアップグレードしておきます。 こうすることによりアップグレード作業がスムーズになります。

bundle update

Railsのバージョンを上げる

Gemfileに記載されているRailsのバージョンを書き換えます。

gem 'rails', '~> 5.2.0'
  ↓
gem 'rails', '~> 6.0.0'

その後、Rails本体および関連Gemを含めアップグレードを実行します。

bundle update

rails app:updateタスクを実行する

bundle updateが正常に完了したら、下記のコマンドを実行します。

rails app:update

対話形式で設定ファイルを上書きするかどうか聞かれますので、ひとまず全てYで上書きします。

上書きが完了したら、ファイルの差分を一つずつチェックします。旧ファイルで必要な設定を適宜反映させて設定ファイルの内容を確定させます。

なお、Rails6.0にアップグレードする場合は、新たにmigrationファイルも生成されますので、rails db:migrateタスクも実行する必要があります。

rails db:migrete

古いバージョンとの差分を確認する

この状態で一旦railsを起動させて、動作確認をします。

動作確認に問題なければapplication.rbの以下の項目を変更します。

config.load_defaults 5.2
  ↓
config.load_defaults 6.0

また、新たに作成されたnew_framework_defaults_6_0.rbというファイルも不要ですので削除します。

最終確認

上記書き換え後に再度動作確認をします。問題なければアップグレード作業はこれで完了になります。

当記事では最低限の作業のみ記載しておりますが、RSpecで問題なくテストがパスするか、DEPRECATIONメッセージが出力されていないか、ステージング環境で問題なく動作するか等の作業が発生します。

また、バージョンによっては独自の作業が必要な場合もありますので、必ずRails アップグレードガイド等の内容もチェックすることをオススメします。

React+Reduxのアクション名のコンフリクトを避ける方法

React+Reduxのパターンで開発しているとアクション名の衝突を避けるために管理が煩雑になってきます。

例えば、ある機能でINCREMENTというアクション名をすでに使用している場合、他の機能で同じような振る舞いのアクションを定義する場合はINCREMENTという命名を避けなければなりません。

これは、アクション名が衝突するとそれぞれに定義されたreducerが発火してしまい意図しない動作を引き起こしてしまうためです。

そのため、XXX/INCREMENTZZZ/INCREMENTのように機能ごとのprefixをつけてアクション名の衝突を避ける工夫が必要になります。

redux-actionsを使ってアクション名の衝突を避ける方法

アクションの定義にはredux-actionsというライブラリを使用しているケースが多いかと思います。

redux-actionsを使用することで上記のようなアクション名の衝突を避けるような命名をすることが出来ます。

const { increment, decrement } = createActions(
  'INCREMENT', 
  'DECREMENT', 
  { prefix: 'XXX' },
);

createActionsのオプションにprefixを設定することで、アクション名がXXX/INCREMENTXXX/DECREMENTという命名に変更されます。

同様にreducer側で使用するhandleActionsメソッドにおいてもprefixを指定する必要があります。

export default handleActions({
  [actionTypes.INCREMENT]: (prevState, action) => {
    return {
      ...prevState,
      count: count + 1,
    };
  },
  [actionTypes.DECREMENT]: (prevState, action) => {
    return {
      ...prevState,
      count: count - 1,
    };
  },
}, defaultState, { prefix: XXX });

github.com

また、注意点としてredux-sagaを使用している場合、takeEvery等でアクション名のパターンマッチングをしていますので、そこの考慮も必要になります。 redux-actionsとは別のライブラリですので、仕様のギャップは吸収しなければなりません。

export default function* () {
  yield takeEvery(`XXX/${actionTypes.INCREMENT}`), increment);
  yield takeEvery(`XXX/${actionTypes.DECREMENT}`), decrement);
}