Engineering Mondayを開催してみた話

こんにちは。カンカクのiOSエンジニアの@redryeryeです。

最近エンジニアリングチームでは「Engineering Monday」と題して、勉強したい技術トピックに関しての社内LT会を毎週実施しています。
今回はこの取り組みに至った経緯やその内容についてご紹介します。

LT会をやろう!に至るまで

モバイルデベロッパーにとって少し前はWWDC!Google IO!とイベントが盛り沢山でしたね。

この時アップデートされたMaterial Design 3への理解をチームで深めたい!ということで@haru067さん主導で週一回Material Design 3勉強会が開催されることになりました。

WWDCの勉強会も同じく開催しようと考えた時に、このペースで勉強会を新しく生やしていくと勉強会少し多すぎないか?という懸念がありました。

そこでドクターズプライムさんの社内勉強会の記事を発見し、LT会という形を提案しました。

結果、トピックは自由でLT会を週一回開催することで、他の技術に関してアウトプットしたいときに気軽に話せる場を作ることにしてみました。
フォーカスしたい技術トピックやハンズオンも交えてチームと共有したい場合は勉強会を別途開催する運用にしています。

進め方

Engineering Mondayは主に以下の4つの点に沿って進めています。

  1. 一人約15分(人数が少ない場合はそれ以上可)
  2. 発表者どなたでも可、内容はプロダクト開発に関するものなら自由
  3. 発表者視点で面白いポイントを紹介できると尚良い
  4. 発表資料は「頑張らない」。発表できるなら資料無くてもOK

基本的にドクターズプライムさんが行っている、登壇のハードルを下げて継続的に勉強会を続けていく運用を参考にさせていただいています。
社内勉強会を継続的に開催してく上で役立つtipsが紹介されているのでぜひこちらの記事も見てみてください。

Engineering Mondayと呼んでいるものの、エンジニアのみならずデザイナや他部署からもメンバーが勉強会に参加してます。
そんな中で、馴染みのない技術の話を聞く時に興味が薄れてしまうという声があがりました。これを解決するために発表者視点で面白いポイントを説明してもらうことで、馴染みのない参加者にとっても分かりやすい発表をするよう心がけています。

運用してみて

運用し始めてもうすぐ2ヶ月が経ちますが、決まったメンバーが登壇していた当初に比べ、今は登壇者が徐々に増えていて登壇のハードルが低いまま継続できていると思います。

今までにあった発表をいくつかご紹介します。

ハードルが下がったといえど、毎週登壇内容を考えるのは難しい。
これを何とかするハックとして、ウィークリーニュースレターから毎週話題を取り上げて紹介する方法があります。上記のiOSのウィークリーレターに加え、AndroidDagashiJSer.infoからトピックを取り上げたりしています。

また、普段使用している開発ツールを見返してみておすすめのモノを紹介するLTも好評です。

おわりに

今回はLT会を定期的に開催することでアウトプットの機会を増やした事例について紹介しました。

カンカクでは、こうしてわいわいとアウトプット・インプットをしながら一緒に開発していく仲間を募集しています。
興味のある方はぜひ以下のリンクよりご応募お待ちしております。

親に依存しないComposable functionの設計

カンカクでAndroidエンジニアをやっている @haru067です。好きな焼肉は牛タンです。

今回はJetpack Composeの設計の話をします。

よくある実装

アプリ開発で非常によくあるパターンとして、リスト画面があり、タップすると詳細画面に遷移する、というのがあります。例えば、記事の一覧があり、タップすると記事の詳細に遷移するような画面を作るとしましょう。

こういうの

記事に対応するUIステートをArticleとし、

data class Article(
    val id: String,
    val title: String,
)

記事一覧のUIをArticleList、一覧の子要素をArticleItemとします。

@Composable
fun ArticleItem(
    article: Article,
    onClick: ..., // ここをどうするか
    modifier: Modifier = Modifier,
) {
    Column(modifier.clickable { ... }) {
        Text(text = article.title)
    }
}

@Composable
fun ArticleList(
    articles: List<Article>,
    navigateToArticleDetail: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    LazyColumn(modifier) {
        items(articles) { article ->
            ArticleItem(
                article = article,
                onClick = ...
            )
        }
    }
}

