ZOZOTOWNの企画LPをアーカイブする!

ZOZOTOWNの企画LPをアーカイブする!

目次

はじめに

こんにちは! ZOZOTOWN企画開発部・企画フロントエンド1ブロックの秋山です。ZOZOTOWNトップでは、セール訴求や新作アイテム訴求、未出店ブランドの期間限定ポップアップ、著名人コラボなどの企画イベントが毎日何かしら打ち出されています。私はそのプラットフォームとなる企画LPをメインに実装するチームに在籍しています。

ZOZOTOWN TOPの企画導線

チーム特性としては以下のようなものがあります!

  • クリエイティブコーディング寄りの実装をする機会に恵まれやすい
  • 普段のLP案件内では、エンジニアよりも他職種(デザイナー、PM、ビジネス職)と連携することが多い

企画LPの儚さ

定常的なページと違って、開催期間が定まっている企画LPの命は儚いものです。月に20本ほどの企画LPがローンチされては、人知れずクローズしていきます。

not found

制作陣一同、タイトなスケジュールのなか心を込めてつくりますが、目に見えやすいかたちで実績が残りません。もちろん、GitHubリポジトリ上のコードやデザインラフとしてはある程度残っていきますが、色々と容量を圧迫するものは削除していきたい所存です。そうすると、成果物は段階的に消滅することになります。

削除されないうちにも、企画量が膨大すぎて過去事例を掘り当てるのに苦労しがち、という問題もありました。「あの企画のとき、どう対応してたっけ?」と聞かれても、咄嗟に目当ての情報を見つけられなかったりします。

企画LPをアーカイブする!

ちょうどデザイナーチームでも似たような課題感を抱えており、この機会にきちんとプロジェクト化して「ZOZOTOWN LP ARCHIVE」という社内ツールをつくることにしました!

まず、普段から親交のあるデザイナーと2人で構想・仕様の大枠を練ったあと、PMにもジョインしてもらい、ビジネス側にもヒアリングしながらプロジェクトを進めました。この起案メンバーでは週次定例を行い、全体の仕様や運用方法を詰める作業や、各部の進捗確認をしていきました。

仕様

  • リアルタイム検索(フリーワード、日付、タグ検索)
  • LP画面のキャプチャ閲覧・DL
  • LP概要閲覧
  • 各種ドキュメントへのリンク

上記の機能をミニマムで盛り込み、デザイナー・PM・事業部・エンジニア等、企画に関わる多方面のスタッフにとって嬉しいツールを目指します。

ホーム画面のUI

詳細画面のUI

キャプチャ画面のUI

これはZOZOTOWNの企画コンテンツ全体としてのポートフォリオでもあります。前提として新規LPは全てアーカイブされますが、せっかくなので過去1年程度の企画はがんばってデータを集め、反映することにしました! これだけでも、200本ほどの量になります。

開発について

さらに! 開発メンバーとして、元気な新卒2年生とフロントエンド道25年の大ベテランの方に入っていただけることとなり、プロジェクトが本格始動しました!

メンバーそれぞれが事業案件を持ちながらの実装且つ、SRE等の他部署と連携が必要だったので、空白の期間も挟みつつ進みました。全ての工程を完了するには2か月くらいかかりました。

技術選定

社内ツールということもあり、個人的に試してみたいものを自由に使うことができました! 会社のプロダクトで技術選定を自由に行える機会はそう多くないので、わくわくします。

採用したのは、Next.js(App Router)、Panda CSSなどです。Recoil、dayjs、js-file-downloadなどのライブラリも、適宜相談・検討しながら導入しました。ヘッドレスCMSはmicroCMSを利用しました。

ちょっと技術の話

工夫した点などを開発メンバーに聞いてみましょう!

たけさん

こんにちは。大ベテランということで技術選定や困ったことがあったときなどのフォローとアドバイスを担当した竹口です。大ベテランということで紹介されましたが、ZOZOへは入社したてといった状態でした。ということでメンバーの技術レベルや社内の開発環境はどういったものかを把握するところからはじめました。

本記事を読んでいただくとわかると思いますが、意欲高めのメンバーが揃っていたということもありNext.jsのApp RouterやPanda CSSといったZOZOTOWN開発本部内で採用実績のない技術を使っています。社内ツールということもあって積極的にやりたいことを自分たちで選択していきました。

今までのNext.jsのルーティング方式であるPages Routerと、比較的新しいApp Routerになってから変更されたところを調べながら、メンバーでああでもない。こうでもない。といった感じで進めました。最終的には社内のホスティング環境がサーバーでレンダリングできないのがわかり、急遽SSG化してGitHub Actionsでビルドしてデプロイといった大きな変更も。柔軟に対応できるメンバーだったのは日頃から密に対話して進めていたからできたことかと思います。

