SmartHR Tech Blog

SmartHR 開発者ブログ

「SmartHRのペーパーレス年末調整」のアンケート画面の文言をmicroCMSで管理して、Storybookでプレビューできるようにした話

この記事はSmartHR Advent Calendar 2022の19日目です。

こんにちは!

SmartHRのプロダクトエンジニアの@diescakeです!

この記事では、SmartHRが提供している「ペーパーレス年末調整(以降、年末調整機能)」のアンケート画面の文言をmicroCMSで管理してStorybookでプレビューできるようにした話をご紹介します。

前提となる年末調整機能の概要や課題の話を含みますが、技術的に「どういう問題に直面して、どう解決したか?」という手段の話をメインとしています。

同様のユースケースをまるっとなぞりたい場面は少ないかもしれませんが、技術的に工夫した点をピックアップしていくので何かしらの参考になれば幸いです!🙏

前提となる「年末調整機能」について

年末調整機能のイメージ画像

年末調整機能はSmartHRが提供しているサービスの1つです。大きく分けて以下2つの機能があり、年末調整に関わる労務担当者さまや従業員さまの負担を軽減して手続きを効率化することを目的としています。

  • 労務担当者さまが、業務として年末調整を遂行するための機能
  • 従業員さまが、自身の年末調整に必要な申告書を作成・提出するための機能

タイトルにも入っている「アンケート画面」は、後者の機能に該当します。

アンケート画面の一例

従業員がアンケート形式で質問に答えていくことで年末調整の各種申告書が手軽に作成できる仕組みを採用しています。

このアンケート画面をmicroCMSを利用して開発しやすくしました。

できあがった「アンケート画面の文言をプレビューする」デモ

アンケート画面の文言をプレビューしている様子

microCMSの入稿画面の「画面プレビュー」ボタンから、アンケート画面のプレビューを表示して文言を確認している様子です。

これらの文言は編集後に確定しなくても下書き状態でプレビューできるので、実際の画面をみながら文言の検討ができます。

アンケート画面には、設問の見出しや説明文の他にダイアログによる補足・ボタン名などの要素があり、microCMSではこれらも含めて編集が可能です。

文言を取り巻くデータフローの全体像はこんな感じ

文言を取り巻くデータフロー図

いきなりですが、全体像はこのようになっています。

詳しくは後述しますが、設計方針として最も重要なポイントは「文言をプレビューする環境」と「本番環境」で文言データの参照方法を変えている点です。

  • 文言プレビュー環境では、動的にmicroCMSのAPIを呼び出し文言データを取得して表示する
  • 本番環境では、静的なデータ(文言データファイル)としてbundleした文言を表示する

microCMSで確定した文言を本番環境に反映するためには、「文言データファイル生成スクリプト」で「文言データファイル」を最新の状態に更新してから、通常のソースコードと同様にコミット、リポジトリにプッシュします。

もともとのアンケート画面の実装は、ソースコードに文言をハードコードしていました。このときのレビューやリリースの運用を崩さないという方針で設計しています。

個々の要素については追って詳しく書きます!

なぜ文言をプレビューできる環境を作ろうと思ったの?

時は遡り、2021年の年末調整が一段落した際にアンケート画面の文言検討・適用について振り返りを実施すると、さまざまな課題があがりました。

これらの課題には文言検討フローや運用の改善だけでは対策が難しいものもあり、プロダクトの改修・ツール類の導入によって根本的な解決が図れないだろうか、と考え始めたのがきっかけです。

具体的にフォーカスしたのは以下の課題です。

課題1:アンケート画面をイメージしながら文言を検討できていなかった

2021年はスプレッドシートでアンケートの文言検討を進めていましたが、動作や画面をイメージできていないことで、いざアンケート画面に適用してみると不整合や違和感に気づき、多数の手戻りが発生していました。 アンケート画面の文言検討においては、税制・法改正や様々なフィードバックの考慮が必要になるほか、影響が大きいものはアンケート全体の整合性を取る必要があります。