navigateToArticleDetailは記事IDを引数にとり、記事の詳細に遷移する関数とします。 このとき、タップ時のイベントであるonClickの型はどうすべきでしょうか?

深く考えなければArticleListの引数navigateToArticleDetailをそのまま渡してしまうのが楽そうです。これを実装Aとします。

// 実装A

@Composable
fun ArticleItem(
    article: Article,
    onClick: (String) -> Unit, // String: 記事ID
    modifier: Modifier = Modifier,
) {
    Column(modifier.clickable { onClick(article.id) }) { // ここでIDを指定
        Text(text = article.title)
    }
}

@Composable
fun ArticleList(
    articles: List<Article>,
    navigateToArticleDetail: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    LazyColumn(modifier) {
        items(articles) { article ->
            ArticleItem(
                article = article,
                onClick = navigateToArticleDetail, // そのまま渡す
            )
        }
    }
}

一方、記事のIDを外部から注入する形にして、()-> Unitで実装することもできます。これを実装Bとします。

// 実装B

@Composable
fun ArticleItem(
    article: Article,
    onClick: () -> Unit, // 引数にあったStringを削除
    modifier: Modifier = Modifier,
) {
    Column(modifier.clickable { onClick() }) { // ここも削除
        Text(text = article.title)
    }
}

@Composable
fun ArticleList(
    articles: List<Article>,
    navigateToArticleDetail: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    LazyColumn(modifier) {
        items(articles) { article ->
            ArticleItem(
                article = article,
                onClick = { navigateToArticleDetail(article.id) }, // 注入する
            )
        }
    }
}

さて、実装Aと実装B、どちらで実装すべきでしょうか?

依存性を考える

両者の違いの一つに依存性があります。 ArticleItem単体の実装を考えたとき、実装AはArticleListに依存しているが、実装Bは依存していないと言えます。

🐦 < つまり、どういうこと?

実装Aでは、ArticleItem内で、記事のIDをonClickに渡していました。

