fish の config ファイルを公開する

以前の記事で 自分のfishのfunctionを公開した ので、備忘録的に fish の config も公開します。

この辺りはエンジニアの三大美徳である「怠惰」が表れるのですが、なかなか一般に公開されているものは多くないように思います。何かの参考になればと思い、また自分が将来 PC を乗り換えた時に思い出せるように書き残しておきます。

エイリアスのいくつかを簡単に説明する

docker 関連では毎回$ docker-composeと打つと大変なので dc としました。

git 関連では、rebase 周りが重宝しています。コミットを3つ rebase したい時は $ grbia HEAD~3で済み、continue した時は$ grbc、abort するときは$ grbaを打つだけです。

新しいブランチを作るときは$ gs -c feature/some-branch-name、push する時は $ gpso feature/some-branch-nameで ok です。$ gfc でリモートブランチをフェッチしてきて、$ gplo feature/some-branch-nameすれば最新になります。

一方、reset や stash、push origin —force-with-lease といったコマンドはあえてエイリアスを作成していません。これから重要な操作を打つのだという意識を自分に持たせるためです。と言いつつ、fish の history による補完を使っているので全部入力することはなかったりします。

.config/fish/config.fish
set -x PATH "$HOME/.nodebrew/current/bin:$PATH"
set -x PATH "$HOME/.yarn/bin:$PATH"
set -x PATH "$HOME/.cargo/bin:$PATH"

alias python 'python3'
alias pip 'pip3'

fish_add_path /opt/homebrew/bin
fish_add_path $HOME/.cargo/env

# 汎用
alias .. 'cd ..'
alias ... 'cd ../..'
alias n 'nodebrew'

# docker関係
alias d 'docker'
alias dst 'docker stats'
alias dps 'docker ps'
alias dpa 'docker ps -a'
alias drm 'docker rm'
alias dim 'docker images'

alias dc 'docker-compose'
alias dcu 'docker-compose up'
alias dcd 'docker-compose down'

# Git
alias s 'git switch (git branch --sort=-authordate | cut -b 3- | grep -v -- "->" | peco | sed -e "s%origin/%%")'
alias fu "git commit --fixup=(git log --oneline | peco | cut -d ' ' -f 1)"
alias show "git show (git log --oneline | peco | cut -d ' ' -f 1)"

alias g 'git'
alias ga 'git add'
alias gau 'git add -u'
alias gaa 'git add -A'
alias gc 'git commit'
alias gcm 'git commit -m'
alias gca 'git commit --amend'
alias gst 'git status'
alias gl 'git log'
alias glo 'git log --oneline'
alias gdf 'git diff'
alias gs 'git switch'
alias gpso 'git push origin'
alias gm 'git push origin main'
alias gplo 'git pull origin'
alias gplm 'git pull origin main'
alias gfc 'git fetch'
alias gmg 'git merge'
alias gmgc 'git merge --continue'
alias gmga 'git merge --abort'
alias grb 'git rebase'
alias grbc 'git rebase --continue'
alias grba 'git rebase --abort'
alias grbia 'git rebase -i --autosquash'
alias grb 'git rebase'
alias gg 'git grep'
alias gs 'git switch'
alias gr 'git restore'

# npm
alias nr 'npm run'
alias nrd 'npm run dev'
alias nrb 'npm run build'
alias nrw 'npm run watch'
alias ns 'npm run start'
alias ni 'npm i'
alias nid 'npm i -D'
alias nui 'npm uninstall'

# yarn
alias y 'yarn'
alias yi 'yarn install'
alias ya 'yarn add'
alias yad 'yarn add -D'
alias yr 'yarn remove'
alias ys 'yarn start'
alias yd 'yarn dev'
alias yb 'yarn build'
alias yw 'yarn workspace'
Tips
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

Next.jsにStorybookを導入してTypeScriptで書けるようにする

この記事では、Next.jsにStorybookを導入してTypeScriptでReactコンポーネントを書けるようにする手順を紹介します。またその際に、私が踏み抜いたバグと解消法を全て共有します。

Next.jsとは、Vercelが作成しているReactのフレームワークです。 SSRにも対応しており、Reactで開発するならNext.jsかFacebook製のCreate React Appを使うのがスタンダードになっています。また、面倒な設定を書かなくてもすぐに使えるZero Configを標榜しており、実際にWebpackやTypeScriptと一緒にReactを書く際にも特別な準備は不要です。

Storybookとは、UIコンポーネントのカタログを作るツールです。 Storybookの実行環境はメインのアプリケーションとは独立しているため、UI作成時に試行錯誤をしてもメインのアプリに影響を及ぼさないのが大きなメリットです。Storybookはエンジニアとデザイナーの橋渡しをしてくれるツールであり、ReactやVue、Angularなどコンポーネント指向のフレームワークと併用することが多いです。

Next.jsで作ったアプリケーションがリリース済みで本番稼働中であったため、Storybookの導入は一筋縄ではいかなかったです。Next.js公式のStorybookの導入サンプルは序章に過ぎなかったんや…。

解説パンダくん
解説パンダくん

Next.jsのExamplesのページ には、StorybookをJSで表示するものと、TypeScriptを使うもの2種類のサンプルがあります。今回の設定はTypeScript版を参照したよ。

それでも頑張ってNext.js + TypeScriptの環境ではStorybookが動作するところまで持っていったので、同じようなエラーを踏んで困っている方のお役に立てれば幸いです。

前準備

Next.jsにTypeScriptを導入する

Next.jsのプロジェクトのセットアップが終わっているとします。TypeScriptとReact、Node.jsの型をインストールします。

$ npm install --save-dev typescript @types/react @types/node

次に、ディレクトリルートにtsconfig.jsonを作成して、以下のように記載します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

Next.jsにTypeScriptの導入する手順は以上です。

Next.jsにStorybookを導入する

Next.jsにStorybookを導入しましょう。説明を簡単にするために今回はアドオンを追加しません。

$ npm install -D @storybook/react

Storybookのインストールを終えたら、以下のコマンドをpackage.jsonに追加します。

package.json
{
  "scripts": {
    // ...
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  }
}

次に、プロジェクトルートに.storybookディレクトリを作成してmain.jsを作成します。stories に Storybook のコンポーネントを配置しているディレクトリのパスを記述します。

.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.tsx'],
}

以上で準備ができました。

Next.js + Storybookが動くまでに発生したエラーと解消法を発生順に列挙していく

$ npm run storybookを実行するとStorybookが立ち上がり、http://localhost:6006で表示できます。

Storybookの画面

しかし、現時点でコンポーネントは何も表示されません。そこであえて一番複雑なコンポーネントを表示しようと思ったら、見事にたくさんのエラーに遭遇しました。

なぜなら、Next.jsでReactのuseContext(Context API)を使っていたり、Next Routerを使っていたり、Google Analyticsを設定していたり、CSS Modulesを使っていたからです。

解説パンダくん
解説パンダくん

$ npm run storybook を実行するたびにエラーが発生したので、1つずつ潰していったんだ。Next.jsの機能をフル活用している稼働中のアプリならではのエラーだったよ。

以下ではそれぞれのエラー内容、エラーの原因、解決法を解説していきます。

(追記)Next.jsがv12.1、@storybook/react がv6.4のときのエラーと解消法