また、本来の目的である「よりよいユーザー体験」を考えた場合、文言の改修のみに囚われずに、UIの調整や回答フローの変更など、取れる手段はたくさんあります。文言のみに着目すると選択の幅が狭まってしまうため、なるべく実際の動作環境に近い画面を見ながら思考することは重要だと考えています。

課題2:アンケート画面への文言適用に工数がかかった

続いて、文言検討で固まった文言をアンケート画面へ適用するためにそれなりに工数がかかりました。

アンケートは約100画面あります。 毎年すべての画面の文言に手を加えるわけではありませんが、広く改善を盛り込むとどうしても対象となる画面は増えます。

1画面あたりの変更は大した手間ではありませんが、「課題1」による手戻りも加わって予想以上に時間がかかっていました。

というわけで、上記2つの課題を以下のように解決しようとしました。

  • 課題1 ➔ microCMSで編集した文言を実際の画面で確認しながら文言検討を可能にする
  • 課題2 ➔ microCMSで確定した文言は「文言データファイル更新スクリプト」を実行するだけでプロダクトに反映可能にする

次に、具体的に実現した方法について説明します。

「文言を取り巻くデータフロー」の具体的な仕組みと実装

先にも掲載した「文言を取り巻くデータフロー図」の変更前後を比較すると、以下のように変わりました。

構成図
変更前 変更前の文言を取り巻くデータフロー図
変更後 変更後の文言を取り巻くデータフロー図

変更前は、アンケート画面に対応するReactコンポーネントに直接文言がハードコードされていて、WebpackでbundleするというよくあるSPAの構成です。

変更後でも仕組みは大きく変わりませんが、Reactコンポーネントから文言が外出しされました。Reactコンポーネントは文言が集約された「文言データファイル」をimportして、文言を参照する構成に変わっています。

この「文言データファイル」は、「文言データファイル生成スクリプト」を実行することでmicroCMSの最新の文言を取得して生成・更新されます。

具体的に、変更後の構成図の各ポイントについて説明します。

①microCMSの文言管理画面

microCMSの文言管理画面

microCMSは日本製のヘッドレスCMSです。

プロトタイプを実装した際に好感触だったため採用しました。

開発者観点で導入がスムーズなだけではなく、以下のような「入稿者をフォローする機能」が充実しているほか、操作性も高いため、運用が手離れしやすいと感じました。

  • 入稿画面から文言をプレビューするためのボタン(動線)を設置できる
    • プレビューする際は、別途共有しているリンクを開いて…といった手順が不要
  • 個々のフォームにバリデーションルールを設定できる
    • 正規表現や文字数上限、ユニーク制約、可変長フォームのmin~maxなど
  • 個々のフォームに「フォームに関するメモ」を記載できる
    • 期待する具体例や注意事項を添えられる
  • リッチエディタで利用可能なスタイルを限定できる
    • 見出しはh2〜h4のみといった細かな制限ができる
  • APIに紐づけて注意事項や社内ドキュメントへのリンクを設置できる

また、microCMSにはレビュー機能があり、文言検討におけるレビューから確定までの工程をサービス上で実施できる点も魅力的でした。

microCMSの運用に関して、工夫した点を以下にピックアップします。

工夫した点1:テーブル(table要素)の実現

本文の入力にはmicroCMSのリッチエディタフィールドを利用していますが、このエディタはテーブル要素の入力に対応していません。

これについては、microCMSのヘルプサイトにもリッチエディタでテーブル(表)の入稿はできますか?という記事があります。

カスタムフィールドでリッチエディタ・HTMLを選択する様子

私はここで紹介されているリッチエディタを使いつつ一部はHTMLで入稿するという代替手段を採用しました。

副次的な効果として、テーブル以外にもリッチエディタでは対応できない要件が発生した場合に「最悪でもHTMLを直書きすれば一応実現はできる」というエスケープハッチ的な役割を果たしています。

ちなみに、microCMSのロードマップに掲載されている「リッチエディタ v2」ではテーブルがサポートされるそうなので、将来的にはこういった代替手段は不要になりそうです。

工夫した点2:入稿者向けのメモ欄

入稿者向けのメモ欄

画面固有の注意事項を記載するためにメモ欄フォームを追加しました。 このメモ欄の文言は実際のアンケート画面からは参照されず、あくまで入稿者向けのメモとしての役割を果たします。