Column(modifier.clickable { onClick(article.id) }) { …

記事のIDを渡す必要が何故あるのか考えてみると、「リスト内のどの要素が押されたのか区別したい」からです。「リスト内の」という言葉にあるように、これはArticleListの実装の都合で、ArticleItem単体で見たときには関係のない話です。従ってArticleItemはこれに依存すべきではないでしょう。

例えば、タップしたときに画面遷移するのではなく、記事名を点滅させたくなった場合を考えてみます。実装AではArticleListArticleItemの両方に変更が必要になりますが、実装BではArticleListを変更するだけで対応できるはずです。

// 実装B

@Composable
fun ArticleList(
    articles: List<Article>,
-   navigateToArticleDetail: (String) -> Unit, // String: 記事ID
+   blink: (String) -> Unit, // String: 記事タイトル
    modifier: Modifier = Modifier,
) {
    LazyColumn(modifier) {
        items(articles) { article ->
            ArticleItem(
                article = artice,
-               onClick = { navigateToArticleDetail(article.id) },
+               onClick = { blink(article.title) },
            )
        }
    }
}

このように、親に依存しないよう設計することで、変更に強く、再利用の高いComposable functionを作ることができます。従って、依存性の観点では実装Bが望ましい、という結論になります。

いかがでしたか?

些細な部分ではありますが、割と気にせず実装している人も多いのではないでしょうか。 カンカクでは、こうした些細な部分に気を使って実装できるAndroidエンジニアを募集しています。 興味のある方はご連絡ください。

Modifierを忘れない

カンカクでAndroidエンジニアをやっている @haru067です。好きな鶏肉は、ささみです。

今回はJetpack ComposeのModifierの話です。

仮引数のModifier

一般に、@Composableの関数にはmodifier: Modifierの仮引数を追加するのが良いとされています。

⛔️ Don't

@Composable
private fun HeaderText(
    text: String  // Modifierがない
) {
    Text(
        text = text,
        ...
        modifier = Modifier.fillMaxWidth()
    )
}

@Composable
fun HeaderGroup(...) {
   Box(...) {
       HeaderText(
           text = "some text"
       )
   }
}

✅ Do

@Composable
private fun HeaderText(
    text: String,
    modifier: Modifier = Modifier   // Modifierがある
) {
    Text(
        text = text,
        ...
        modifier = modifier
    )
}

@Composable
fun HeaderGroup(...) {
   Box(...) {
       HeaderText(
           text = "some text",
           modifier = Modifier.fillMaxWidth()
       )
   }
}

これは、Jetpack ComposeのAPIガイドラインにも書かれています。
https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#elements-accept-and-respect-a-modifier-parameter

なぜ?の部分に関しては、こちらのブログ記事が詳しいです。
chris.banes.dev

フレームワーク・ライブラリの開発においては、インターフェイスをきちっと定める必要性が高い一方、アプリ開発者では後から足せば何とかなる分「頭ではわかってるけど、面倒くさい」「付け忘れた」となりがちです。事実、上に挙げたAPIガイドラインでもフレームワーク・ライブラリ開発ではMUSTとなっていますが、アプリ開発ではSHOULDとされています。

Jetpack Compose framework development and Library development MUST follow all guidelines in this section.

Jetpack Compose app development SHOULD follow all guidelines in this section.

Live Templateの活用

では、どうしたら面倒と感じずに書けるか?

考えた結果、行き着いたのはLive Templateでした。Jetpack Compose(Android Studio)には、いくつかのLive Templateがデフォルトで用意されています。
https://developer.android.com/jetpack/compose/tooling#live-templates

例えば、compと入力してTabキーを押すと、次のように定型文が挿入されます。

これを編集して、modifier: Modifierも挿入されるようにします。

この設定画面を次のように変更します。

// Before
@androidx.compose.runtime.Composable
fun $NAME$() {
$END$
}

// After
@androidx.compose.runtime.Composable
fun $NAME$(modifier: androidx.compose.ui.Modifier = androidx.compose.ui.Modifier) {
$END$
}

結果

良い感じになりました。完。

その他

既存AndroidアプリでのGraphQL/Apollo Kotlinの部分的導入

こんにちは、カンカクでAndroidエンジニアをやっている @haru067です。最近好きな寿司は、鯖です。

前回の記事では、COFFEE Appでの新規機能の開発にあたり、Jetpack Composeを活用した話をしました。 ここでは特に言及しなかったのですが、Jetpack Composeの他にもう一つ技術的な試みがありました。それがGraphQLの導入です。

そこで今回は既存のAndroidアプリに対して、新規機能の開発でGraphQLを導入した話をしたいと思います。

GraphQLとは

GraphQLは、Web API向けのクエリ言語(及びそのランタイム)です。 よく対比されるのはREST APIですが、GraphQLには、

  • スキーマに基づいてサーバー/クライアント間で一貫した型が利用でき
  • 複数のリクエストを送ることなく一度のリクエストで
  • 必要なデータのみを過不足無く

取得できる、といったメリットがあります。

AndroidにおけるGraphQLのクライアントライブラリで有名なものは、 Apollo Kotlin(以降、Apollo)でしょうか。2021年末にはv3.0.0がリリースされ、Kotlin Nativeのサポートやキャッシュの改善など、様々な機能が追加されました。COFFEE AppでもApolloを使用しており、この記事でもApolloの利用を前提として話を進めます。

導入の経緯

我々の場合では、どちらかと言えばサーバー側の嬉しさが先にあって、クライアント側のエンジニアでも検証した結果、良さそうだね、という流れでした。クライアント側の都合に合わせたAPIを設計する際には、サーバー側のエンジニアもその都合を理解する手間が発生しがちですが、GraphQLでは必要なものを予め用意しておけば、あとはクライアント側のエンジニアで自由に取得してね、という形にできるため、サーバー側のエンジニアの負担が減ると期待していました。

例えば、COFFEE Appでは店舗の情報を表すデータとして、ShopShopSummaryOrderShop、といった似通った型が複数存在し、混乱の元になっていました。(これはアプリの画面毎に必要な情報が微妙に異なり、それぞれで不要な計算を避けるためです。)こういった場合にGraphQLでは必要なものだけを取得するため、その辺りがシンプルに表現できて嬉しいはず、という期待がありました。

タイミングとしてもちょうど良かった

既存アプリにGraphQLを導入するにあたって、REST APIで取得する箇所とGraphQLで取得する箇所が混在するのはなるべく避けたく、そう考えたときに、(機能的に独立性のある)新規開発での導入は、「移行」ではなく「追加」によって実装できるため、タイミングとしては最適でした。

導入方針

ここからはAndroidの話です。 Apolloではクエリを書くと、それに基づくデータクラスが自動生成されます。

例えば次のようなクエリがあったとき、

query LoungeReservation($id: Int!) {
  reservation(id: $id) {
    id
    shop {
      name
      brand {
        name
      }
    }
  }
}

次のようなデータクラスが生成されます。

public data class Reservation(
  public val id: Int,
  public val shop: Shop,
)

public data class Shop(
  public val name: String,
  public val brand: Brand,
)

public data class Brand(
  public val name: String,
)

GraphQLではクライアント側で必要なデータを自由に取得できるため、UI(画面)に合わせてクエリを書き、Apolloが生成したデータクラスをそのままUIステートとして扱うことが多いように思います。これにはレスポンスとUIステートのマッピングを行う手間が削減できるというメリットがあります。

しかし、今回の実装ではUIステートを別途定義し、Apolloが生成するデータクラスを一度変換した上で利用することにしました。

// こうではなく
@Composable
ReservationText(reservation: Reservation) {
  Text("予約店舗: ${reservation.shop.brand.name} ${reservation.shop.name}")
}

// こうする
@Composable
ReservationText(uiState: ReservationUiState) {
  Text("予約店舗: ${uiState.brandName} ${uiState.shopName}")
}

data class ReservationUiState(
  val brandName: String,
  val shopName: String,
) {
  companion object {
    fun from(reservation: Reservation): ReservationUiState 
      return ReservationUiState(
        brandName = reservation.shop.brand.name,
        shopName = reservation.shop.name,
      )
    }
}

こうした理由は、大きく3つありました。

柔軟性:GraphQLとREST APIをうまく共存させたい

今回は新規機能の開発であり、データ取得の大半はGraphQLで完結すると想定していました。しかし、GraphQLとREST API、それぞれのデータソースから取得した値を結合したい、といったユースケースが出たらどうだろうか、今は無くても今後出てくる可能性があるかもしれない、と考えたときに、全体方針として抽象化を1段挟んでおいたほうが安全だろう、としました。

拡張性:複雑なUIステートも書けるようにしておきたい

(新)Guide to app architectureでも言及されていますが、UIステートの値から算出される値をUIステート内に定義する、というパターンがあります。

例えば、COFFEE Appではラウンジの予約画面で、入力項目を埋めた場合に確認のUIを表示したい、というケースがありました。

コードで表すと次のような感じです。

 data class ReservationUiState(
  val selectedCheckinId: String?,
  val selectedSeatId: String?,
   ...
) {
  val isConfirmSheetVisible: Boolean
    get() = selectedCheckinId != null && selectedSeatId != null

  ...
}

こうした処理は、Apolloが生成するデータクラスを拡張することでも実現できますが、そこまでするなら別途UIステートを定義してしまったほうが良いよね、という結論に至りました。

依存性:UI層がData層に依存してほしくない

前に述べたように、実装量の面ではApolloが生成するクラスをそのまま利用したほうが良いですし、GraphQLらしい作りと言えるでしょう。しかしそれはApolloへの依存をUIが持つことに他なりません。1からアプリを作る場合には、依存を許容することが多いと思いますが、今回は既存アプリへの導入であり、REST APIと共存する部分的な活用でした。それゆえGraphQLはあくまでREST APIと並んだ存在であり、UIから見たときには、それらが隠蔽されている、という方針を重視しました。実装の手間と依存、どちらを取るかという話で、これは最後まで迷ったところでした。

実際、使ってみて

想定と現実

まず、「必要なものを予め用意しておけば、あとはクライアント側のエンジニアで自由に取得してね、という形にできる」という想定には、完璧なスキーマが定義されている、という前提があるように思いました。 例えば、「リストの0件はnullで表現されるのか、空のリストで返ってくるのか」「ここは何故nullableになっているのか。実装上仕方なくなのか、手違いなのか」といった確認の往復は、いざクライアント側のエンジニアが実装に入ったタイミングで実際にありました。スキーマが成熟していけば徐々には減るとは思うのですが、コストとしては確実に存在していて、APIをレビューするのか、スキーマをレビューするのか、という差の話だと思いました。

あとはよくある話として、「クライアント側のエンジニアで値を自由に取得できる」は負荷観点でヤバいクエリが叩けるのでは?というのがあります。これについてはPersisted Queryという仕組みがあり、登録したクエリを叩くことで、無秩序なクエリが実行されるのを防ぐことができます。今回の実装ではPersisted Queryを使用しなかったのですが、代わりにある程度実装が固まったところで、関係者を集めてクエリをレビューする時間をとりました。これはバックエンド-フロントエンド間の認識を合わせる意味もありましたが、iOS-Android間で妙に異なるクエリがないか確認することで、実際片方のOSで実装漏れが見つかる、といった副次的な効果もありました。

Apollo Kotlinについて

わりとKotlin/Androidフレンドリーというか、Androidで利用することを想定してしっかり作られている印象を受けました。まずコルーチンに対応しています。大事ですね。 次に(一部限定的ではありますが)マルチプラットフォームにも対応しています。これは人によってはありがたいかもしれません。 あとはキャッシュ周りが良い感じです。SQLiteのキャッシュをSingle Source of Truthとする ことができて、キャッシュ→ネットワークとFlowで複数回データをemitするようなAPI も最近追加されました。これはイマドキなアーキテクチャを作っていく上でありがたいと思います。また、(効率のよい)キャッシュの保存もID指定を少し足してあげる程度 で実装できます。気になる点としては、SQLiteのキャッシュが蓄積される問題があって、この辺りが解消されるといいな、という期待があります。

以上、レポっす

というわけで、GraphQLのお話でした。アプリが実際に動いている様子を見たい方は、COFFEE Appをダウンロードしてみて下さい!

あとは、最後にお決まりのやつを貼っておきます。よろしくお願いします。

カンカク入社から1年間のフロントエンド開発を振り返ってみた

こんにちは。フロントエンドエンジニアのKeita(@KeitaBangkok)です。

2021年4月下旬にカンカクに入社してから、もうすぐ1年が経ちます。

カンカク1人目のフロントエンド専任者として、複数の自社プロダクトを横断しつつもコア開発を担当する裁量大きめな1年になりました。入社から22年4月現在まで、弊社のフロントエンドエンジニアは自分1人となっています。

この1年間で自分が各プロジェクトでやってきたこと、そして今後やっていきたいことを中心に振り返ってみます。

担当プロジェクト一覧

まず、自分が各プロジェクトに取り組む上での共通のマインドのようなものをまとめてみます。

弊社のバックエンドエンジニアは、一通りのフロント実装をこなせるフルスタックな方が多く、特に入社当時はフロントエンド専任の自分がどのように価値を出していくかを考えていました。言語化すると、特に以下3点を意識して開発しています。

  • 内部コードの質にこだわる
    divタグの踊り食いで見た目の実現はできても、意味合いに適した正しい文書構造をつくることが「マークアップ」と言える行為だと思っています。
    例として、意味合いに適した定義リスト(dlタグ)を使うよりも、divタグを使用した方がCSSの記述量が少なく済む場合があったりします。ただ、ここは逃げずになるべく文書構造にこだわったマークアップをするべきだと考えています。

  • 各主要ブラウザで正常な表示/挙動を担保する
    弊社ではInternet Explorer とOperaを除く主要ブラウザに対応するルールを設けています。こうなると一番の曲者はSafariですね。
    簡単な例を出すと、CSSプロパティscroll-behaviorがSafariの大半のバージョンで対応していないためJavaScriptでの処理が必要になったりと、Google Chromeで確認しているだけでは気づけない点にも気を配る必要があります。全ブラウザで表示/挙動が統一された平和な世界が訪れるにはまだ時間がかかりそうな印象です。

  • デザインデータからは読み取れない箇所のUI/UX実装をリード
    マウスオン時のスタイルやアニメーションを用いた各要素の表示については、主体的にデザイナーと相談し実装するようにしています。例えば実装案A~Cをこちらで用意した際、デザイナーさんのロジックに基づいて選ばれたものが自分の中の第一候補でなかったりする場合も多く、気づきがあって面白いです。

続いて、各プロジェクトについて見ていきます。

GOOD EAT CLUB

食のマーケット&ファンクラブ『GOOD EAT CLUB』のフロントエンド改修を担当しています。直近では、自分が特に工数を使っているサービスでもあります。

バックエンドエンジニアの方が裏側の機能開発と合わせてフロント実装を一任することもありますが、現状自分の担当となる範囲は主に以下2点です。

  • UI/UX改善施策の実装
  • キャンペーンサイトのリリースマネージャー

現状GOOD EAT CLUBの開発環境はフロントエンドとバックエンドが共通のリポジトリで管理されており、画面表示部分はRuby on Railsのerbおよびhamlファイル、そして一部のVue.jsコンポーネントで描画されています。

今後として、フロントエンドをバックエンドから分離するタスクが実施予定になっています。TypeScriptも導入するためVue.jsよりも相性の良いReact(Next.jsにする予定)でフロントの実装をし、バックエンドとはGraphQLで連携する想定です。

これは技術的にも工数的にもチャレンジングなタスクになることが目に見えており、完遂するためにはいよいよ自分以外のフロントエンドエンジニアの協力が必要だなという所感です。

TAILORED CAFE online store

スペシャルティコーヒーを販売するECサイト TAILORED CAFE online storeの開発も担当中です。 現在もリニューアルが進行中で、自分が携わっているプロジェクトの中でも特に新試作起案→実装までの回転率が高いです。

また、開発に着手する前の段階で、コンセプト決定などの根本的な部分のディスカッションに参加する機会が多い環境でもあります。

フロント実装としては、 TOPページや定期便購入ページのリニューアル、コーヒー診断機能の改修、クリエイターとのコラボ商品販売ページの作成などを行ってきました。自分は本ECサイトがShopify開発デビューとなったため、Liquidファイルを使用したサイト実装についてキャッチアップしつつの開発作業でした。

今後もCVR改善のための施策を行っていきますが、フロントの技術的な面としては、現在静的実装になっている部分をLiquidファイルの特性を生かし、コード改修なく管理画面上からのサイト更新ができる範囲を拡張していく予定です。

WAKE

冷凍スムージーのサブスクD2Cブランド『WAKE』の立ち上げと運用も担当していました(現在はサービスサイト移行のため休止中)。

f:id:keitainoino:20220405145318p:plain
【WAKE】TOPページ

定期便商品を販売する上でメリットが大きいと判断し、ecforceというECプラットフォームを利用していました。

ecforce自体が比較的新しいサービスのためサイト実装上の諸情報が世に出回りきっていないこともあり、サイト公開前数日はカスタマーサポートに電話しながらカタカタとコードを打っていたのを覚えています。

サイトはLiquidファイルメインで構築されており、Shopify開発で得た知見はある程度生かすことができたように思います。数字的な結果を受け、短いスパンで新規施策を形にしていくことが多く、実装にはスピード感が求められました。

当初は定期便商品のみを販売していましたが、ニーズを踏まえギフト商品の展開も行っていました。

f:id:keitainoino:20220407003748p:plain
【WAKE】GIFTページ

HOLON

クラフトジンのブランド『HOLON』を販売するECサイトです。

自分の入社前からサイトの運用が始まっており、自分の担当は新商品販売時の調整や新規セクションの追加が主になっています。

HOLONはInstagramの運用が成功していることもあり、今後試したい施策として、サイトにもInstagramコンテンツを表示することで滞在時間ならびにCVR向上を実現できるか検証したいと思っています。

また、根本のサイト構造はリリースから大きく変わっていない状態での運用が続いているため、各数値やヒートマップ観測などをもとに、掲載コンテンツの最適化も実施したい気持ちです。

自社コーポレイトサイト

弊社のコーポレイトサイト改修もタスクの一つです。

入社前の時点でSEOやUX観点の改善案レポートを自主的に提出するという僭越なアクションをしており、入社後は列挙していた改修を第一タスクとした取り組んだのを覚えています。

コーポレイトサイト改修で一番大きかったタスクとしては、採用強化のためのコンテンツ拡充とNext.js+HeadlessCMS導入になります。詳細は以下記事をご覧ください。 developers.kankak.com

現在CMS化しているのはNewsおよびCareerセクションのみであるため、他セクションについてもCMS化を進めていく予定です。

TAILORED CAFE ブランドサイト

自社運営のカフェ、『TAILORED CAFE』のブランドサイト改修も担当しています。

入社以後、大きめなタスクとしてまずデザインのフルリニューアルがありました。リニューアル前の以下サイトキャプチャと現行サイトを比較してみてください。

f:id:keitainoino:20220404195929p:plain
【旧】tailoredcafe.jp

フルリニューアルにあたりデザイナーさんが作成したデータが以下です。弊社ではデザインデータ作成にFigmaが採用されています。

f:id:keitainoino:20220408133658p:plain
【Figma】tailoredcafe.jp

自分が直近で意識的にも特に重きを置いてインプット/アウトプットを行っているのはReact関連のものになりますが、上記のようなデザインカンプからのコーディングタスクも定期的に発生するため、根底にあるWeb制作面のスキルも衰えることがないと思っています。

本サイトはリニューアル以前からwebpack構築の静的な運用が続いていましたが、ご時世的に各店舗の営業時間の変更や店舗メッセージの更新頻度が高く、こちらもコーポレイトサイト同様Next.jsおよびHeadlessCMSを導入し、サイト更新の簡易化を図りました。

後にONLINE STOREの商品掲載箇所もCMS化しており、引き続きCMS導入セクションを拡充していく予定です。

KITASANDO COFFEE ブランドサイト

同じく弊社が運営しているカフェ『KITASANDO COFFEE』のブランドサイトです。

自分の入社前まで、このサイトはノーコードサービス"STUDIO"で構築されていました。 エンジニア知識を必要とせず、簡易的にサイトを立ち上げることができるSTUDIOはメリットが多いですが、細かく見ていくと以下3点の改善ポイントがあるように思えました。

  • SEO観点でベターなHTMLタグを配置
  • HTML階層を効率化
  • デザイン改修のハードルを下げる

見た目が全く同じサイトをスクラッチで構築し、上記3点を解消しました。

フロントエンドエンジニアが在籍していて、ある程度工数も取れると言うことであれば、スクラッチで構築しておいた方が良いと個人的には思っています。STUDIOの都合上、やや冗長的に思えたHTMLの階層構造も解消し、内部コード量もスッキリとさせることができました。

結果的に、後にデザイン込みのコンテンツ拡充を行う機会も出てきたため、一から構築し直して正解だったなと思います。

現在は同ブランドでラウンジ業態店舗を下北沢にオープンしているため、この新店舗を訴求する新コンテンツを追加していく予定です。また、このサイトにもHeadlessCMSを導入し、店舗運営者でもサイト更新が可能な形に改良していきます。

おわりに

これまでとこれからのフロントエンド開発について語ってみました。

開発の内容に加えて、過去1年で様々なプロジェクトに携わることができた点も、自分が弊社でモチベーション高く業務に取り組めている要因の一つだと感じています。

2年目の計画としては、フロントエンドのモダンスキル導入や、バックエンドと連携する環境を新規構築していくことで、開発の質向上も重点的に行っていく予定です。

事業や開発内容に興味をお持ちいただけた方は、ぜひ以下リンクからお気軽にご連絡ください!

自社コーポレートサイトにNext.js+HeadlessCMSを導入してみた

こんにちは。カンカクでフロントエンドを担当しているKeita(@KeitaBangkok)です。

最近、自社のコーポレートサイトにNext.js(TypeScript)とHeadlessCMSを導入しました。

社内的に意義のあるアウトプットになりつつ、比較的モダンな技術を扱う良い機会となったので、簡潔に概要をまとめていこうと思います。

導入の経緯

社内の採用強化を目的としたプロジェクトの一つとして、コーポレートサイトのコンテンツ拡充の話が挙がりました。

元々静的構築なサイトでしたが、これにNEWSセクションとCAREERセクション(採用職種とリンクを掲載)を追加し、諸情報の露出を増やしていく狙いです。

このため元々はwebpack構築だったサイトに、エンジニア以外のコーポレート担当の方でも容易に更新できる仕組みとしてHeadlessCMSを導入、そして今後も考慮し思い切ってこのタイミングでReact(Next.js)にリプレイスしよう、という流れでした。

サイト外観の変化以上にリポジトリ内のディレクトリ構造がガラッと変わる改修タスクになりました。

ちなみに、弊社では自社運営カフェのブランディングサイトで既にNext+HeadlessCMSを導入済みでした。

HeadlessCMSの選定

HeadlessCMSにはContentfulを採用しています。

最終候補としてContentfulとmicroCMSが挙がり、無料枠の多さに分があるContentfulに決定しました。microCMSと比較するとContentfulは日本語非対応になっていますが、それぞれのコンテントモデル(CMS化する項目)のタイトル等はこちらで設定するため、コーポレート担当の方が触る部分は実質日本語にすることができ、操作に抵抗感のない管理画面になっているように思います。

f:id:kankak_inc:20220329182546p:plain
contentful操作画面

実際に担当の方に管理画面の操作方法を共有しましたが、特に滞りなく理解してもらえた印象です。

実装概要

簡潔に開発の概要をまとめていきます。
実装にあたりNext+TypeScriptな環境を構築しました。

ディレクトリ構造

tsxファイルを格納しているディレクトリ構造は以下の通りです。

⁝
├─ src
    ├─ pages
    ├─ section
    ├─ components
    └─ ...

この内、CMS関連のファイルが格納されているのはsrc/pagessrc/sectionです。

create next appで環境構築するとルーティング用のpagesフォルダがルートに配置されますが、個人的にはこの上で別途componentsフォルダ等を設けると雑多な構造になる印象があります。そのため、Next.jsがデフォルトでサポートしているsrc/pagesの構造を採択しました。

Contentful用の型定義ファイルやライブラリファイルもsrc配下にまとめ、ルートからの枝分かれを最小限にした構成としています。

CMS導入部分

以下はページ表示の大元となっているsrc/pages/index.tsx内メインコンテンツ部分の抜粋です。

<main>
  <Hero />
  <News content={news} />
  <Career content={career} />
  <Vision />
  <Brands />
  <About />
  <Contact />
</main>

実際のサイトを見ながらだとわかりやすいと思いますが、セクション毎にコンポーネント化しています。NewsとCareerタグでは下位コンポーネントにCMSのデータ受け渡しを行なっています。

例として、データを受け取ったNewsセクションsrc/section/News.tsxの中身は次の通りです。

import { EntryCollection } from 'contentful';
import { ICorpNewsFields } from '../@types/generated/contentful'; // Contentful用型定義ファイル

// Contentfulデータの型定義
interface NewsProps {
  content: EntryCollection<ICorpNewsFields>;
}

const News = (props: NewsProps) => { // 受け取ったContentfulデータの型指定
  return (
    <section className="news" id="news">
      <div className="section__inner">
        <div className="sectionTitle__border">
          <div className="sectionTitle__wrap">
            <h2 className="sectionTitle">NEWS</h2>
            <span className="sectionTitle__sub">プレスリリース</span>
          </div>
        </div>
        <ul className="newsList">

          {props.content &&
            props.content.items.map((value, i) => (
              // Contentfulデータ(Newsタイトル,URL,日付)をループ出力
              <li key={i}>
                <a href={value.fields.url} target="_blank" rel="noopener noreferrer">
                  <div className="newsList__text">{value.fields.title}</div>
                  <span>{value.fields.date}</span>
                </a>
              </li>
            ))}
        </ul>
        <div className="newsMore">
          <a href="https://prtimes.jp/main/html/searchrlp/company_id/47640" target="_blank" rel="noopener noreferrer">
            Learn more &gt;
          </a>
        </div>
      </div>
    </section>
  );
};

export default News;

value.fields.xxxにそれぞれContentfulで設定した値が入っています。Contentfulのライブラリを用い生成したICorpNewsFieldsにてデータの型安全を担保しました。

これを画面表示した結果が以下キャプチャです。

f:id:kankak_inc:20220329212310p:plain
CMS化したNewsセクション

導入後の所感

デザインにこだわった既存サイトにCMS機能を持たせたいニーズは結構あると思いますが、その際にはやはりHeadlessCMS導入が有効ですね。簡単なテキスト修正レベルのタスクのために都度コードをいじりpushを繰り返すことは地味に負荷がありますが、これを解消できるメリットも大きいです。

また、採用強化を主目的としたタスクでしたが、弊社で利用実績のなかったNext.jsやHeadlessCMSを技術環境に追加できたこと自体も、エンジニアの方のアトラクトにプラスに働くのではないかと思ったりもしています。

最後に

以上、最近のフロントエンドエンジニアとしてのアウトプットを語ってみました。

カンカクでは現在、全方位的にエンジニアを募集しています。
興味をお持ちいただけたら、まずはカジュアルにお話してみませんか?以下リンクから気軽にご応募ください!