2022年2月20日時点の最新の Next.js のバージョンは v12.1.0、@storybook/react のバージョンは v6.4.19 です。本記事は元記事に追記をしており、追記したケースは左のバージョンで遭遇したエラーとその解消法です。

Storybook で Next.js の public ディレクトリ配下の画像などの静的ファイルを読み込みたい

main.js にstaticDirsで指定しましょう。

main.js
module.exports = {
  //...
  staticDirs: ['../public'],
}

参考: Serving static files via Storybook Configuration

Storybook で next/router をモックする

Storybook で next/router をモックする方法は2通りあります。1つは、Storybook のアドオンであるStorybook Addon Next Routerを使う方法。もう1つは、next-router-mockというパッケージを使う方法です。

前者は Storybook 専用であるのに対して、後者は Jest や Vitest と testing library react を使ったコンポーネントテストでも活用できます。このため、今回は後者を使うことにします。

以下のコマンドでインストールしましょう。

npm i -D next-router-mock

次に、.storybook/preview.jsを作成し、以下の記述を追加します。

.storybook/preview.js
import { addDecorator } from '@storybook/react'
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider'

addDecorator((Story) => (
  <MemoryRouterProvider>
    <Story />
  </MemoryRouterProvider>
))

これで useRouter を呼び出しているコンポーネントも Storybook で表示することができます。

参考: Usage with Storybook

Storybook で next/image をモックする

Storybook はコンポーネントの表示に特化したアプリケーションであり、本番で動作しているアプリケーションとは独立した実行環境を提供しています。一方、Next.js の Image Optimization(画像最適化)機能である next/image を使うためには、Next.js のサーバーが起動していることが必要です。

Storybook だけを動かせば UI を確認できることが強みであるので、画像最適化機能のためだけに Next.js のサーバーを立ち上げたくはありません。

そこで、以下の記述を.storybook/preview.jsに追加します。

.storybook/preview.js
import * as NextImage from 'next/image'

const OriginalNextImage = NextImage.default

Object.defineProperty(NextImage, 'default', {
  configurable: true,
  value: (props) => <OriginalNextImage {...props} placeholder={undefined} unoptimized />,
})

これで画像を表示できました。以下は React と Storybook のコンポーネントのサンプルコードです。

PostImage.tsx
import Image from 'next/image'

type Props = {
  src: string
  alt: string
}

const PostImage: React.VFC<Props> = ({ src, alt }) => {
  return (
    <Image width={400} height={300} src={src} alt={alt} />
  )
}

export default PostImage
PostImage.stories.tsx
import React, { ComponentProps } from 'react'

import PostImage from './PostImage'

export default {
  title: 'Domain/Post/PostImage',
}

export const Default = {
  render: ({ ...args }: ComponentProps<typeof PostImage>) => 
    <PostImage {...args} />,
  args: {
    src: '/img/2022/02/01/tailwind-ui.png',
    alt: 'text',
  },
}

Image コンポーネントをStorybookで表示できている

参考: Get started with Storybook and Next.js

Storybookでコンポーネント表示エリアをダークモード対応にする

ダークモードに対応しているコンポーネントをデザインするとき、Storybook の背景色がデフォルトの白のままだと開発しづらいです。

例えば、本ブログは背景色を黒く設定しているので、Storybook も同様の色になるようにしています。

Storybookで背景をダークモードにする

これを実現する方法は、.storybook に preview-body.htmlを作成し、以下の記述を追加するだけです。

.storybook/preview-body.html
<style>
    body {
        /* Tailwind CSS の bg-gray-900 */
        background-color: rgb(17 24 39);
    }
</style>

Storybook のテーマでも色変更をする方法はあるのですが、対象が左カラムの色など Storybook 自体の色の変更だったのでこの機能は使っていません。

恐らくこの機能を使って同様のことができると思うのですが、body に background-color を指定する方法の方が簡単なのでこちらを採用しています。

参考: Adding to body

Next.jsがv9.5、@storybook/react がv5.3のときのエラーと解消法

なお、これ以下のケースは記事を最初に執筆した2020年7月時点のものです。

この時の Next.js のバージョンは v9.5、@storybook/react のバージョンは v5.3 であり、他により良い解決法がある場合は Twitter で教えていただけると幸いです。

「Module parse failed: Unexpected character ’@’ (1:0)」でSCSSが読み込めない

Next.jsは9.3からCSS Modulesをサポートしています。この機能を使うと、Next.jsがビルドするReactコンポーネントからSCSSを読み込めます。

Header.tsx
import styles from './Header.module.scss'

export function Header() {
  return (
    <nav className={styles.error}>
      <p>Header</p>
    </nav>
  )
}

しかし、StorybookがSCSSを読み込めないため、以下のエラーが表示されます。

Module parse failed: Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type,
 currently no loaders are configured to process this file.
 See https://webpack.js.org/concepts#loaders
> @import '../../Assets/scss/lib/color';
| @import '../../Assets/scss/lib/variable';
| @import '../../Assets/scss/lib/icon';

Next.js + StorybookでSCSS(CSS Modules)を使うために、CSSに関するローダーを追加します。

$ npm install -D sass css-loader sass-loader style-loader

次に、StorybookのWebpackでこれらのローダーを使えるようにします。main.jsに以下の設定を追加します。

.storybook/main.js
module.exports = {
  webpackFinal: async (baseConfig) => {
    // scss を読み込む
    baseConfig.module.rules.push({
      test: /\.scss$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1, // 1 => postcss-loader
            modules: {
              localIdentName: '[local]___[hash:base64:2]',
            },
          },
        },
        'sass-loader',
      ],
    });

    return {...baseConfig};
  }
}

これでSCSSが読み込めない解消されました。

「Module not found: Error: Can’t resolve ‘src/hooks/useCounter.ts’」で絶対パスでのインポートができない(Absolute imports)

Next.jsの9.4から、モジュールを絶対パスでインポートができるようになりました。

Counter.tsx
import useCounter from 'src/hooks/useCounter'

export function Counter() {
  const [count, increment] = useCounter()

  return
    <>
      <p>{count}</p>
      <button onClick={() => increment()}>
        +
      </button>
    </>
  )
}

この機能を使っていると、以下のエラーが表示されました。

ERROR in ./src/Components/Header/Header.tsx
Module not found: Error: Can’t resolve ‘src/hooks/useCounter.ts’
 in ‘/Users/panda/nextjs/app/src/Components/Header’

Storybookでも絶対パスでモジュールをインポート(Absolute imports)するためには、.storybook/main.jsに以下の記述を追加します。

.storybook/main.js
module.exports = {
  webpackFinal: async (baseConfig) => {
    // @see https://github.com/storybookjs/storybook/issues/3916#issuecomment-407681239
    baseConfig.resolve.modules = [
      ...(baseConfig.resolve.modules || []),
      path.resolve('./'),
    ]

    // scss を読み込む
    // ...
  }
}