一部の画面では要素に対するスタイルの当て方などに固有の処理が存在するため、見落とさないよう自然と視界に入る場所に設置しています。

②文言データファイル生成スクリプト

次に、文言データファイル生成スクリプトについてです。

このスクリプトはTypeScriptで実装されていて、microCMSから最新の文言を取得し諸々加工したあとに「文言データファイル」を出力します。

大まかに以下のような処理をしています。

  1. microCMSのAPIを呼び出して最新の文言データを取得する
  2. 文言データから必要な情報を抽出する
  3. 抽出した文言データをserializeしてTypeScriptとして正しい文法になるように加工する
  4. 文言データから画像のURLを抽出・ダウンロードして保存する
  5. 文言データ中の画像のURLのドメインを相対パスに置換する
  6. TypeScript文字列をPrettierでフォーマットする
  7. 「文言データファイル」として出力する

また、ここで生成される「文言データファイル」は以下のような構造をしています。

export const cmsQuestionContents = [
  {
    questionNo: '001',
    screenName: '年末調整開始',
    title: '{{氏名}}さんの年末調整を開始します',
    bodyFields: [
      {
        fieldId: 'richEditor',
        html: '<p>SmartHRの年末調整機能を使って、年末調整の申告に必要な書類を作成します。<br>アンケートは....',
      },
    ],
    dialogTitle: '所要時間の目安',
    dialogBodyFields: [
      {
        fieldId: 'richEditor',
        html: '<p>所要時間の目安は以下のとおりです。<br>生命保険等 : 加入なし、....',
      },
    ],
    buttons: [
      {
        fieldId: 'questionButton',
        name: '開始',
      },
    ],
  },
  {
    questionNo: '002',
    // ...(略)
  },
  // ...(略)
] as const

この「文言データファイル」はプロダクトのソースコードと同様にリポジトリで管理されていて、本番環境に反映するためにはPRを作ってマージする必要があります。

従来通りのフローを踏襲することで以下のメリットが得られます。

  • プロダクトの本番環境へのリリースフローに影響がない
  • GitHubのPRで文言の変更差分の確認が容易
    • 必ずコードレビューを通すため予期しない変更が本番環境に反映されてしまうことを防げる
  • 文言をリポジトリ管理するためtextlintによるチェックが機能する
  • 本番環境からmicroCMSへのアクセスが発生しないためインフラ面の影響がない

続いて、「文言データファイル生成スクリプト」に関して工夫した点をピックアップします。

工夫した点1:画像をダウンロードする

microCMSに画像を入稿した場合、当然microCMSのサーバー上に画像が置かれます。

この状態だと本番環境で画像を表示する際にmicroCMSのサーバー(imgix)へアクセスが発生するため、あとからmicroCMSの画像に変更を加えた場合は本番環境に影響が出てしまいます。

この問題を防ぐため、画像も文言と同様にリポジトリで管理するようにしました。

若干力技ですが以下のような流れで実現しています。

  1. ローカルに保存されているすべてのアンケート画面の画像を削除する
  2. microCMSから取得した文言に含まれている画像のURLをすべて抽出し、ダウンロードして保存する

PRでは画像の差分を視覚的に確認できるため便利です。

PRで画像の差分を視覚的に確認している様子

工夫した点2:文言データファイルをTypeScriptファイルとして出力する

プロダクトはTypeScriptで実装されているため、型の恩恵にあずかるために「文言データファイル」もTypeScriptファイルとして出力するようにしました。

const transformToTypeScript = (contents: ReturnType<typeof extractQuestionContents>) =>
  [
    `// Last updated at ${dayjs().format('YYYY/MM/DD HH:mm')}`,
    '',
    '// このファイルはスクリプトによって自動生成されています。',
    '// 詳細は、script/microcms/README.mdを参照してください。',
    '',
    `export const cmsQuestionContents = ${JSON.stringify(contents, null, 2)} as const`,
  ].join('\n')

これは結構素朴な実装で実現されていて、文言データ(JSON)をJSON.stringifyでserializeして文字列結合でTypeScriptの文法に合わせ込んでいます。

また、このタイミングでファイルを直接変更してしまわないようにコメントを追加したり、何かの役に立ちそうなタイムスタンプを追加したり、constアサーションを付与しています。

工夫した点3:PrettierのAPIを呼び出してフォーマットする

最後に、「文言データファイル」にフォーマット上の差分がでないようPrettierでフォーマットするようにしています。 「文言データファイル生成スクリプト」の最終段でPrettierのAPIを呼び出しています。

const PRETTIER_CONFIG_PATH = `${ROOT}/.prettierrc`

// NOTE: リポジトリで管理するため、変更の差分が見やすくなるようにPrettierでフォーマットする。
const prettyTypeScript = await resolveConfig(PRETTIER_CONFIG_PATH).then(options =>
  format(imageUrlReplacedTypeScript, options ?? {}),
)

フォーマットのルールを統一するために、プロダクトの参照している.prettierrcを直接読み込んでいる点がポイントです。

普段Prettierをprogrammaticallyに実行することってなかなかないと思うんですが、今回の用途ではうまくハマって期待通りに動作しました。

③アンケート画面のコンポーネント

続いて、生成された「文言データファイル」を参照してアンケート画面に文言を表示する工程です。

個性豊かなアンケート約100画面を共通の仕組みに置き換えていく作業を実施しています。 その数もさることながら「個性豊か」をどう共通の仕組みで実現するか、頭を悩ませました。

工夫した点を以下にピックアップします。

工夫した点1:文言データはconstアサーションを利用して型チェックを厳格化する

先の「文言データファイルをTypeScriptファイルとして出力する」でもサラッと触れましたが、文言データであるcmsQuestionContentsas constを付与してliteral typesのままexportするようにしています。

これによって、以下の大きなメリットが得られます。

  • microCMS側で画面を削除したり、ボタンを減らしたりするなどの破壊的変更によって、プロダクトコードがnull参照することになってもコンパイルエラーとして検出できる
  • 個々のアンケート画面に対応するコンポーネントのソースコードを参照する際に推論が効いているため、実際にアンケート画面に表示される文言がわかる

VSCodeで文言データが推論によって特定できている様子

実際にpropsで渡ってくる具体的な文言が参照できるため、開発体験の向上に繋がります。

工夫した点2:HTMLをReactコンポーネントに置換する

microCMSのリッチエディタで入力した文言はHTML文字列として得られますが、これをそのまま表示する(dangerouslysetinnerhtmlに流し込む)のではなく、一部の要素を特定のReactコンポーネントに置き換えてレンダリングしています。

例えば、SmartHRでは下図のように、外部リンクの場合は末尾に外部リンクであることを表すアイコンを付与しています。

SmartHRの外部リンクの表示

この要件を実現するために、HTMLのa要素を特定のReactコンポーネントに置き換える実装をしています。 この実装にあたってはreact-html-parserというライブラリを利用しました。

かなり端折りますが、以下のようなイメージで実現できます。 正確な使い方は公式ドキュメントをご確認ください。

import parse, { domToReact } from 'html-react-parser'

const createReplacer = node => {
  switch (node.name) {
    case 'a':
      // 外部リンクを表現するReactコンポーネント
      return <ExternalLink href={node.attribs.href}>{domToReact(node.children)}</ExternalLink>
  }
}

工夫した点3:ユーザー固有の情報を表現する

アンケート画面の文言は説明をわかりやすくするために、以下のように「会社名」や「氏名」といったユーザー固有の情報を含む場合があります。

アンケート画面の見出しに「株式会社スマート」という会社名が含まれている様子

このような情報を {{定数名}} という記法で表現できるようにしています。

例えば、上図のタイトルにあたる文言は以下のように登録されており、Reactコンポーネント側で定数に対応したデータと置き換えてレンダリングしています。

今年({{今年(西暦)}}年)1月~12月に、{{会社名}}以外から給与収入はありますか?

ちなみに、{{今年(西暦)}}はユーザー固有の情報ではありませんが、こういった年を表す表現は翌年のアンケート画面を実装する際にインクリメントする手間を省けるため定数化しています。

④文言プレビュー画面(Storybook)

そして、最後にmicroCMSで確定・下書きした文言をStorybookでプレビューする工程です。

microCMSの画面プレビューボタン

microCMSの入稿画面の「画面プレビュー」ボタンを押すと、別ウィンドウでStorybookが開きます。

Storybookでアンケート画面を表示している様子

入稿画面に対応するアンケート画面が表示され、microCMSで確定・下書き保存した文言を動的に取得して反映します。

また、Storybookで表示されるアンケート画面はプロダクトの実装と同一のコンポーネントを利用していて、以下のように動作を切り替えています。

  • プロダクトの実装では「文言データファイルから読み込んだ文言データ」をpropsに渡す
  • Storybookの実装では「microCMSのAPIを呼び出して取得した文言データ」をpropsに渡す

文言プレビュー画面に関して、工夫した点を以下にピックアップします。

工夫した点1:プレビューボタンの遷移

microCMSでは「画面プレビュー」ボタンを押したときに開くページのURLを指定できます。

このURLにはCONTENT_IDDRAFT_KEYというデータを含められます。

  • CONTENT_IDは個別のアンケート画面に紐づくコンテンツIDを表します
  • DRAFT_KEYは個別のアンケート画面の下書きに紐づくdraftKeyを表します

コンテンツIDはデフォルトでランダムな文字列が設定されますが、編集可能で任意の文字列が設定できます。

ここではアンケート画面の番号と同等の文字列をコンテンツIDに設定することで、CONTENT_IDを参照してアンケート画面の番号を指定できるようになります。

「画面プレビュー」ボタンを押したときに開くページのURLを以下のように設定しています。

https://hogehoge-storybook.netlify.app/?path=/story/hogehoge--foofoo-{CONTENT_ID}&knob-draftKey={DRAFT_KEY}

CONTENT_IDはアンケート画面の番号であるため、入稿画面から紐づくアンケート画面を直接開けます。

また、下書きがある場合はDRAFT_KEYに文字列が指定されます。Storybookの実装では、DRAFT_KEYに指定がある場合は、microCMSで下書き保存した文言を優先して取得して表示します。

課題は解決できたの?

以下2つの課題を続く方法で解決しようとしてきました。

  • 課題1:アンケート画面をイメージしながら文言を検討できていなかった
    • ➔ microCMSで編集した文言を実際の画面で確認しながら文言検討を可能にする
  • 課題2:アンケート画面への文言適用に工数がかかった
    • ➔ microCMSで確定した文言は「文言データファイル更新スクリプト」を実行する

課題1の文言検討の工程についてPdMより「明らかに文言ギメの精度が増しました!」というフィードバックが得られました!

hiroponchack「明らかに文言ギメの精度が増しました」

課題2の文言の適用工程においても、主観的ではありますがスムーズに文言の適用が完了できたと感じます。

概ね狙い通り課題を解決できたと考えています!

おわりに

以上「SmartHRのペーパーレス年末調整」のアンケート画面の文言をmicroCMSで管理して、Storybookでプレビューできるようにした話をご紹介しました。

年末調整機能はその性質上、集中的に「利用されるシーズン」と「利用されないシーズン」がはっきり分かれていて「利用されるシーズン」中は変更が難しいプロダクトです。

そのため、フィードバックサイクルを回した改善の積み重ねが難しい事情はありますが、今回の取り組みのように課題に向き合い解決を図ることでユーザーの価値向上につながる施策を講じていければと思っています。

というわけで!!

恒例のやつですが、今回の取り組みのようにSmartHRはさまざまなプロダクトの課題に一緒に向き合っていくエンジニアを募集しています!!!

SmartHRのエンジニア採用サイトもありますので、よければぜひ覗いてみてください!

hello-world.smarthr.co.jp

謝辞

本機能の実装にあたっては、ZOZOさんとKRAYさんの技術記事を大いに参考にさせていただきました!

techblog.zozo.com kray.jp

近しい事例が少なく技術的な不確実性が高い状況で、商用実績を伴うソリューションの紹介はとても頼もしく参考になりました。ありがとうございました!