every Tech Blog

株式会社エブリーのTech Blogです。

Next.js + useForm/zod で楽をする管理サイト作り

f:id:siukaido:20220325141415p:plain

こんにちは。TIMELINE開発部の齊藤です。好きなエディタはEmacsです。社内の一部エンジニアからは珍獣扱いされてますが、Emacsは最強のエディタなので20年近く愛用しています1

さて、皆様は日頃のサービス運用に、社内向けの管理サイトなどを作っているかと思われますが、弊社でもご多分に漏れず管理サイトを用意して、日々の運用を行なっております。

この管理サイトの出来不出来によっては、運用コストも大きく変わったりするので、案外重要なものだったりするのですが、作るのは正直めんどくさいです。

ユーザさんにお見せするサイトと異なり、MAUは一桁ぐらいですし、いいものを作っても誰かに誉められることも少ないので、正直めんどくさいです(大事なことなのでry

なので、めんどうなことは少しでも楽をしつつ、それでいて運用事故/コストを少しでも減らせられるようにがんばっていたりします。

序文

私が所属しているTIMELINE開発部では、「au payマーケット」アプリで提供している「ライブTV」の運用/開発などを主に担当しています。

コロナ禍でライブコマースがEC市場で再燃。複数のプラットフォームで配信できる『TIMELINE』ならではの強みとは

元々、弊社ではCHECKというライブコマースサービスを運用していました。

ライブTVはその時の資産を利用しており、管理サイトなども数年前のリリース時に私が作ったのがそのまま利用されていたりします。

当時は何も考えなしに作ってたのですが、TIMELINEに合流して改めて見ると、あちらこちらでめんどくせーってところが散見されたため、そこらへんを修正していったあれやこれやを紹介させていただきます。

ビルドの自前管理がめんどくさい

webpackを用いてビルドしているのですが、元々はそのコンフィグファイルを自前管理していました。

当時は「 grunt に比べてなんて楽なんだろう...」と感動して使っていたのですが、久々に昔書いたコンフィグファイルを眺めていると、「こんなん管理するとかムリー!!」っていう感情に溢れてしまうほどめんどくさいものでした。

そこで、MAMADAYSで採用していた Next.js へ全面的に載せ替えることにしました。

Next.js の利点として SSR/SSGによるものがよく挙げられますが、管理サイトのフレームワークとして採用する利点はそこではなく、「ビルド管理をNext.js に任せられる。また、それに伴う各種恩恵(後述)に与れる」ところです。

もちろん載せ替える手間はあります。しかし、今後発生するであろうめんどくささと比べたら微々たるものですし、元々が React で書いていたので最低限の修正だけで載せ替えることができました。

これにより next build するだけでビルドしてくれます。コンフィグを自前管理しなくてもいい2だなんて、神!

さらには、開発時のホットリロードも独自で書いてたのですが、それすらも next dev するだけでやってくれます。至れり尽くせりすぎる!

開発効率は格段に上がり、細かい修正とかへのストレスもだいぶ軽減されました。

ルーティングがめんどくさい

元々は react-router-dom を使って、下記のような方法でルーティングを行っていました。

import { HashRouter, Switch, Route } from 'react-router-dom';
import Index from './containers/Index';
import Hoge from './containers/Hoge';

const routes = [
{
    path: '/',
    exact: true,
    component: Index,
  },
  {
    path: '/hoges,
    exact: true,
    component: Hoge,
  },
];

export default class App extends Component {
  render() {
    return (
      <HashRouter>
        <Switch>
          {routes.map(({ path, exact, component }) => (
            <Route key={path} path={path} exact={exact} component={component} />
          ))}
          <Route render={(props) => <NoMatch {...props} /> } />
        </Switch>
      </HashRouter>
    );
  }
}

教科書どおりな react-router-dom の使い方ですし、NestedRouteing などの考えはものすごく良いのですが、管理サイトでそこまでやるメリットが思いつきませんでした。

むしろ、これだとページを追加するたびに routes を修正しないといけないし、path とファイル名が一致しないケースがあったりと、めんどくささが満載です。

ですが、Next.js に載せ替えたため、pages 以下にファイルを置くだけで、ルーティングをよしなにしてくれます。

だいぶめんどくささが軽減されましたし、path とファイル名が一致してるだけで、ものすごく気分が楽になります。

コンポーネント名を覚えるのがめんどくさい

JavaScript は型がなく厳密な書き方をせずとも動くので楽っちゃ楽なのですが、型がないがゆえに補完をうまくしてくれません。

そのため、ファイル名や位置とかをある程度、脳内メモリに格納した上で開発しないといけないわけですが、私も本厄を迎える歳となってしまい細かい記憶力に心配がでてくるようになってきました。

TypeScript であれば、最強のエディタであるEmacsがよしなに補完してくれる3し、import とかも気にせずに済むんですが...。

という悩みも、Next.js に載せ替えたことにより、tsconfig.json を置くだけでTypeScript化は完了です。

無事に加齢による衰えをシステムでフォローしてくれようになり、高齢化対策も万全です!

デザインがめんどくさい

cssが苦手です。書きたくないです。レスポンシブデザインとかになると、めんどくささに溢れてます。

なので、管理サイトは基本的に Bootstrap を使ってデザインしてます。

ただそれでもクラス名を覚えるのがめんどくさいので、 ReactBootstrap を使ってます。

もちろんReactBootstrapのコンポーネント名とかもEmacsが補完してくれます4

また、どうしてもcssを書かなきゃいけない個所であっても、Next.js が Sass に対応してくれるので、生でcssを書くよりも格段に楽ができます。

バリデーションがめんどくさい

元々は下記のように各項目ごとにバリデーションを書いていました。

const [form, setForm] = useState({ name: '', state: 0 });

const isValidName = () => {
  if (form.name.length < 5) {
    return false;
  }
  if (form.name.length > 10) {
    return false;
  }
  return true;
};

const isValidState = () => {
  if ([0, 1].includes(form.state)) {
    return false;
  }
  return true;
};

const onSubmit = (e) => {
  e.preventDefault();

  if (!isValidName() || !isValidState()) {
    // エラーハンドリング
    return;
  }
  // 正常処理
};

まー、めんどくさい。しかも、漏れも生じまくる。入稿してくれる方の職人芸もありつつの、事故回避でした。

そこで zod を用いることにしました。 zodに関しては uttkさんのこの記事 が秀逸です。めちゃくちゃ参考にしました。

上記の例をzodを使って書き直すと

const schema = z.object({
  name: z.string().min(5).max(10),
  state: z.union([z.literal(0), z.literal(1)]),
});

const [form, setForm] = useState({ name: '', state: 0 });

const onSubmit = (e) => {
  e.preventDefault();

  try {
    f = schema.parse(form);
  } catch (e) {
    // エラーハンドリング
    return;
  }
  // 正常処理
};

意識するのはzodの定義のみで、非常にわかりやすくなりました。

しかも、zodからTypeScriptの型も吐き出せます

type IForm = z.infer<typeof schema>;

// IForm = type {
//   name: string,
//   state: 0 | 1,
// }

なので、一石二鳥!使わない手はないです。

フォームがめんどくさい

運用の大部分を占めるのが入稿作業だと思います。

これも元々はお見せするのも恥ずかしいレベルのオレオレフォームを作っていたのですが、useForm を使うようにしました。

zodと組み合わせるとすげー便利ですし、すっきりさせることができました。

const methods = useForm<IForm>({
  resolver: zodResolver(schema),
  defaultValues: {
    name: '',
    state: 0,
  },
});

const onSubmit: SubmitHanlder<IForm> = (data) => {
  // 正常処理
};

跋文

こんな感じで手を抜けるところは抜いて、それでいて運用コストを少しでも下げられるような改善を日々行っています。

また、手を抜くことにより、理解する箇所を極限まで減らしていくことにもつながるので、普段フロントエンドを書いていないエンジニアでも、運用サイトの更新ができるという面もあったりします。


  1. Emacsは エディタではなくOSである という方もおられますが、ここではエディタとして扱わせていただきます

  2. もちろん細かい設定をいじりたいときは修正する必要がありますが、そうだとしても格段に管理が楽になりました

  3. だいたいのエディタでやってくれます…

  4. だいたいのエディタでやってくれます…