これで絶対パスでモジュールを読み込むことができました。(参考: I can not import with absolute path in Storybook

「Cannot read property ‘publicRuntimeConfig’ of undefined」でpublicRuntimeConfigから値を取得できない

publicRuntimeConfigを使うことにより、Next.jsでnext.config.jsから実行時に値を読み取れます。

next.config.js
module.exports = {
  serverRuntimeConfig: {
    // serverでの利用可能
    mySecret: 'secret',
    secondSecret: process.env.SECOND_SECRET,
  },
  publicRuntimeConfig: {
    // serverとclient両方で利用可能
    appHost: 'next-storybook-app.com',
  },
}

これでconfig.tsappHostの値を取得できます。

config.ts
import getConfig from 'next/config'

const { publicRuntimeConfig } = getConfig()
const { appHost } = publicRuntimeConfig

しかし、この機能を使っていると、Storybookで以下のエラーが表示されました。

Storybookのエラー

「Cannot read property ‘publicRuntimeConfig’ of undefined」は、.storybook/preview.jsに以下のように記述することで解決できます。

.storybook/preview.js
import { setConfig } from 'next/config';
import { publicRuntimeConfig } from '../next.config';

setConfig({ publicRuntimeConfig });

また、publicRuntimeConfigを呼び出しているconfig.tsも以下のように書き換えます。

config.ts
import getConfig from 'next/config'

const { publicRuntimeConfig = {} } = getConfig() || {}
const { appHost } = publicRuntimeConfig

これでStorybookでNext.jsのpublicRuntimeConfigを使っているファイルを読み込めました。(参考: publicRuntimeConfig undefined when using Storybook with Next.js

_app.tsxで読み込んでいるスタイルが当たらない

普段_app.tsxで読み込むようなscssファイルは、preview.jsで読み込むことでStorybookの各コンポーネントに適用されます。

.storybook/preview.js
import '../src/assets/scss/style.scss'

Google Analyticsをモックする

Next.jsでGoogle Analyticsを使っている場合は、gtagモックをpreview.jsに記述します。

.storybook/preview.js
window.gtag = () => {}

関連記事: Next.jsでGoogle Analyticsを使えるようにする

これでGoogle Analyticsのイベントをモックできました。

StorybookでuseContext(Context API)を使えるようにする

useContextを使っているコンポーネントをStorybookから呼び出したい場合は、StorybookのDecoratorを利用します。このDecoratorを使って、Contextを利用するコンポーネント(Consumer)をProviderでラップします。

Headerコンポーネントでstoreからユーザー名を取得しているケースを例に解説します。

Header.tsx
import React, { useContext } from 'react'
import { Context } from 'src/lib/store/context'

type Props = {
  username: string | null
  loggedIn: boolean
}

const Component: React.VFC<Props> = (props) => (
  <div>
    Hello,{' '}
    {props.loggedIn ? `${props.username}` : 'ゲスト' }さん
  </div>
)

const Container: React.VFC = () => {
  const { state } = useContext(Context)
  const loggedIn = state.user.loggedIn === true

  return <Component
    username={state.user.name}
    loggedIn={loggedIn}
  />
}

Container.displayName = 'Header'

export default Container

Storybook側のHeader.stories.tsxは以下のようになります。

Header.stories.tsx
import React from 'react'
import Header from './Header'
import { Context } from 'src/lib/store/context'

export default {
  title: 'Header',
}

export const header = () => <Header />

const store = {
  state: { user: { name: 'パンダ', loggedIn: true } },
  dispatch: () => {},
}

header.story = {
  decorators: [storyFn =>
    <Context.Provider value={store}>
      {storyFn()}
    </Context.Provider>
  ]
}

これでStorybookでuseContextを使っているReactコンポーネントを描画できました。

Storybookで使えるstoreのモックを作成する

上記でStoreをモックできましたが、毎回全てのコンポーネントにdecoratorを記述するのは面倒です。

Header.stories.tsxの中で、「非ログイン状態」と「ログイン済みでユーザー名がある状態」の2つのコンポーネントを作るときには、2つのdecoratorを記述しなければなりません。

しかも、コンポーネントの数はある状態の数×別の状態の数であるように、状態(state)の数の掛け算で増えていきます。

何度も同じコードを書くことは「繰り返しを避ける」というDRY原則に違反します。この問題を解決するために、Storeに格納する値をStorybookのコンポーネント側で自由に設定できるようなラッパー関数を作りました。

.storybook/store.ts
import React from "react";
import { Context, Store } from "../../src/Lib/store/context";
import { initialState } from "../../src/Lib/store/reducer";

type State = typeof initialState

// initialStateと外から与えられた値をマージする
const mockState = (state): State => ({...initialState, ...state})

const mockContextValue = (state): Store => ({
  state: mockState(state),
  // dispatcherをモックする
  dispatch: () => {},
})

export const withStore =
  (comp: React.ReactElement, state: Partial<State> | {} = {} ) => {
    const Component = () => comp
    // Providerでラップする
    Component.story = {
      decorators: [storyFn =>
        <Context.Provider value={mockContextValue(state)}>
          {storyFn()}
        </Context.Provider>
      ],
    }

    return Component
  }

withStore関数を使うことで、HeaderコンポーネントにモックのStoreを渡すだけで様々な状態を表現できるようになりました。

Header.stories.tsx
import React from 'react'
import Header from './Header'
import { Context } from 'src/lib/store/context'

export default {
  title: 'Header',
}

// 非ログイン
const guestStore = {
  user: {
    loggedIn: false
    name: null
  },
}

export const guest = withStore(<Header />, userSearchStore)

// ログイン済み
const userStore = {
  user: {
    loggedIn: true
    name: 'パンダ'
  },
}

export const guest = withStore(<Header />, userSearchStore)

ぜひ使ってみてください。

以下は古いバージョンで発生したエラーです

ここから以下は、 Next.js v9.5, Storybook v5.3 で発生したエラーとその解消法です。備忘録的に残しています。

この記事の更新日時点では、エラーが発生しなかったり、違う解決法があったりするので、ここより上の記述を参考にしてください。

(deprecated)Storybookのコンポーネントを複数のディレクトリから読み込む

Storybookのコンポーネントを複数のディレクトリから読み込むユースケースはそれほど多くないと思いますが、方法を記載しておきます。

preview.jsにstories.tsxファイルがあるコンポーネントを記載するだけです。

.storybook/preview.js
const req1 = require.context('../src', true, /.stories.tsx?$/)
const req2 = require.context('../stories', true, /.stories.tsx?$/)

configure([req1, req2], module)

(deprecated)「Cannot read property ‘pathname’ of null」でuseRouterが読み込めない

Next.jsでは、React HooksのuseRouterを使うとURLのpathnameやqueryを値として取得できます。

App.tsx
import { useRouter } from 'next/router'

export function App() {
  const { pathname } = useRouter()

  return <p>{pathname}</p>
  )
}

しかし、このままではStorybookで「Cannot read property ‘pathname’ of null」というエラーが表示されます。

そこで、.storybook/preview.jsに以下のように記述しNext.jsのuseRouterをモックすることでエラーを解決できます。

.storybook/preview.js
import * as nextRouter from 'next/router'

// ダミーデータは適宜変更する
nextRouter.useRouter = () => ({
  route: "/",
  pathname: "/about",
  query: { query: 'Next.js Storybook' },
  asPath: "",
  basePath: "",
})

また、Routerオブジェクトを使っている場合は以下のようにモックします。

.storybook/preview.js
import * as nextRouter from 'next/router'
import Router from 'next/router'

nextRouter.useRouter = () => ({
  // ...
})

nextRouter.router = {
  push: () => {},
  prefetch: () => new Promise((resolve, reject) => {}),
};

これで、StorybookでNext.jsのRouterをMockできました。(参考: How to mock useRouter?How to mock next/router in Storybook)

まとめ

エラーの発生時とエラーの解消なんて簡単なものですよ。未知のエラーに遭遇した時は、以下のステップをたどるだけです。

  1. 未知のエラーが発生する
  2. エラー文をコピーしてGoogle検索する
  3. GitHubのissue、もしくはStack Overflowを読む。なければissueを立てる
  4. スレッドの最初(問題提起)と流れを把握し、一番リアクションの多いリプライを読む
  5. 解決策を手元で試す
  6. 解決する
  7. 次のエラーが発生。1に戻る

この手順は歴としたLoop文ですね。breakがないからLoopを抜けられない?その通りです。あなたがエンジニアである限りはね。

我々にあるのはcoffee breakだけです。

解説パンダくん
解説パンダくん

......。お後がよろしいようで

Next.jsStorybookTypeScript
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

コミットに関する意思決定を楽にするため Commitizen に従う

私はコミットメッセージを書く際、 Commitizen の prefix を使っています。 2019年に Commitizen を知るまでは、コードの変更内容から適切なメッセージを書くことに頭を悩ませていました。

コミットメッセージの作成は小さな意思決定であり、自分の頭で一から考える上に繰り返し発生するタスクです。人が一日に下せる質の良い意思決定の数には上限があり、意志力は体力と同じように自然と消費されるものです。

このため、意志力の無駄遣いを避けて質の良い意思決定をするために余力を残すのであれば、コミットのたびに繰り返し発生する「コードの変更内容を自分で分類し、コミットメッセージを考える」というタスクのコストは極力抑えたいものです。

良いコードを書きたいという思いがあるなら尚更です。コミットメッセージで意志力を消費した結果、目の前のコードがリファクタリングできると気付いても「今は疲れているし、後でやるからいいか」とやり過ごしてしまうかもしれません。そして、その「後で」の機会が来ることは恐らくないでしょう。

意志力を回復させるためには、散歩をしたり風呂に入ったり映画やドラマを見るなど、決断が必要ない別のことに意識の対象を向けることが有効です。しかし、それならそもそも意思決定の数を減らせば良いのです。

このため自分は Commitizen に従っています。Commitizen はコミットを分類する prefix を自動で付与してくれるので、コミットメッセージを書く際の意思決定を軽くしてくれます。

本記事は、Commitizen の prefix とコミットメッセージの例を紹介するものです。 ただし、Commtizen の具体的な使い方は紹介しません。それは自分が Commitizen の prefix をコミットメッセージに付与しているものの、Commitizen の CLI ツールは使っていないからです。その理由は後述します(このため、本記事は Commitizen というより Conventional Commits の記事だとした方がより適切かもしれません)。

Commitizen に見るコミットのその分類

まず、Commtizen によるコミットの分類を見てみましょう。Commtizen は、コミットを11種類に分類し、それぞれの内容を簡潔に言い表す prefix を用意しています。

feat:     A new feature
fix:      A bug fix
docs:     Documentation only changes
style:    Changes that do not affect the meaning of the code
          (white-space, formatting, missing semi-colons, etc)
refactor: A code change that neither fixes a bug nor adds a feature
perf:     A code change that improves performance
test:     Adding missing tests or correcting existing tests
build:    Changes that affect the build system or external dependencies
          (example scopes: gulp, broccoli, npm)
ci:       Changes to our CI configuration files and scripts 
          (example scopes: Travis, Circle, BrowserStack, SauceLabs)
chore:    Other changes that don't modify src or test files
revert:   Reverts a previous commit

feat は feature の略で機能、fix は bug fix の fix というように、 prefix だけみても意味がわかりやすいものになっています。また、コード変更の種類が抜け漏れ重複なく(MECE)カバーされているため、コミットの内容はこれらのうちどれかに必ず当てはまるのではないでしょうか。

なお、日本語版は以下の通りです(cz-conventional-changelog-ja より引用)。

機能: 新機能
バグ修正: バグ修正
ドキュメント: ドキュメントのみの変更
スタイル: フォーマットの変更
        (コードの動作に影響しないスペース、フォーマット、セミコロンなど)
リファクタ: リファクタリングのための変更
          (機能追加やバグ修正を含まない)
パフォーマンス改善: パフォーマンスの改善のための変更
テスト:不足テストの追加や既存テストの修正 
ビルド: ビルドシステムや外部依存に関する変更
      (スコープ例: gulp, broccoli, npm)
CI: CI用の設定やスクリプトに関する変更
   (スコープ例: Travis, Circle, BrowserStack, SauceLabs)
雑務:その他の変更
    (ソースやテストの変更を含まない) 
復帰: 以前のコミットに復帰

コミットメッセージを考える際、上記の prefix の中から最適な prefix を選択すればそれだけでもコード変更の種類を簡潔に表現できます。

また、prefix はいずれか一つしか選択できません。このため「機能追加とリファクタリング」といったよくあるコード変更のパターンであっても、コミットを2つに分けておこうという意識が働きます。結果的に一つのコミットに複数の意味がある変更を盛り込まなくなるため、細かいことを忘れてしまった将来の自分や他のレビュワーが読みやすいコミット単位を自然に作れます。

なお、Commitizen のレポジトリに掲載されている画像は 2015年に作成されたもので内容が古いです。 この画像を元に Commitizen の prefix が紹介されている記事を多く見かけるため注意が必要です。

commitizen が公式で紹介している prefix のリスト

特に chore に注意が必要です。 例えばビルド関係の変更をする際、画像内で chore のカバー範囲だと表現されている一方で、現行の prefix では chore ではなく build の prefix 設けられており、こちらを選択する方がより適切です。

本ブログのコミット履歴の例

次に、prefix をつけた実際のコミットを紹介します。以下の例はこのブログのコミット履歴です。 読み下せるように、古いコミットは上に、新しいコミットは下に並べ替えています。一連のコミット内容は RSS フィードの作成と、作成したフィード生成関数に対するテストの修正などです。

feat: createPostFeed を追加
fix: ビルドのたびに lastBuildDate が変わり flaky なテストになるため日付を固定
refactor: rss 作成のテストを snapshot にする
feat: /api/feed RSS フィードを返す
feat: /feed RSS を表示する
feat: feed URL Header で表示する
docs: update CHANGELOG.md

ここでは prefix に feat, fix, refactor, docs を使っています。ここには出てきませんが、他にも GitHub Actions でテストを実行する yml を追加するときは ci を、ESLint の fix や Prettier でコードをフォーマットするなら style を使っています。

正直どれの prefix を使うか迷う場面も出てきたりはします。 上記の例では、失敗するテストの修正コミットに test を使うか迷った結果 fix を使っていたり、その他にデザインを変更する際に Tailwind CSS のクラス名の追加や削除、変更は feat か fix か迷うことがあります。ただ、そのようなケースはごく稀であり、深く気にする必要はないと割り切っています。

fish の関数を使ってコミットメッセージの prefix 設定を楽にする

さて、記事の冒頭で自分は Commitizen の prefix を使っているものの、 Commitizen というツール自体は使っていないと書きました。その理由は Commitizen が対話型の CLI を備えており、自分はそれが煩雑に感じるからです。

対話型の CLI はプロジェクトのセットアップなど一回きりだと気にはならないものの、コミットは短いサイクルで何度も行うアクションであるため質問と回答のサイクルが煩わしく思えてしまいます。

対話型インターフェースについては、『UNIXという考え方』(Amazon) という書籍の中で「定理8: 過度の拘束的インターフェースは避ける」という項目で以下のように書かれています。これが煩雑さを感じる原因を簡潔に言い表してます。

一旦、そのアプリケーション(注: 対話型のアプリケーション)をコマンドインタープリタから起動すると、そのアプリケーションが終了するまでコマンドインタープリタとの対話ができなくなる。ユーザーはアプリケーションのユーザーインターフェースの内部に拘束され、拘束を解くための行動を起こさない限り、その拘束から逃れられない。

そこで、結局やりたいことはコミットメッセージに prefix を付与するだけであり、自分は fish を使っていることから、fish の関数を作ってコミットメッセージに prefix を付与するようにしました。

.config/fish/functions/commit.fish
function gcf
	git commit -m "feat: $argv"
end

function gcfi
	git commit -m "fix: $argv"
end

function gcd
	git commit -m "docs: $argv"
end

function gcdu
	git commit -m "docs: update changelog"
end

function gcs
	git commit -m "style: $argv"
end

function gcr
	git commit -m "refactor: $argv"
end

function gcp
	git commit -m "perf: $argv"
end

function gct
	git commit -m "test: $argv"
end

function gcb
	git commit -m "build: $argv"
end

function gcci
	git commit -m "ci: $argv"
end

function gcc
	git commit -m "chore: $argv"
end

function gcrv
	git commit -m "revert: $argv"
end

gc は git commit の略です。fish の関数といっても、各 prefix に対応するコマンドを用意し、引数をコミットメッセージにするだけです。例えば、機能の追加なら以下のように gcf とメッセージを入力するだけで済みます。

$ gcf 素晴らしい機能を追加 

 No staged files match any configured task.
[main 712fc22] feat: 素晴らしい機能を追加
 3 files changed, 23 insertions(+), 1 deletion(-)

$ git log --oneline 
712fc22 (HEAD -> main) feat: 素晴らしい機能を追加

これで prefix をコミットメッセージに記入するコストを削減できます。 これはとても楽なのでおすすめのテクニックです。これは生産性向上というよりは効率アップのテクニックですね。

関連記事: 書籍「時短の科学」に生産性向上の手段を学ぶ

パッケージの追加と削除のコミット

なお、パッケージの追加と削除に関してはコマンドをそのままコミットメッセージにすると良いという話を聞きました。なるほど、それはその通りだと思ったのでそれ以降実践しています。

自分はパッケージ追加のコミットを簡単にするため、fish の alias にyarn addyarn add -Dを登録して、functions/commit.fishgit commit の gc を追加した gcya gcyad関数を作成しています。

.config/fish/config.fish
alias ya 'yarn add'
alias yad 'yarn add -D'
.config/fish/functions/commit.fish
function gcya
  git commit -m "yarn add $argv"
end

function gcyad
git commit -m "yarn add -D $argv"
end

これにより、例えばプロジェクトに react-use を追加する際、以下のようにできます。

$ ya react-use
$ git add .
$ gcya react-use

gcyaya に gc をつけるだけなので、矢印の↑キーを2回押して history を遡ってya react-useを表示し、この最初に ga を書けば済むのでとても簡単です。

綺麗なコミットメッセージを習慣にする

コミットメッセージを綺麗に書くというのは一つの習慣です。 prefix を選び、コードの変更内容に適したメッセージを考えて書く。これはエンジニアにとってルーティンと言ってもいいでしょう。

ルーティンは人が見ているか見ていないかに関係なく実行するものです。「コードレビューがある業務以外はコミットメッセージは適当に書く」という方もおられると思いますが、自分しか見ない Private レポジトリの個人プロジェクトであってもコミットメッセージを綺麗に書くという習慣を身につけることはおすすめです。

自分も最初は個人プロジェクトのコミットメッセージに気を使っていませんでした。ただ、個人プロジェクトだからといって自分の中のハードルを一旦下げてしまうと、業務でもその意識を引きずってしまいます。いざ綺麗に書こうと思っても「すぐに良いのは思いつかないから、まあこのくらいで大丈夫か」とメッセージが荒くなってしまうことに気づきました。

そのきっかけから、手を抜く方が非効率的だと悟りました。以来、誰に見せるわけでもないコミットメッセージあっても、メッセージを綺麗にしようという意識は捨てないようにしています。

最初は面倒と思うかもしれませんが、何度も繰り返していくうちに誰もが身につけている代表的な習慣「ハミガキ」と同じになります。歯を磨かず寝てしまったときに口の中に違和感を感じるのと同様に、綺麗にコミットメッセージを書く習慣が身につけば、あまり良いとは思えない自分のコミットメッセージに違和感を覚えるはずです。 そして、もし他の人のコミットメッセージにその違和感があるならそれもレビューのポイントになります。

まとめ

コミットメッセージに限らず、一般的にレビューというものは違和感をピックアップして言語化していく作業であると考えています。

有り体に言ってしまえば、綺麗を知っていないと綺麗と汚いの見分けがつきません。違和感とは、ある対象が自分にとっての理想や常識とかけ離れているときに生じるものです。特にリファクタリングの文脈で使われる Code Smell(コードの嫌な臭い)という言葉はこの違和感を表現した秀逸なメタファーです。

正直なところコミットメッセージをレビューされることはほとんどないため、いくら書いてもうまくなった気がしません。このため、自分の書くコミットメッセージは自己採点で60 ~ 80点ばかりですが、それでも綺麗に書く意識は持ち続けたいと思っています。

この記事がコミットメッセージを綺麗に書きたいという方の参考になれば幸いです。

最後に、私の同僚である 02 さんの「リーダブルコミットのすゝめ」 というスライドも本記事と同様のテーマを取り扱っており、とても参考になるのでおすすめです。

ToolsCommitizen
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

Software Design 2022年3月号に寄稿した

嬉しいことに、2月18日発売の Software Design 2022年3月号に寄稿する機会を頂いた。 特集のタイトルは「自動テストとテスト駆動開発、その全体像」。自動テスト、テストファースト、テスト駆動開発(TDD)の全体像を解説する第1章を @t_wada さん(和田さん)が執筆し、自分は TDD を JavaScript で実践する第2章、第3章の執筆を担当した。

寄稿の経緯は以下の通りだ。 昨年12月に技術評論社の @akito912912 さん(以下、中田さん)から Twitter で特集の一部を執筆しないかと DM を頂いた。願ってもない機会と思い二つ返事でお受けした。以前書いた TDD の解説記事 を目にして頂いたことが声掛けのきっかけになったそうだ。

実は、中田さんから連絡を頂いたのはこれで2回目だった。 去年5月にも同氏から Software Design で React の特集を組む ことになったので、 「React を深く知るための入口」 という記事のような内容で執筆可能かと連絡を貰っていた。

しかし、当時自分は Twitter の DM を頻繁に見ておらず、メッセージに気づいたのが原稿の締め切り1ヶ月後だった。これは2021年でトップ3に入るくらい悔しいことで、DM を確認していないことに内心忸怩たる思いがあった。それ以来、TweetDeck に DM のカラムを追加して、連絡が入ればすぐに対応できるようにした。

この再発防止策が功を奏して、数ヶ月後に再び中田さんから連絡を頂いた時は「ぜひ!」とすぐに返事ができた。 返事をした時間を見ると、それはメッセージを貰ってからたった1時間半後だった。

さて、TDD の勉強会に参加して記事を書いたら雑誌への寄稿の機会を頂いた、という本記事のタイトル回収は以上で終わりである。 ここからは、その勉強会中とその後にあったことを紹介していきたい。

2019年にTDDの勉強会に参加した

全ての始まりは2019年11月に TDD の勉強会に参加したことだ。 勉強会の名前は TDDBC Sendai 9th(以下、TDDBC)。 BC は Boot Camp の略だ。

当時私は「プロフェッショナルは TDD を実践するものだ」と述べられているボブおじさんの Clean Coder (Clean Code ではない)を読んで、自分もやるぞと意気軒昂たる若者だった。 しかし、「最近新版が出た」と噂になっていた TDD本 を買ってみるも、いまいち実践方法がわからなかった。

そんな折、何がきっかけだったか忘れたが TDDBC 開催の情報を目にした。しかも TDD 本の訳者の和田さんが講師になるという。

ただし、開催地は仙台。東京から仙台へは新幹線代がかかる。当時、日本の給与所得者の平均給与も貰っていなかった自分には痛い出費だ。一応、会社から勉強会の交通費は出る。しかし、それは都内または関東近郊までの範囲の勉強会だけだ。なので仙台への往復代2万円超は全額自己負担になるとわかっていた。 それでも、自己投資と考えて思い切って参加ボタンを押した。

仙台には何度か訪れたことがある。そのうち一度は中学生の時だ。親に頼んで親しい友人と一緒に連れて行ってもらい、ジョジョの聖地巡りをした。仙台はジョジョ4部の舞台だったからだ。作中に登場する靴のムカデ屋にのモデルになった店にも行った。

あの頃、友人と二人で店の中に入り「この店がジョジョに登場する店なんですね」と初老の店主に尋ねたら「そうなんだよ。でも漫画の中だと私は爆破されて死んでしまうんだけどね!」と泣き笑いの一歩手前のような、嬉しさと寂しさが入り混じった顔で彼は笑っていた。聖地巡礼という言葉が一般化し、ジョジョはアニメ化されてさらに人気に火がついた今、もっと多くの人が訪れているのだろうか。

仙台の勉強会で古い友人に再会した

閑話休題。仙台駅を降りて地下鉄に乗り、 TDDBC の会場に向かう。 会場はコワーキングスペースとして使われているマンションの一室だ。部屋にいくつも長テーブルが置かれており、1テーブルあたり2人が着席できるようになっている。ペアプロをしやすいような座席配置をしているのだろうということは想像ができた。

勉強会が始まる前、トイレに行くため席を立った。するとトイレにパープル色のウルトラライトダウンを着ている短髪男がいた。この上着を着る男は自分が知る限り一人しかいないと思い、もしやと思って話しかけてみるとやはり大学の寮(熊野寮)時代の友人だった。 実に4年ぶりの再会だ(写真はメルカリから)。

紫のウルトラライトダウン

久闊を叙しているうちに、勉強会開始の時間になったので席に戻った。彼は卒業前に「地元で公務員になる」と言って去っていったのだが、その後エンジニアになったことは風の噂で耳にしていた。

しかし、仙台の勉強会で彼と再会したのは驚きだった。 彼と最後に連絡を取ったのは自分が新卒で入社した総合商社(経理配属)を辞めてエンジニアになるという相談だった。それは自分が実際にエンジニアとして就職する前だったので、向こうからしても本当にエンジニアとして働いていることは驚きだっただろう。

彼は頭がキレる男だった。そして、その頭の良さをマージャンで特定の牌を引く確率をひたすら計算するなど変なところで発揮している愛すべき男だった。 思えば熊野寮にはそんな人ばかりいた。これはたまたまだが、熊野寮の自分が住んでいたブロックの同期や後輩たちは、卒業後の進路にソフトウェアエンジニアを選んだ者が多い。自分も含め、転職して未経験からエンジニアになった人も何人もいる。この話はまたどこかで書きたい。

さて、一緒に席に戻るとたまたま彼が確保した席は自分の斜め前だった。勉強会が始まり、和田さんが基調講演をしている間、前を向くとどうしても彼のパソコンの画面が目に入る。何をしてるんだろうと見てみると彼は Twitter でタイムラインに延々と流れてくるマンガの投稿を一つずつ拡大して読んでいた。

自分は必死にメモを取っていた一方、全然話を聞いてないように見える彼はやはり愛すべき男だと思っていた。すると、彼は要所要所で @t_wada さんの重要な発言をツイートしていた。相互フォローの関係になったのでタイムラインに彼のツイートが流れてくるのだ。

しかも、たまに自分が聞き逃したところだったり、すぐに理解できないところを「これはこういうこと」と解説するツイートをするので思わず舌を巻いた。 そんな面白い男だ。

TDDBC はとても温かい勉強会だった

一通り予定されていたメニューをこなした後、最後に懇親会があった。 懇親会の中で飛び込み LT があったので、自分も記念にと「主要なPHPフレームワークをテストの点から見る」という社内用に準備していたスライドを発表した。内容は Slim4、Lumen(Laravel の代わりに)、BEAR.Sunday でテストの書き方を比較したものだ(LT は前のパソコンに入っていたので残念ながらスライドは行方不明)。

この LT 会で一つだけ後悔していることがある。LT の掴みに「ここ仙台は特別な地。スタンド使いとスタンド使いが引かれ合うように、大学時代の友人と思わぬ再会をしました」と話したら、オーディエンスの方から「あなたのスタンドは何ですか!」と聞かれ、何も準備していなかったのでしどろもどろになってしまった。

せっかく聞いてくれた人に申し訳ない。何と答えば場を沸かせられたのだろうか、いまだに答えは出ていない。 兎にも角にも、TDDBC に参加している人はみな優しくて志高く、1日を通してとても温かさを感じる勉強会だった。

TDD 本に和田さんからサインを貰った

懇親会も終盤に差し掛かり、終電の時間が近づいてきた。友人とまた会う約束をして後ろ髪を引かれつつ会場を去った。新幹線に乗り込んで発車待ちの間その日の出来事を思い返していると、なんと前の車両に和田さんが乗ってくるのが見えた。 この日は一日中和田さんを見ていたので見間違えるはずがない。

実は懇親会の最後の方に、TDD本や他のテストの本の販売があって、買っても持参した本でもその場で和田さんからサインを貰えるという流れになっていた。自分は TDD 本を持参していたのだが、何となく気恥ずかしくて終電を口実にサインを貰いに行かなかった。

しかし、これはせっかくの機会と思い、意を決して前の車両の和田さんの席まで向かった。 そして「和田さんですよね。勉強会とてもためになりました。先ほど TDDBC に参加したものです。もしよければサインお願いします!」と TDD 本を出したところ、快く引き受けてくださった。

サインを書いてもらっているあいだ、少し間があったので「率直に自分の LT の内容どうでしたか」と恐る恐る尋ねてみた。すると、「有名なフレームワークの比較はよくあるが、テストの点から比べたところに特色があって良かった」と答えてくださった。これはとても嬉しかった。続いてサインの宛名を尋ねられたので、パンダでお願いしますと答えた。

和田さんのサイン入りの中表紙

達成感と満足感の中、日帰りでも参加してよかったと充実した気持ちで東京に帰った。

アドベントカレンダーで記事を書いたら初めてはてブのトレンド入りした

話はここで終わらない。東京に戻って数週間後、TDDBC で学んだことを社内勉強会で発表した。さらに、社内で初めてアドベントカレンダーをやるということで、それならと興奮冷めやらぬうちに TDD をテーマに記事を書くことを決めた。

それまでいくつか技術ブログはいくつか書いていたものの、いまいちどんなことを書けば読んでもらえるかわからず、多くない分量で Tips を書くというようなことをしていた。

しかし、今回は長くても書いてみようと思い、気合を入れて土日を使って執筆して自分の技術ブログに公開した(「テスト駆動開発(TDD)とは何か。コードで実践方法を解説します」)。本当に記事は読まれるだろうかと「記事を公開しました」とツイートをした。すると、いいねとリツイート数が見る見るうちに増えていった。

自分のブログに仕込んだ Google Analytics のリアルタイムの数字もこれまで見たことのない数字になっていた。数字が伸びていることに驚いていると、同僚が「はてブのトレンド入りしてる」と教えてくれた(記事公開時はオフラインで出勤していた)。

これが初めてのトレンド入りだった。 また、当時のマネージャーが 1on1 で「トレンド入りおめでとう!ブログにはてブのシェアボタンを置いたらもっと伸びると思うよ」とアドバイスをくれたので、その日の夜に早速実装してみた。

この経験から「自分が語るのに相応しいか不安を感じる骨太なテーマであっても、時間をかけて網羅的でまとまった分量の記事を書くと読んでもらえる」ことがわかった。 しかも、長い記事を書くと多くの方があとで読む目的ではてなブのスターを押してくれることも理解した。

これ以降、技術記事を書くモチベーションが上がった。Twitter で「ためになる」「わかりやすい」という感想を頂いたことも、もっと記事を書いていこうと思う原動力になった。翌年の2020年に記事を合計44本書いたことがその証左だ。以上のことから、この TDD の記事は自分にとって思い入れのある記事となった。

Software Design への寄稿の機会を頂いた

さて、そんな3年前の記事を編集者の中田さんに読んで頂き、Software Design への寄稿のチャンスを頂いた。しかも、あの時サインを貰った和田さんが監修してくださるという。 過去に参加した勉強会の経験と、過去に作成した記事が今でも自分に機会をもたらしてくれる資産となったと思った。

自分はコスパという単語はあまり好きではない。もしコスパを気にしてあの時の新幹線代をケチっていたら、上に書いたようなお金で買えない経験は得られなかっただろう。

リターンは将来自分が想像できない様々な形で発生することを考えると、その時点で見えているコストとベネフィットを比較してコスパを考えることは、実は割に合わないのではないだろうか。 コスパには時間軸はなく、特定のコストに対する将来のリターンを考慮するとむしろ ROI で考える方が良い。

まとめ

思えば、TDDBC への参加がミラクルの始まりだった。 勉強会で古い友人に偶然再開し、和田さんにサインを貰い、書いた記事がはてブとTwitterでバズり、果ては専門誌 Software Design への寄稿の機会まで頂いた。

新幹線代という金銭面、記事を執筆した土日という時間面のコストは無駄にならない自己投資だった。過去の自分の意思決定に感謝している。確かに パレートの法則 の通り、自分が書いた記事の8割は全然読まれていないと考えている。しかし、残りの2割が全体のコストを上回るリターンをもたらしてくれるということを信じて今日も記事を書き続けている。

もちろん、執筆の目的はリターンではなく、自分は書くことが好きだから純粋に楽しんで書いているということが大前提だ。その気持ちで技術記事でもエッセイでもどんどん書いていきたい。

最後に。TDD というテクニックはダイレクトにコーディング体験を変えてくれる。 やるべきことに集中できるし、自分のコードに自信を持てるようになる。ぜひ興味のある方は Software Design 2022年3月号 を手に取ってみてください。また、動画で TDD を体験してみたい方は和田さんの YouTube 動画 をご覧ください。

TDD
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

「時短の科学」という書籍には生産性に対する疑問の答えが書かれている

世間では生産性向上や DX がお題目のように唱えられている。ソフトウェアエンジニアとしての業務効率化にあたり、自分は何から始めれば良いか考えるために「時短の科学」を読んでみた。この本では接客を中心とするサービス業を題材に、生産性とは何かを定義し、それを向上させた手段とその結果どのようにビジネスがうまく回り始めたのか、様々な事例が紹介されている。

これまで製造業の分野で議論されていた生産性の向上について、製造業と違ってサービス業では銀の弾丸(魔法の杖)がないとされてきた。サービス業では顧客の要求が多様であるため、一律にこれをすれば生産性がアップするという方法が編み出されていなかったからだ。

それでも何か方法はあるのではないかとフィールドワークを経て著者が帰納的に導き出された法則が現場のエピソードともに紹介されている。

接客のサービス業と開発のIT業界とは働き方が異なるものの、ソフトウェアエンジニアの観点から、この書籍で何か活かせることはないかと考えながら読んでみた。結果、もちろんホテルや旅館、飲食店の事例をそのまま当てはめることはできないものの、いくつもの現場を見てきた著者が抽出した法則は、エンジニアの働き方にも役に立つところがあると思い記事を書くことにした。

一言で書くと、この書籍は「生産性」に対する議論の土台を提供している。これを読んだあとは「生産性ってなんだろう」「向上といってもどうすれば良いのだろう」という疑問に答えることができるだろう。

「生産性向上」を宣言しても仕事は楽にならない

本記事は書評ではないので、満遍なく書籍の内容を紹介するものではない。だが、書籍内で役に立つと思ったことを抜き出して紹介していきたい。

その際、本書の「著者が赴いた現場の多数の事例を引用しながら、アルバイトをしたことがある人なら誰でも頷くような身近な困り事を仕組みで解決していく」という重要な持ち味が抜け落ちてしまうことはご了承願いたい。

そもそも生産性とは労働生産性であり、これは「労働生産性 = 付加価値 / 労働投入量」と定義されている。このため、労働時間の削減、つまり人を減らしたり残業を削れば、数式上は生産性が向上したといえる。あるいは、同じ人員で労働時間を増やさず売上を伸ばすことができた場合、これも生産性の向上だ。

しかし、現実問題として「生産性を向上させよう」と宣言しても1週間かかっていた仕事が3日で完成するわけではないし、同じ仕事をしているままでは売り上げは伸びない。

世間では「生産性向上」がただの効率アップと勘違いされている

では、同じ仕事を終えるにあたり、作業時間を短縮すれば良いのではないかという考えもある。しかし、これこそが罠なのだ。本書ではこれは生産性の向上ではなく「効率アップ」と分類されている。

効率アップについて、書籍内では「店員さんがお客さんに水を出すスピードを上げる」という例があげられてる。これをパソコンを使った業務に当てはめると、「ショートカットキーをたくさん覚えてマウスクリックの回数を減らす」というようにも言えるだろう。3秒かかった作業が1秒になるというのだ。

これは効率アップであって生産性向上ではない(なお、ショートカットキーを暗記すること自体を否定するものではない)。

世間でいう生産性の向上の議論を見ても確かに後者の域を超えないように思うこともある。確かにこちらの方があれこれ悩まずに済むし、今日からすぐにでも実践できる。しかし、これこそが生産性の勘違いという罠だ。

生産性向上とは、今の仕事を分類し、無駄をなくして仕事に集中すること

結論から書くと、生産性の向上とは無駄をやめることだ。そこで本書では業務を以下のような3つのタイプに分類している。

  • 顧客満足に繋がらず、従業員もやめたいと思っている無駄
  • 顧客満足に繋がらないが、必要な作業
  • 顧客満足度の向上につながる仕事

以下ではそれぞれ無駄、作業、仕事と呼ぶ。先ほど挙げた例を引き合いに出すと、もし店員が水を直接お客さんに出さないことが顧客満足度の低下に繋がらないとすれば、店員が水を出すスピードを上げるのではなくセルフサービス用にドリンクサーバーを用意すれば良い。するとお客さんは自分が水を求めるタイミングで非同期に水を汲みに行ける。

もしセルフサービスが顧客満足度の低下に繋がるなら、それは無駄ではなくて顧客満足度の向上に繋がる仕事だという発見があったと言うことで元に戻せば良い。

もう一つ別の事例を挙げると、書籍内では「旅館で部屋用の浴衣のサイズを各部屋ごとにS・M・Lと置いていたが事前の準備が大変だった。それをチェックイン時に係が宿泊客の背格好を見て、浴衣のサイズを判断して渡すようにした」という改善例が紹介されていた。

業務フローの改善を決めた人物は「顧客から不満の声が上がるなら元に戻す。しかし、2週間ほど様子を見て何もクレームがなければ新しいやり方を採用する」と考えて現場で実行。クレームは1件もなかったそうだ。エラーが出たらロールバックするのと同じ考えだ。あるいは、カナリアリリースのように一部の宿泊客にだけ試してみても良いかもしれない。

顧客に受け入れられなかったら元に戻す

カナリアリリースといえば、料理人も実はこの方法を採用していると聞く。コース料理の中の一皿を新しい料理と入れ替え、しばらく様子を見る。ウェイターに感想を伺わせるか、それが無理なら厨房に下がってきた皿に料理が残されているか、完食されているかを見て採用するか判断するというのだ。エンジニアリングと思わぬところに共通点があって面白い。

閑話休題。生産性の向上とは、つまり無駄をなくして、作業を減らし、仕事に集中することだ。自分は社会人1年目で経理に配属されたのだが、紙の請求書を1枚1枚めくりながら電卓を叩くスピードが早くなったら周りから褒められたことを思い出す。今ならバクラク請求書を使えばOCRで読み取るだけで済む。これこそが業務フローの改善による生産性の向上だ。

さて、短期で怠惰、そして傲慢なエンジニアなら無駄をなくして作業を減らす、ということは容易に想像できるだろう。さらに現場でも日々実践しているはずだ。しかし、本書でも紹介されている顧客満足を向上させる仕事に集中するということを実践できている現場は少ないのではないだろうか。

仕事に集中すると顧客の満足度が上がる

仕事とは、顧客満足度の向上に繋がることだ。それは接客業だとお客さんに直接ありがとうと感謝されるようなことを指す。しかし、こと IT 業界においては「このシステム使いづらい」「機能が足りない」「バグが出てます」と恨まれることはあってもユーザーから直接感謝されることはほとんどない。間接的にはもちろんあるが、その辺りがサービス業と異なるところだ。

一方、エンジニアの仕事では「顧客の課題を解決する」「ユーザーと向き合う」ということが一番重要なのは異論がないだろう。つまり顧客と向き合い続けなければいけないのだ。サービス開発をする仕事に携わっている方であれば、これはエンジニアでもデザイナーでもプロダクトマネージャーでも同じである。だが、なかなかこれを文字通り実践するのは難しいのも現実だ。

「偉い人が求めるから」「Aさんが絶対あった方がいいというから」「競合には既にあるから」ある機能を実装したいという要望は顧客から一歩遠い。そうではなく、あくまでユーザー起点で施策を打っていく。この辺りはリーンスタートアップで説かれていることと同じだ。開発の無駄がなくなる結果、生産性が向上するのだ。

好循環のサイクルを回す

ユーザーは面倒なジョブを片付けてくれたことに対してお金を払う(ジョブ理論)。無駄をなくして顧客満足度が向上する仕事に集中した結果、かつてのモーレツ社員のように労働時間を増やさなくても売上が向上する。しかも、開発チームは作ったものを顧客が喜んで使ってくれることがわかる。すると、またユーザーが求める(=喜ぶ)ものを作って出す。こうして弾み車が回っていく(弾み車の法則)。実際、本書でも「現場には好循環と悪循環しかない」と喝破されている。

だからこそ、施策を立てる人にこそ一番ユーザーの声を聞いてほしいし、エンジニアも「この施策はユーザーの要望からきたものですか」と問うても良いと考えている。それが無理なら仮説でもいいのだが、その仮説の根拠をできるだけ確固たるものにしておきたい。ここでも推測より計測である。

ユーザーはおそらくこんな課題を抱えているだろう、という推測をやめる。まずは実際に課題を聞く。お問い合わせや改善要望を読む。施策を考える前にこの一手間を加えることで、これから数ヶ月取り組むであろう業務、機能がユーザーが求めていない無駄なものではなく、顧客満足度の向上に繋がる仕事に変貌していく。

顧客の満足度向上が仕事に対するモチベーションを向上に繋がる

会社生活において給料アップや突然のボーナス、上司・同僚からの賞賛も嬉しいものだが、自分が行った仕事に対するお客さんの感謝が「またユーザーのためになることをしよう」というモチベーション向上に繋がる。

これは2020年の出来事だが、自分は今でも Twitter で自社で開発していたプロダクトに対する不満を書いていた有料ユーザーの方にチームでアポをとってユーザーインタビューをしたことを覚えている。要望を聞いてこちらの立場・考えを説明し、1ヶ月かけてある機能を実装した。

その後、再度リモートでのインタビューをしたところ、大変好評頂き、インタビュー後に Twitter で「新機能がとても良い」とプロダクトを宣伝してくださった。この出来事を通してリモート環境下ながらチームの一体感が増した上に、顧客の課題を解決することの実感を得た。これが本当の「仕事」だったのだ。

「顧客の声を聞く」という難しさに向き合う

ただし、現場の開発者としてはUXリサーチやユーザーインタビューで実際にプロダクトを触ってもらって声を聞くことにハードルがあることは認識している。今のプロダクトのフェーズ、ユーザーのプロダクトを使っている度合い、リモートなら画面共有の抵抗などちょっと思いつくだけでもハードルに枚挙に暇がない。

そして、そのハードルを超えて話を聞けたとしても、実はそれだけでは何を開発すればいいかわからないというのもまた現実だ。ある要望は開発者側からは一見無駄に思えたり、開発に時間がかかりすぎたり、その要望に応じても他のユーザーは喜んでくれるかわからない。だが、この難しい現実と向き合い始めた時、五里を覆っていた霧が少しだけ晴れ始めるのである。

最後に。書籍内に「うちの店はいつも人手不足だという嘆きは、仕事の生産性が低いと言っていることと同じ」という厳しい指摘がある。IT業界では人材不足が常に叫ばれているが、さて、これをどう捉えたらいいものか。この「時短の科学」に解決のヒントが書かれているかもしれない。

なお、本文中に少し触れたように、本書の教訓は エッセンシャル思考リーンスタートアップ で補えるかもしれないが、本書の中でも触れられているように戦略より戦術、知識ばかりの頭でっかちになるより徹底して実行することが重要と説かれている。本書では日本のサービス業における生産性向上の豊富な事例が紹介されているため、より身近に感じることができることが上記の2冊と趣を異にするところだろう。

ProductivityBook
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer