SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

エンジニア採用サイトをリニューアルした話

アイキャッチ

挨拶

こんにちは!私はBOXIL SaaS開発エンジニアのハヤシ(ぱずー)です。

前回、私がスマートキャンプで成長したエピソードを紹介しましたが、今回はエンジニア採用サイトのリニューアルに携わったので、それについて紹介します。

最後まで読んでいただけると嬉しいです!

今回、リニューアルした採用サイトです。こちらも見ていただけると嬉しいです!

https://engineer.smartcamp.co.jp/

想定読者

  • これから採用サイトをリニューアルしたい人
  • エンジニアでスマートキャンプに興味がある人

目次

TL;DR

  • アニメーションの実装経験があまりなかったため、なんもわからん状態だったけど気合いで乗り切った
  • 採用サイトをリニューアルすることでエンジニア組織の方向性が定まった。

なぜ採用サイトをリニューアルしたか

主に以下の理由で採用サイトがリニューアルするモチベーションになったと認識しています。

  • エンジニア採用サイトの更新が2020年で止まっており、単純に情報を更新したい
  • テクノロジーを活用する姿勢を求職者に見せたい

また、リニューアルにあたっての想いを企画担当者からいただきましたので、掲載します。

スマートキャンプはIPOを目指すことになり、そのためにもよりテクノロジーを活用していく必要性があります。
そこで私たちの思いに共感し、それを推進したいと言っていただけるようなエンジニアを広く募りたくリニューアルを開始しました。
これから弊社に必要なのは、AI技術の進歩を始めテクノロジーが劇的に進歩している中で、経験したことのない未知の領域でもおもしろさを感じ飛び込んでいく力がある人だと考えています。
なので採用ページを見ていただいた方に「スマートキャンプがテクノロジーに投資をする意思があること」と「ワクワクやおもしろさを大切にしていること」が伝えたいということでなかなかない趣向にしてほしいとデザインも依頼し、自分たちエンジニアで開発・運用していくことに決めました。

※IPOを目指すことについての想いは以下の記事をご覧ください。

https://note.com/smartcamp_tent/n/nc9ad8887a5af

どう実装したか

さまざまな想いが詰まった採用サイトを実装することになり、実装を担当する自分も技術的チャレンジしながら開発できる!とワクワクしていました。

企画担当者から「斬新なデザインを希望」とのメッセージ通りに、今まで見たことのないようなデザインが上がってきました。

↑一番最初にデザインを見たときのslack上でのやりとり。

複雑なアニメーションだったので、実装しなくて済む方法も模索する道はありましたが、せっかくならばチャレンジしてみようと自力で実装することを決めました。

今まで凝ったアニメーション作成の経験はなかったので、かなりチャレンジングな開発でしたが、その中で何を考えて実装したのかをご紹介します。

開発にあたっての方針

技術選定は以下を念頭において進めていきました。

1. モダンな技術を使う

  • 完成させることだけ目的であれば、HTML/CSS、素のJavaScript(jQuery)などでも実装は可能でしたが、今回は技術的にチャレンジしたいという思いもあり、モダンな技術を用いて実装することに決めました。

2. 独自実装を極力避ける

  • ライブラリを最大限活用して実装する方針にしました。

3. シンプルな構成を目指す

  • 今後、チームで採用サイトを運用していくにあたり、JavaScriptに不慣れなメンバーがコードに触れる可能性があるため、極力わかりやすい技術を採用することを目指しました。

4. 最初から完璧を目指さない

  • 最初から完璧な状態でリリースすることはせず、まずはリリースを先行させて、コードのリファクタは徐々に行なっていく方針にしました。

技術構成

上記を念頭に以下のような技術を使うことにしました。

  • フレームワーク
    • React.js (Next.js v13 + SG)

      理由:今までフロントエンドはVue.jsで書くことが多かった弊社でも、最近のプロジェクトでは、React.jsを使用した実績が増えてきており、知見が溜まっていたため、React.jsを採用しました。

  • TypesScript

    理由:言わずもがなですね。

  • 状態管理
    • jotai

      理由: 特に理由はないですが、状態管理にはjotaiを採用しました。RecoilとAPIが似ているのですが、更にシンプルにしたような感じで特に不満なく使えています。

  • スタイリング

    • CSS Modules + scss

      理由: 詳しく書きたいのでこの項で説明します。

  • アニメーション
    • framer-motion
    • イイ感じのスクロールはframer-motionをラップしたパッケージであるscroller-motionを使用。

      理由: 最初はcssだけで作ろうと思ったのですが、全く自信がなかったのもあり、良い感じにアニメーションを作れるライブラリを探していたところ、framer-motionを見つけ採用しました。 例えば、アニメーションを使用して要素を表示・非表示したい場合は以下のようにすることで簡単に実現できます。

    <motion.div
      initial={{ opacity: 0 }} // 初期表示は要素は非表示
      animate={{ opacity: 1 }} // アニメーションすると要素が表示される
      exit={{ opacity: 0 }} //要素が非表示になるときに要素を見えなくする
    />
  • ホスティング
    • Cloudflare Pages
      • 弊社のドメイン管理はCloudflareを使用しており、親和性があると感じたため。
      • また、Node.jsv18は執筆時点では未対応だったため、v17.9.1で動かしてます。
      • GitHubと簡単に連携できて、Preview環境をすぐに作れるので成果物をスムーズにチェックしてもらえたのでかなり良かったです。

スタイリングにCSS in JSではなくCSS Modulesを使用した理由

CSS Modulesを採用した主な理由は以下になります。

  • チームで採用サイトを更新していくことを考えると、JavaScriptを詳しく知らなくてもスタイリングできるほうがよさそう。
  • 結局、CSS in JSは、CSSで実現できる以上のことはできないため、あえて採用するモチベーションがないと個人的に感じた。
  • Tailwind CSSも検討したが、独自に書き方を覚える必要があるので、今回は避けた。

また、Next.jsはCSS Modulesをビルトインサポートしていて、特別な設定が不要なため、そのこともCSS Modulesを選択した理由となりました。

CSS Modulesについては、Next.js v13でも継続してサポートしていることから、意外と今後も使われ続けるのではないかと思っています。

実装で工夫したところ

作業しやすいディレクトリ構成にする

フロントエンド開発では一般的にAtomic Designなどを用いてディレクトリを構成することが多いと思いますが、コンポーネントを分類する手間が個人的に負担だと感じていたため、今回はAtomic Designの採用を見送りました。

そこで、ページ固有のものか、共通のものかを明確に分類できるようなディレクトリ構造を採用することに決定しました。

以下のような構成にして、作業しやすい構成を目指しました。

app/
├── assets/ # 画像、フォント、アイコンなどのアセットを格納する
├── components/ # 共通で使用するコンポーネントを格納する
├── constants/ # 全ページ共通で使用する定数を格納する
├── features/ # ページや特定の機能に関するコンポーネントやhooksを格納する
│   └── [page名]
│        ├── components/ # 特定のページでのみ使うコンポーネントを格納する
│        ├── hooks/ # 特定のページでのみ使うhooksを格納する
│        ├── constants/ # 特定のページ単位でのみ使う定数を格納する
│        ├── index.tsx # エントリーポイント
│        └── style.module.scss # index.tsxで使用するスタイルファイル
├── hooks/ # 全ページ共通で使用するhooksを格納する
├── pages/ # 各ページを表すcomponentを格納する
├── utils/ # すべてのユーティリティ関数を格納する
└── styles/ # すべてのCSSを格納する

ポイントとしては、featuresというディレクトリを作ってページごとにコンポーネントやhooksを配置したことです。

こうすることで、どこでそのコンポーネントが使われるかが明確になり、コードの見通しが良くなることを期待しました。

実際にこの構成にしてみて、以下のようなメリットがありました。

