🛍

💅ECサイトモックを Next.js で立ち上げるワークアウト経験

2022/05/11に公開・約7,100字

モチベーション

👩「将来ネイルチップ💅販売するためのサイト作って!」

と SPA アプリ開発屋が妻に言われ てしまい 、楽に立ち上げられないかと模索中、

https://zenn.dev/stripe/books/stripe-nextjs-use-shopping-cart

というニーズにとてもマッチしたブックがリリースされていたので、参考にさせて頂きながら作ってみることにしました。
この記事は作ってみた際のピックアップメモとなります。
※2022/05/06 時点の上記ブックを参考にしているため、内容やチャプター名が変更になっている可能性があります。

環境や方向性

  • モックなので、お金を掛けたくない(大事)
    • ホスティングは Vercel に任せる ワークアウトなので、ローカルでサーバ動作させるのみに留めます
    • 商品管理(バックエンド)は Stripe に任せる
  • モック時間を掛けたくない
    • 他にもしたいことがいっぱいあるから…😅
      • 設計は後回しにする
    • あまり細かいところは気にせず、セキュリティ等々大事なところは押さえて作る
    • デザインは簡易的にする
      • 勉強がてら Tailwind CSS を使う

ブックを読んで進める

02 Next.jsを利用したアプリのセットアップ

内容に加え、以下の環境をセットアップ。

  • 各種ツール・言語設定
    • TypeScript
    • ESLint
    • Prettier
    • husky, lint-staged (コミット時整形)
    • Tailwind CSS
  • ディレクトリ整理

TypeScript

npx create-next-app@latest --ts

ESLint, Prettier

npm i -D eslint-config-prettier prettier prettier-plugin-organize-imports
.eslintrc.yaml (json からリネーム)
root: true
extends:
  - "next"
  - "next/core-web-vitals"
  - "prettier"
rules:
  import/newline-after-import: warn

Prettier を導入しますが、整形ルールは標準で行くので、 .prettierrc.yaml は作成無しで。
import 文の並びなども整理するようにしています。

husky, lint-staged

npx husky-init && npm i -D husky lint-staged npm-run-all
.husky/pre-commit
-npm test
+npm run lint-staged
package.json
@@ -6,8 +6,14 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint",
-    "prepare": "husky install"
+    "test": "run-p --aggregate-output test:*",
+    "test:next": "next lint",
+    "test:lint": "eslint src --ext ts --ext tsx --max-warnings 0",
+    "test:format": "prettier --check 'src/**/*.{ts,tsx,css}'",
+    "fix": "run-s fix:*",
+    "fix:lint": "eslint src --ext ts --ext tsx --fix",
+    "fix:format": "prettier --write 'src/**/*.{ts,tsx,css}'",
+    "lint-staged": "lint-staged"
   },
   "dependencies": {
     "next": "12.1.6",
@@ -27,5 +33,11 @@
     "prettier": "^2.6.2",
     "prettier-plugin-organize-imports": "^2.3.4",
     "typescript": "4.6.4"
+  },
+  "lint-staged": {
+    "src/**/*.{ts,tsx,css}": [
+      "eslint --fix",
+      "prettier --write"
+    ]
   }
-}
+}
\ No newline at end of file

ディレクトリ整理

create-next-app 標準のままでは、設定系ファイルやらソースコードやらが同ディレクトリ階層で管理されるため、 src ディレクトリを切って pages , styles を移動します。
以後発生するソースコードは src ディレクトリで管理します。

Tailwind CSS

Install Tailwind CSS with Next.js - Tailwind CSS を参考にセットアップ。

tailwind.config.js
 module.exports = {
-  content: [],
+  content: [
+    "./src/**/*.{js,ts,jsx,tsx}",
+  ],
   theme: {
     extend: {},
   },

03 Next.jsで静的なサイトを作ってみよう

daisyUI を導入

今回は Tailwind を導入しているので、Bootstrap はインストールしません。
ブックでは NavBar Container のコンポーネントを使用しており、同様に(楽を)したいため、このタイミングで daisyUI を導入します。

https://daisyui.com
npm i daisyui @tailwindcss/typography
npm i -D eslint-plugin-tailwindcss
# 導入漏れ…
tailwind.config.js
   theme: {
     extend: {},
   },
-  plugins: [],
+  plugins: [
+    require("@tailwindcss/typography"),
+    require('daisyui')
+  ],
 }
.eslintrc.yaml
   - "next"
   - "next/core-web-vitals"
   - "prettier"