工夫したのはNext.jsによる開発経験が高めのチームではなかったためPull Requestに対してファイルや変数の命名、コンポーネントの粒度など細かく事例を示したり代替案を出したりしながらレビューしたところです。

入社したてで良いメンバーと楽しく進めることができたのは良い経験となりました。

ゾイちゃん

こんにちは。LPアーカイブのホーム画面実装を担当したゾイです。ホーム画面のメイン機能はリアルタイム検索でしたので、私からはリアルタイム検索の機能実装について工夫した点を話したいと思います!

リアルタイム検索には企画タイトルや企画の開催期間のような基本的な情報はもちろん、技術・デザイン・企画種などのタグ検索機能を実装する必要もありました。

SSRで検索機能を実装することも検討しましたが、以下の3点を踏まえCSRで検索機能を実装することにしました。

  • ヘッドレスCMSにリクエストを送りすぎると負担が重い
  • SSRの場合ロード時間が長くなりUX的に良くない
  • LPアーカイブサイトはSPAっぽくサクサク遷移させたい

そのため、状態を共有するコンポーネントが多くてもパフォーマンス的に問題ない設計を工夫する必要があり、状態管理ライブラリーの導入を検討しました。

結果から言うと、今後のメンテナンスしやすさを優先し可読性が良いと思われるRecoilを導入することになりました。

Recoilを導入した理由は次の通りです。

  • Recoilはatomが更新されたら該当のatomを利用するコンポーネントのみ再レンダリングするため、パフォーマンスが良い。
  • RecoilはReduxのような他の状態管理ライブラリーに比べて可読性が良く、今後のメンテナンスがしやすい。
    • 新しい状態を追加することが必要になった場合RecoilだとRecoilRootの配下に新しいatomを追加するだけで済むので、Contextのように親コンポーネントを再度ネストする必要がない。

RecoilはSSR時にメモリーリークが発生している問題があるようです。しかし、LPアーカイブでは以下の点を踏まえ、Recoilを利用しても問題ないと思いました。

  • LPアーカイブでは主にCSRを利用している
  • 社内ツールなのでのスケールも比較的に小さい

それでは、具体的にRecoilを利用してリアルタイム検索機能をどう実装したかを話したいと思います。

1. RecoilRootの配下に親コンポーネントを包む

<RecoilRoot>
  <Home /> 
</RecoilRoot>

2. SSR時にヘッドレスCMSから取得したデータをatomに保存する

  // 全データ保存用のatom
  const [result, setResult] = useRecoilState(resultAtom) 

  // ヘッドレスCMSからのレスポンスが更新された場合のみ状態を更新する
  useEffect(() => {
    if (result !== data) setResult(data) 
  }, [data, result, setResult])

3. 各々のフィルターコンポーネントの入力状態を(2)とは別のatomに保存

// 企画タイトルでフィルターするコンポーネントの例
export const TitleSearch = () => {
  const [_, setSearchInput] = useRecoilState(searchInputAtom)


  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchInput((prev) => ({
      ...prev,
      title: e.target.value,
    }))
 }

 return (
    <input
      type="text"
      placeholder="例)ZOZOWEEK"
      onChange={handleChange}
    />
 )
}

4. ユーザーのインプットによりフィルターコンポーネントの値が更新されたら(2)のデータをフィルターし、新しい結果を表示する

import { useRecoilValue } from 'recoil'

import { searchInputAtom } from '@/recoil/atom/searchInputAtom'
import { resultAtom } from '@/recoil/atom/resultAtom'

// (2)で取得したデータ
const result = useRecoilValue(resultAtom)
const [filteredResult, setFilteredResult] = useState(result)

// (3)で更新したフィルターのデータ
const searchInput = useRecoilValue(searchInputAtom)

useMemo(() => {
  const filters = [
    // LPのタイトルでフィルターする場合の例
    const lpTitleRegex = new RegExp(searchInput.title || '', 'i')
    (item: ZozoLpArchiveContent) => lpTitleRegex.test(item.lpTitle),

   // 開催期間など、その他のフィルター(省略)
  ]

  const filteredResult = result.filter((item) =>
    filters.every((filter) => filter(item))
  )

  setFilteredResult(filteredResult)
}, [result, searchInput])

秋山

秋山の方からは、CSS in JSライブラリPanda CSSの使用感の紹介です。

ZOZOTOWNのフロントエンドリプレイス環境ではNext.jsのPages Routerを採用しており、CSS in JSライブラリにはEmotionを利用しています。