メリット

  • コンポーネントを分ける負担の軽減
    • Atomic DesignであったMoleculesやOrganismsとかの括りがなくなったので、確実にコンポーネントを分類する際の工数は減ったように思えます。
  • フォルダを行ったり来たりすることが少なくなった。
    • ページ単位でcomponentやhooksがまとまってて参照しやすくなり「あれこのhooksどこだっけな」みたいなのがなくなりました。

デメリット

  • コンポーネントを分ける負担は相変わらず存在する
    • Atomic Designよりは負担は減ったのですが、それが共通・ページ固有のコンポーネントなのかを考えながら作業しないといけないのは変わらず存在します。
  • ディレクトリ一式(componentsとかhooksとか)を都度作らないといけない

また、featuresをどの粒度で切るかを考えるのはかなり重要だと思いました。

今回は採用サイト作成だったので、ページ単位でfeaturesディレクトリを作成しましたが、業務アプリケーションの開発では、同じようなコンポーネントが複数のページで使用される可能性があります。 その場合はページ単位ではなく、ドメイン単位(ユーザーとか)でのfeaturesディレクトリを分割した方がより適切な構成になると思っています。

コンポーネントのテンプレートを作成するスクリプトを組んで開発効率を上げる

上記のディレクトリ構成に従って、コンポーネントを量産していく際に、対応するindex.tsxとstyle.module.scssを自動生成できるスクリプトを作成することで、効率化を図りました。

import fs from 'fs';

function capitalizeFirstLetter(string: string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

function lowerCaseFirstLetter(string: string) {
  return string.charAt(0).toLowerCase() + string.slice(1);
}

const component = (name: string) => `import style from './style.module.scss';
export const ${capitalizeFirstLetter(name)} = () => {
  return (<div className={style.${lowerCaseFirstLetter(name)}}></div>)
}
`;

const style = (name: string) => `.${lowerCaseFirstLetter(name)} {
}
`;

const handler = () => {
  const filePath = process.argv[2]
  const componentPath = `${process.cwd()}/src/${filePath}`;
  const fileName = filePath.split("/").slice(-1)[0]
  // フォルダがない場合は再帰的に作成する
  if (!fs.existsSync(componentPath)) {
    fs.mkdirSync(componentPath, { recursive: true });
  }
  fs.writeFileSync(`${componentPath}/index.tsx`, component(fileName));
  fs.writeFileSync(`${componentPath}/style.module.scss`, style(fileName));
};

handler();
npm run create:component components/Image

npm scriptsに登録して、こんな感じで実行してあげることですぐにコンポーネントの雛形を作れるようになったので、意外と開発体験が上がってよかったです。

next-seoを使ってSEO対策でラクをする

next-seoという素晴らしいライブラリを利用することで、複雑になりがちなSEO周りを簡単に設定できました。 やったことはシンプルで、プロジェクトルートにnext-seo.config.tsを作成し、必要な設定を記述するだけです。

今回は必要最低限の設定に留めましたが、ページ毎に使用する情報を変更するなど、他にももっと細かい設定があるので、詳しくはこちらをチェックしてください。next-seo

next-seo.config.ts

const seoConfig = {
  // メタタイトル
  defaultTitle: "スマートキャンプ株式会社 エンジニア採用",
  // メタディスクリプション
  description:
    "スマートキャンプ株式会社のエンジニア採用ページです。スマートキャンプはエンジニアが活躍して社会の非効率をなくすテックカンパニーを目指しており、そのための取り組みやチーム体制、採用情報などを公開しています。",
  // faviconの設定
  additionalLinkTags: [
    {
      rel: "icon",
      href: "favicon.ico",
    },
  ],
  // OGPの設定
  openGraph: {
    type: "website",
    locale: "ja_JP",
    url: "https://www.example.com/page",
    site_name: "スマートキャンプ株式会社 エンジニア採用",
    description:
      "スマートキャンプ株式会社のエンジニア採用ページです。スマートキャンプはエンジニアが活躍して社会の非効率をなくすテックカンパニーを目指しており、そのための取り組みやチーム体制、採用情報などを公開しています。",
    images: [
      {
        url: "/ogp.png",
        width: 1200,
        height: 630,
        alt: "Og Image Alt",
      },
    ],
  },
};

export default seoConfig;

_app.tsx

import { DefaultSeo } from 'next-seo';


export default function App({ Component, pageProps }: AppProps) {

  const SeoContent = () => {
    return <DefaultSeo {...seoConfig}  />
  }

  return (
    <Provider>
      <SeoContent/>
      <Component {...pageProps} />
    </Provider>
  )
}

headタグ内にAAを置く

エンジニア採用サイトらしいことをやりたいなと思い、headタグ内にアスキーアートを設置することにしました。

これも↓の感じで簡単にできたので、やってみると面白いかもしれません。

const logo = `<!--         .=+:
            -**##+.                  :+*##*=:  :**+      :**+      :.     .******+=  -*********  :+*##*=.       -      +**:      ***. .******+:
          -****####=.               =##*==###- :###=     *##+      *=     .##*===###.:==+##*=== =##*==###.     -*.     +##*     =###. .##*==+##=
        -******######=              ##*   :##+ :####:   +###+     -**.    .##+   -##=   :##+    *##   -##-    .**=     +###=   :####. .##+   +##
      :+*******########-            ###.       :####*  -####+    .***+    .##+   :##=   :##+    *##           =***.    +####: .#####. .##+   +##.
    :+*********##########-          :###*+-.   :##+*#=.##+##+    =****:   .##*---*##.   :##+    *##          .****+    +##+#* +#**##. .##*..:*#*
  .+****###****###########*:          :-+*##*. :##+:##*#*.##+   :******   .###*###=     :##+    *##          +*****:   +##.*#*##:+##. :#######+.
.=*****####****#############+:            -##= :##+ +###:.##+   +**+***-  .##+ .##+     :##+    *##         :***+***   +##.:###= +##. :##+::.
.-+**######****############*=:     .++=   :##+ :##+ .##= .##+  :***.-***  .##+  -##-    :##+    *##   -++:  ***=.***-  *##. +#*  +##. :##+
    ::::::*****#########+-.         *##:.:+##- :##+  :-  .##+  ***=  ***= .##+   *##.   :##+    +##-.:*##: -***. -***  +##. .-.  +##. :##+
          +****#####*=:             .+#####*=  :##+      .##+ -***.  :***..##+   :##+   :##+    .+#####*: .***-   ***= +##.      +##. :##+
           :=+*#*+-                    .::.     ...       ... ....    .... ...    ...    ...       .::.    ...    .... ...       ...   ...
              :-
-->`

const RawHtml = ({ html = "" }) => (
    <script dangerouslySetInnerHTML={{ __html: `</script>${html}<script>` }} />
  );

//_app.tsx内のHeadに入れてあげる
<Head>
  <RawHtml html={logo} />
</Head>

こんな感じで表示されます。

ハマったところ

アニメーションなんもわからん

アニメーションを作った経験がほとんどなかったため、どうやって実装したらいいのか全くわかりませんでした。

そこで以下の二つの方針で実装を進めることにしました。

  • CSSを極力使わない
  • とにかく実装イメージをつける

CSSを極力使わない

CSSでアニメーションの実装を調べていたら時間がかかるので、ライブラリに頼りまくって実装する方向に決めました。

今回は、framer-motionを使用しましたが、CSSをあまり書かなくていいのと、コンポーネントにどうアニメーションするかの設定を書くことができるので、直感的に使うことができたので、作業が非常に捗りました。

とにかく実装イメージをつける

幸いにもアニメーションのイメージ動画は頂いていたので、まずとにかくアニメーションを繰り返し見て、流れを整理してから実装していきました。

以下のような整理をすると、実際にコードに落とし込んだときも、どの部分の実装しているのが明確になり、スムーズに進めることができました。

  1. 画面が表示される
  2. タイトルが落ちる
  3. タイトルと背景がぶつかる
  4. ぶつかった衝撃でタイトルが弾むと同時に背景が傾く
  5. 傾斜によってタイトルが滑る
  6. マウスを上にスクロールすると背景が上昇し、傾斜が逆になり、同時にタイトルが上に吹き飛ぶ
  7. かっこいいメッセージが表示される

特に難しかったのが、複数の要素を同時に動かして連動しているように見せることです。

②では、タイトルをバウンドさせながら傾斜を滑らせる処理がはいるのですが、ここにかなり苦戦しました。(なーんだ簡単じゃんと思われた方は、ぜひ自分の師匠になってください...!)

具体的な例を挙げると、framer-motionにはuseAnimationControlsというhooksが提供されていて、任意のタイミングでアニメーションを発火させることができます。

そのオプションにどれぐらい要素を落とすかを設定する値をハードコードするなど、かなり強引な実装で回避しました。

  titleControl.start({
    x: "calc(50% - 50vw)",
    y: [null, y - animationOptions.bounce, y, y, y], // animationOptionsはディスプレイに応じてハードコードしてしまっている
    rotate: [null, 0, animationOptions.rotate],
    transition: {
      duration: 1.4,
      delay: 3,
      ease: [1, 1.6, 0.28, 0.71],
      x: { delay: 3, duration: 0.7 },
      rotate: {
        delay: 2.65,
        duration: 1,
      },
    },
  });

本当は、どのディスプレイでも同じ表示になるようにしたかったのですが、複雑な計算が必要になりそう & 実装期間との兼ね合いもあり、今回は見送りました。(すぐ分かる方いたら教えて欲しいです...!お待ちしています。)

最下部にスクロールするときに画面がチラついてしまう

特徴としてトップページは最下部から上に向かってスクロールするような仕組みになっています。

そのため、画面ロード時にJavaScriptを使用して最下部まで移動する必要があったのですが、どうしても一瞬移動前のパーツが見えてしまう問題がありました。

そこで、ローディング画面を2秒間見せ、その間に最下部に移動することで違和感を与えないように変更しました。

開発してみての感想

複雑なアニメーションを実装したのは初めてだったので、試行錯誤しながらどう実装していくかを考えるのは非常に勉強になり、純粋に楽しかったです。一部強引な実装をしてしまった部分があったので、そこは反省しつつも、全体的には技術的なチャレンジもできたので、良いものが出来上がったのかなと思っています。

ただ、作って終わりではないので、リファクタを継続しながら、もっともっとエンジニアドリブンで開発していける組織を目指していきたいなと開発してみて思いました。

ちなみに↓のセクションでは、表示するメッセージを各メンバーに考えてもらったのですが、皆が向かっていきたい方向やビジョンが垣間見えて凄くお気に入りです。

最後に

ここまで読んでくださってありがとうございました!

今回は、更新が止まっていた弊社の採用サイトをチームで更新していくことを考えつつリニューアルしました。 複雑なアニメーションを実現が特に難しかったですが、試行錯誤した結果、斬新なデザインを実現できました。

そして、採用サイトをリニューアルする意義とは何かを考えてみると、あらためて会社の方向性、会社の制度、伝えたいメッセージを考えるきっかけになって、組織の方向性がより明確に定まることなのかなと思いました。

弊社はMISSONに「テクノロジーで社会の非効率を無くす」を掲げていますが、 そういった意味合いでもエンジニアが中心となって採用サイトをリニューアルするという取り組みに携われたことは非常に良かったと個人的には思いました!

これからもその想いの実現に向けて頑張っていきたいと思います!

今後は以下のアップデートを予定しているので、引き続きチェックしていただけると嬉しいです。

  • 新たに対談ページを追加
  • アニメーションを追加してもっとリッチにする
  • 新メンバーの画像とメッセージを追加する