+  - "plugin:tailwindcss/recommended"
+plugins:
+  - "tailwindcss"
 rules:
   import/newline-after-import: warn

導入したので _app.tsx を更新、 _document.tsx も追加しテーマ設定。

_app.tsx
 import type { AppProps } from "next/app";
+import Link from "next/link";
 import "../styles/globals.css";
 
 function MyApp({ Component, pageProps }: AppProps) {
-  return <Component {...pageProps} />;
+  return <>
+    <div className="navbar bg-primary text-primary-content">
+      <Link href="/#home">
+        <a className="btn btn-ghost normal-case text-xl">Hello EC</a>
+      </Link>
+    </div>
+    <Component {...pageProps} />
+  </>
 }
 
 export default MyApp;
_document.tsx
import { Head, Html, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="ja" data-theme="cupcake">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

001

07 Stripeに登録した商品を、Next.jsで取得・表示しよう

daisyUI のカードを使ってデザインを整えます。

https://daisyui.com/components/card/

002

08 use-shopping-cartで簡単な決済フォームを追加しよう

checkoutSingleItem() の引数が異なる

https://zenn.dev/stripe/books/stripe-nextjs-use-shopping-cart/viewer/step4-1_client_checkout#useshoppingcarthookで、「いますぐ注文」ボタンを実装しよう

useShoppingCart を利用して、 checkoutSingleItem関数を取得しています。
この関数にStripeの料金IDを渡すことで、StripeのCheckoutページに移動できます。

と書かれているのですが、TS 環境だと型で叱られます。

003

https://github.com/dayhaysoos/use-shopping-cart/blob/1811d21a3ce4256f9b8fbf2008924720ff1c6328/use-shopping-cart/core/slice.js#L170-L171

実装を見るとサンプルのように sku や price を含んだオブジェクトが引数のようです。

https://github.com/dayhaysoos/use-shopping-cart/commit/2cec9c700db04e22bc3e274131fd458b22464050

を見ると最近 productId → itemsOrPriceId に更新されたようですね。型定義の更新漏れでした。
あまりよろしくないですが @ts-ignore で回避します。
(次のチャプターで削除する処理なので、一旦無視も可能)

004

10 use-shopping-cartで、カート機能を追加しよう

useShoppingCart() から引き出す要素の型解決ができない

TS環境でサンプル通り useShoppingCart() を使用すると、型が any になっていました。

005
006

https://github.com/dayhaysoos/use-shopping-cart/blob/a7c64e4466607f00f145a691011d1d35603fb2c1/use-shopping-cart/react/index.d.ts#L126-L128

SelectorResult & CartActions で戻り値型指定されているんですがなぜでしょう🤔

  // useShoppingCart から直接分割代入すると型が any になるため、
  // 一旦まるごと参照して分割代入
  const shoppingCart = useShoppingCart();
  const { cartDetails, removeItem, formattedTotalPrice, clearCart } = shoppingCart

で解決しました。

11Webhookで注文内容を取得しよう

micro の型定義のインストール

TS環境では micro のインストールだけでは型定義が不足しているので、以下にてインストール。

npm i -D @types/micro

event からのデータ取り出し時に型解決出来ない

決済完了時のイベントから、注文内容を取得する

のサンプルコードが、TS環境だと any となっていました。

007
008

イベント毎にデータ型が変わる

ということだと理解しましたので、以下のような型アサーションで回避…

function extractCheckoutSessionDataObject(event: Stripe.Event): Stripe.Checkout.Session | undefined {
  if (event.type.startsWith('checkout.session.')) {
    return event.data.object as Stripe.Checkout.Session
  }
  return undefined
}

const checkoutSessionData = extractCheckoutSessionDataObject(event);
if (checkoutSessionData?.payment_status === 'paid') {
  const item = await stripe.checkout.sessions.listLineItems(checkoutSessionData?.id);
  // ...
}

ブックの内容を終えて

実装後は以下のようなレイアウトになりました。

009

Stripe を用いることで、簡単にカート実装が出来て、ECサイト立ち上げのイメージが湧いてきました。

もちろんこのワークアウトの内容だけでは、在庫管理や発送状況の管理などの機能がないので、立ち上げまではまだまだ掛かりそうです。

Tailwind は、クラス名で当てたいスタイルをネーミングセンス無しに当てられるのは新感覚で良いかなと思う反面、 className が肥大化してコンポーネント内のコード量が増えるのが気になりました。
今回のようなワークアウトの実装だと直感で書けるので採用で、商用になるとメンテナビリティ考慮で CSS Module の方がいいかなぁという所感。

Discussion

ログインするとコメントできます