今回はNext.jsのApp Routerを使いたかったので、React Server Componentがデフォルトということになります。Emotionは'use client'したコンポーネントでしか利用できない(実装を開始した2023年末時点)ため、App Routerのよさを生かすにはEmotion以外の選択肢を探す必要がありました。

  • 今後他のメンバーが参画する可能性もあり、ZOZOTOWNフロントエンドリプレイス環境と似た書き方をできる方がいい
  • 公式ドキュメントが丁寧

Tailwindなども検討してみたのですが、上記の理由から、Panda CSSを採用しました!

Panda CSSにはいろいろな機能が搭載されています。コンポーネント内で何度も使う基本のスタイルと、マイナーチェンジしたスタイルを使い分けて書くことができる“レシピ”という機能がとても便利でした!

基本のスタイルはbaseの中に書いていきます。variants内にはbaseから変化をつけたいスタイルを書いていきます。

const Detail = styled('div', {
 base: {
   width: '620px',
   padding: '20px 15px',
 },
 variants: {
   isCatch: {
     true: {
       padding: '10px',
     },
   },
 },
})


<Detail isCatch={true}>
  略
</Detail>

今回は利用しませんでしたが、“パターン”という機能もあり、Box、Flex、Wrap、Aspect Ratioなど便利なレイアウトパターンが用意されています。これらは適宜importして使います。

Panda CSSを使っていて個人的に感じたデメリットとしては、CSSプロパティをキャメルケースで書かないといけない&値を’ ’で囲わないといけないのがちょっと面倒なことくらいです。

const Capture = styled('li', {
 base: {
   marginRight: '20px',
 },
})

今回はEmotion、styled-componentsっぽい書き方をしてみました。Tailwind風に書くことも可能で、導入にあたって他のCSS in JSからPandaに変更する場合の学習コストはかなり低そうです。ただし自由に書ける反面、規約などで書き方をチームで統一するなどしないと破綻しそうにも思いました。

あとこれは余談ですが、⌘+Sするたび、ターミナルに🐼が出現して地味に癒されます。

ターミナルにパンダが出現する様子

スクラム開発

開発佳境の時期にはデイリースクラムを行い、毎日アイデアを出し合ったり、疑問点を都度解消したりしていました。

デイリースクラムにはGitHubのProject機能を利用しました。タスク管理はGitHubのIssueで行いました。Issue・PRは細かめに分けて、実装・レビュー共にさくさく進められたと思います。また、Slackで専用チャンネルを作り、スクラム以外の時間でも気軽に相談できる環境となっていました。

ユーザーの反応

当初想定していたユーザー層は企画LP関係のスタッフに限られており、使われ方としては以下のようなものがありました。

  • ビジネス職が企画立案時や営業時の参考にする
  • PMが効果検証や要件策定時の参考にする
  • デザイナーがアイデア帳、過去施策との比較、振り返り用として利用する
  • フロントエンドエンジニアが過去の実装例を掘り当てる
  • ZOZOTOWN全体の企画LPポートフォリオとして活用する

いざローンチしてみると、以下のような予想外の恩恵もありました。

  • バックエンドエンジニアが検索リクエスト数を振り返るのにも使える
  • 新メンバーなどは見てるだけでも勉強になりそう
  • デザインかっこよ
  • 眺めているだけでも楽しい
  • UIがよくてわかりやすい
  • ZOZOTOWNサイトと似たUIなので、馴染みのある操作方法で違和感なく説明なくてもすぐ利用できた

といった、デザインを絶賛する声も。

多方面から嬉しいリアクションをもらえたのも、横のつながりを駆使して、職種の垣根を超えて力を合わせた成果です!

おわりに

普段の業務での成果物はトップダウンの事業案件にまつわるものがほとんどです。しかし、今回の「ZOZOTOWN LP ARCHIVE」は現場メンバーの「こんなものを作って便利にしよう!」というアイデアから生まれました! いろいろな立場のスタッフの協力を得て、無事に成果物のローンチ・運用まで漕ぎ着けられました!

それにしても、発案・構想・設計・実装・運用フロー作成と、最初から最後まで育てた成果物には愛着もひとしおです!「自分たちの城」感がすごいですね! 尚且つ、色々な部署にとって、小規模だけど有益なプロダクトである(自己満じゃない)というのがまた良かったです。

株式会社ZOZOでは、アイデア次第でこんなふうに自由度の高い開発経験をできる環境が整っています! ご興味のある方はぜひ、ご応募お待ちしております!

corp.zozo.com

カテゴリー