styled-components でスタイルが反映されない問題について

はじめに

フロントエンドエンジニアの関(@kur0buchi)です。

今回は styled-components で記述したはずのスタイルがうまく反映されていないという事象に数日間悩まされたのですが、最終的にあっけない解決方法だったので同じ悲しみを背負う開発者が少しでも減るよう記事に残します。

TR;DL

外部ライブラリやバージョン範囲指定の都合により複数バージョンの styled-components がインストールされている場合、一部のスタイルが無効化される場合がある。

前提

この記事の内容は React(Next.js) + TypeScript + styled-components の環境での事象です。
また、弊社ではプライベートレジストリに存在する社内コンポーネントライブラリを運用しているのですが、ここは適宜任意の外部ツールに置き換えてください。

事象

Next.js のバージョンを9から12まで上げる作業をしている際に、やっとこビルド完了までこぎ着けたあと、一部スタイルが崩れている箇所がありました。
(Next.js v9 → v12 の記事はすぐ出せそうなまとまったものがないので機会があれば……)

見ると社内ライブラリ上のコンポーネントのスタイルを読み込んで styled-components で上書きしているような箇所でのみ起こっているようで、loader 周りか? babel ?styled-components の破壊的変更?と調べても解決策が得られず、途方に暮れていた頃でした。

解決

なんと styled-components で記述したスタイルが2つに分けて生成されていました。
これを見れば同じ要素に指定されたスタイルは下側に書かれたスタイルが適用されるのも一目瞭然です。

ここからまた調査を進めると、yarn.lock で依存関係を見るに2つのバージョンの styled-components がインストールされており、そのバージョンは上記の画像と一致することがわかりました。

styled-components@5.2.0:
  version "5.2.0"
  resolved "https://registry.yarnpkg.co...
  integrity sha512-9qE8Vgp8C5cpGAIdFaQVA...
  dependencies:
  ...
styled-components@^5.2.0:
  version "5.3.11"
  resolved "https://registry.yarnpkg.co...
  integrity sha512-uuzIIfnVkagcVHv9nE0VP...
  dependencies:
  ...

上側のバージョン固定されている方がアプリケーション上の package.json で指定しているバージョン、下側の Caret Range(^x.x.x) は社内コンポーネントライブラリで使っている際に指定しているバージョンです。
この2つの styled-components がそれぞれ振る舞い、時にブラウザ上であべこべに適用されているようでした。

今回の例では幸い競合先も自分の管轄なので、 styled-components のバージョンを揃えて無事解決と相成りました。

起きてることから検索する際のキーワードをどうするかも難しいからかいくら調べても情報が出てこずだったので、似たような事象が起こっている人がこの記事にたどり着き、解決のきっかけになったら幸いです。
(普通こんなことにはならないよって可能性もある……?)

最後に

vivit ではバージョンアップを繰り返していけるエンジニアを募集しています。
www.wantedly.com

画像生成AI、 Midjourney をエンジニアで使ってみる会を開催しました!

こんにちは!!

vivit で SRE をやっている 宮本 です。

先日、開発部のメンバーで画像生成AI の Midjourney を試してみる(遊んでみる)会があったので、その模様をレポートしたいと思います! 合わせて、vivit で導入しているエンゲージメントを高める為の福利厚生についてもご紹介します。

今回のエントリーはいつもよりネタ要素が強めです。 ためになる話は殆ど無いので、その点はご容赦ください 🙇‍♂️

開催のきっかけ

vivit には部署内メンバーのエンゲージメントを高める為の素敵な福利厚生があります。 その名も、「エンゲージメント費用」です!!

概要

  • 四半期ごとに支給で、繰越不可
  • 一人当たり 5,000円の支給
  • 使い方は特に指定されていないが、エンゲージメントの向上に繋がることが条件
  • マネージャーの決済が必要

というものです。

メンバー間の交流を促進する為の費用なのですが、何に使うかは悩むところです。 多い使途は飲み会や高級なランチに行くことなのですが、毎回それなのも面白くありません。

そこで今回は第二四半期のお疲れ様会も兼ねて、Midjourney の有料プランを各自で契約して遊んでみる会をすることになりました。

Midjourney の料金プラン

vivit のエンジニア内では AI を積極的に活用しようという方針があったことも背景の1つです。

生成した画像とプロンプトの紹介

以下、お題ごとに見ていきます。

キャンプを楽しんでいる画像

With family, partners and sometimes friends. Free yourself from everyday life and share your favorite things with loved ones in nature Camping time is more comfortable and satisfying than anything else. You don't have to do anything special. Surround yourself with a warm meal while bathing in the sun, and feel the slow flow of time. Just by having such a fun "extraordinary", you can spend a little excitement in your usual daily life. What we want to deliver is such a rich lifestyle that "spends time with someone outside". In order for as many people as possible to “love the outdoors more”, We will continue to help spread the fun of camping. base town feel the image is pop using pastel colors Looking down on the whole from a high point of view

画像生成のプロンプトと言えば , 区切りで単語を羅列するイメージですが、こちらは長文を書いていますね。 命令が多い分、無視されているものもありそうですが、それっぽい画像を生成してくれているのは流石 Midjourney です!


Generate an image of a peaceful camping scene in a forest early in the morning. Morning dew sparkles on the surface of a tent set up between nearby trees. A breakfast prepared on a camp stove is spread out on a picnic table, softly lit by the first rays of sunlight. Convey a tranquil atmosphere that blends seamlessly with the awakening nature around.

こちらも文章でプロンプトを入力していますが、キャンプに行きたくなる素敵な画像が生成されました!
ただし致命的な間違いが。。。

絶対にテーブルの下で焚き火をしてはいけませんよ 🙅‍♂️

細かいところを見ると明らかにおかしいところもありそうです。

vivit エンジニアの風景 採用媒体に使えそうなイメージ

Five Japanese system engineers aged between 20 and 50 are working happily. 
Photorealistic images. 
wearing a T-shirt. 

じわじわくる。特に左下。


Five Japanese IT engineers aged between 20 and 30 are happily working in the office with their PCs spread out. 
A photo-realistic image is generated. 
wearing a T-shirt. 
Some have healthy bodies. 

表情が非常にリアルです! 左下の PC は未来の PC ですかね?

まとめ

紹介しきれなかった画像も含めて、多くの画像を生成して楽しい時間を過ごせました!

四半期に1度ぐらいは部内でこのような楽しい取り組みをしてみるのも良いと思います。

vivit では新しいことにチャレンジし、ともにプロダクトを成長させていけるエンジニアを募集中です!

少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

【Shopify】Liquid で JSON をオブジェクトとして扱う

はじめに

アウトドア用品のセレクトショップ hinataストア の運用チームで開発をやっている氏家です。 hinataストアはShopifyテーマによって構築されており、Liquidのコードを変更して様々な機能を実装しています。

hinatastore.jp

最近、外部から出力してきたJSONファイルをLiquidで扱いたい場面がありました。 なるべくLiquidで完結させたかったのでLiquidでのJSONの扱い方について調べていたのですが、 少し苦戦したので備忘録も兼ねてブログを書いています。

前提: Liquid ではJSONを扱えない

.json ファイルをLiquidでインポートしてパースすることができればいいのですが、実際には難しいです。

以下のようにJSON形式の単なる文字列を .liquid ファイルで定義してみても、

snippets/hoge.liquid

{ "hoge": "hoge" }

theme.liquid

<script>
  {%- capture hoge -%}
    {% render "hoge" %}
  {%- endcapture -%}
  console.log(`1. {{ hoge }}`);
  console.log(`2. {{ hoge.value }}`);
  console.log(`3. {{ hoge.hoge }}`);
</script>

出力

1. { "hoge": "hoge" }
2. 
3. 

オブジェクト的な使い方はできません。 JSON形式の文字列を直接渡した場合も同様です。

theme.liquid

<script>
  {% assign hoge = '{ "hoge": "hoge" }' %}
  console.log(`1. {{ hoge }}`);
  console.log(`2. {{ hoge.value }}`);
  console.log(`3. {{ hoge.hoge }}`);
</script>

出力

1. { "hoge": "hoge" }
2. 
3.

split などのstring filterを使ってパースする方法も考えられますが、実装がかなり重たくなってしまいそうです。

1. メタオブジェクトを使う

Shopifyに追加された「メタオブジェクト」を使用し、JSON形式のフィールドを定義して設定することができます(まだ一部のストアではメタオブジェクトが利用できないかもしれません)。 メタオブジェクトとして設定されたJSONは、Liquid上では metaObject という型で評価されます。 そのため、cartproduct といったオブジェクトと同様にメンバを参照することができます。

メタオブジェクトの定義

theme.liquid

<script>
  {% assign hoge = shop.metaobjects.test_object.test_object.hoge.value %}
  console.log(`1. {{ hoge }}`);
  console.log(`2. {{ hoge.hoge }}`);
</script>

出力

1. {"hoge"=>"hoge"}
2. hoge

メタオブジェクトの詳しい解説は以下のブログが参考になります。

commerce-media.info

2. メタフィールドを使う

メタオブジェクト同様、メタフィールドでもJSONを扱うことが出来ます。 JSONをどのリソースのメタフィールドに設定すれば分からないときは、API経由(もしくはメタフィールド更新用のShopifyアプリ)で shop.metafields に保存する方法があります。

今回はShopify GraphQL Admin API でメタフィールドを設定します。 APIを叩く環境は何でもいいですが、Shopify GraphiQL App というShopifyアプリを入れておくと簡単に叩けて便利です。

Shopify GraphiQL App — Install

1, ShopifyストアのストアIDを取得

リクエス

{
  shop {
    id
  }
}

レスポンス

{
  "data": {
    "shop": {
      "id": "gid://shopify/Shop/xxxxxxxxxxx"
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 1,
      "actualQueryCost": 1,
      "throttleStatus": {
        "maximumAvailable": 10000,
        "currentlyAvailable": 9999,
        "restoreRate": 500
      }
    }
  }
}

2, 1で取得したIDを元に metafieldsSet Mutation を叩く

リクエス

mutation metafieldsSet {
  metafieldsSet(metafields: [
    {
      namespace: "custom",
      key: "test",
      ownerId: "gid://shopify/Shop/xxxxxxxxxxx",
      type: "json",
      value: "{\"hoge\": \"hoge\"}"
    }
  ]) {
    metafields {
      value
      ownerType
      key
      namespace
      type
      id
    }
    userErrors {
      field
      message
    }
  }
}

valueに渡すJSONは↓のサービスなどで文字列をエスケープする必要があります。

Free Online JSON Escape / Unescape Tool - FreeFormatter.com

レスポンス

{
  "data": {
    "metafieldsSet": {
      "metafields": [
        {
          "value": "{\"hoge\":\"hoge\"}",
          "ownerType": "SHOP",
          "key": "test",
          "namespace": "custom",
          "type": "json",
          "id": "gid://shopify/Metafield/23866410696888"
        }
      ],
      "userErrors": []
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 10,
      "actualQueryCost": 10,
      "throttleStatus": {
        "maximumAvailable": 10000,
        "currentlyAvailable": 9990,
        "restoreRate": 500
      }
    }
  }
}

ownerTypeSHOP になっていることから、 shop.metafields に設定されていることがわかります。

3, Liquidから呼び出す

theme.liquid

<script>
  {% assign hoge = shop.metafields.custom.test.value %}
  console.log(`1. {{ hoge }}`);
  console.log(`2. {{ hoge.hoge }}`);
</script>

出力

1. {"hoge"=>"hoge"}
2. hoge

JSONの値が配列だった場合は通常通り for in などでループ処理すればパースできます。

まとめ

LiquidでJSONをオブジェクトとして扱う方法を紹介しました。 JSON形式のコンテンツを表示させたいときなどに参考にしてみてください。

メタオブジェクトの登場により、assets配下に静的ファイルを置いたり、セクションを作成してノーコードで値を設定できるようにしなくても、 簡単にコンテンツを管理できるようになった気がします。 他にも様々な使い方ができそうなので、画期的なアイデアを見つけたらまたブログにしたいと思います。

vivit では新しいことにチャレンジし、ともにプロダクトを成長させていけるエンジニアを募集中です!

少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

hinataメディアAPIのリファクタリングを通して感じたこと

こんにちは!バックエンドエンジニアの北條です。

23卒大学生で、今はインターンとして業務に携わっています。

現在「hinataメディア(以下メディア)で使用しているAPIリファクタリングをする」というタスクを行なっているのですが、新しい発見や学びが多くありました。

メディアのエンジニア向けにドキュメントを作成したのですが、タスクの背景や個人的な感想を交えたカジュアルな記録も残したいなと思い記事にしました。

タスクの始まり

メディアはバックエンドとフロントエンドが分かれており、バックエンドはAPIを作成するのが主な業務になります。

書き方にルールが決まっていなかったのでAPIの作成が人によって違うなど、今後さらにAPIが増えたときにメンテナンスできなくなるのではないかという状態でした。

いつか整備したいんだよね〜とチームの方が言っていたので、興味半分で1on1のときにマネージャーに話してみたら「じゃあ、やってみましょう!」ということに。

チームの方にサポートしてもらいながらではありますが、まったくイメージがつかないまま期待と不安に溢れた状態でタスクが始まるのでした。

課題

メディアではOpenAPIによりパラメータや応答データを決め、それに沿うようにバックエンド、フロントエンドを実装していきます。

パラメータとして受け取る値や返す値の形に制約はあるのですが、受け取った値をどのように加工するかというロジック部分の制約がありません。

上記のように、メディアAPIはロジック部分を自由に書けてしまうという問題点があります。

情報を格納すべき場所で情報の検索や加工を行なっていたりなど、逆もまた然りです。

また、開発者によって実装の仕方もテストの仕方も違うのでレビューが大変という問題もありました。

以上まとめると

  • 実装方法の型や方針がないので構造的な制約もなく役割が曖昧

  • 結果的にレビューが大変

ということになります。

解決策

具体的な解決策は下記の通りです。

  • 役割を決めて層で分ける

  • 抽象クラスを定義する

  • Builderパターンを用いる

また、これらは全て現状よりもレビューをしやすくなるということも踏まえています。

役割を決めて層で分ける

層を分けるというものは元々行なっていたものの口約束だけになってしまい層を分けている意味を成していませんでした。

なので情報を格納すべき層(画像Builder部分)、情報を加工する層(画像Service部分)などディレクトリに定義付けを行うことで役割を明確にします。

今までは変更箇所全てのファイルを見て実装が正しいかレビューをしていましたが、層によって見るべきポイントが分かれているためレビューもしやすくなります。

抽象クラスを定義する

抽象クラスを作成することで具象クラスでのオーバーライドを強制させ、誰が開発してもある程度同じ形になるようにしました。

下記はインスタント珈琲を作るコードです。

今後色々な飲み物を追加したいとなったときは抽象クラス(Drinkクラス)を継承し、materialメソッドを実装するだけで済みます。

class Drink
  def break_time
    material
    drip
  end

  def material
    raise NotImplementedError, '材料を用意してください'
  end

  def drip
    'お湯を注ぐ'
  end
end

class Coffee < Drink
  def material
    '珈琲の粉を加える'
  end
end

class BlackTea < Drink
  def material
    '紅茶の粉を加える'
  end
end

Builderパターンを用いる

BuilderパターンとはGoFにより考案されたデザインパターンの一種です。

モノを作る過程を用意し、インスタンス化するクラスや引数を変えるだけで別のモノを生成するというのがBuilderパターンです。

Builderパターンを用いることで今後APIを量産するとなっても開発しやすくかつレビューのしやすい状態を目指しました。

また、一つ一つの属性をメソッドとして切り出すことでテストもしやすくなっています。

Builderパターンを用いて任意の飲み物を作成するコードを書いてみました。メソッド部分を抽象化することでさらに幅広い飲み物にも対応することができます。

class Drink
  attr_accessor :water, :material, :sugar, :salt, :milk
end

class DrinkBuilder
  def initialize
    @object = Drink.new
  end

  def make(params)
    water(params)
    material(params)
    sugar(params)
    salt(params)
    milk(params)
    result
  end

  def water(params)
    @object.water = params[:water]
  end

  def material(params)
    @object.material = params[:material]
  end

  def sugar(params)
    @object.sugar = params[:sugar]
  end

  def salt(params)
    @object.salt = params[:salt]
  end

  def milk(params)
    @object.milk = params[:milk]
  end

  def result
    @object
  end
end

コーヒー = DrinkBuilder.new.make(water: 10, material: '珈琲の粉', sugar: 0, salt: 0, milk: 0)
ミルクティー = DrinkBuilder.new.make(water: 7, material: '紅茶の葉', sugar: 2, salt: 0, milk: 3)

最後に

メディアAPIリファクタリングをしてみてメディアのAPIの仕組み、抽象クラスやBuilderパターンといったオブジェクト指向についての知識、RubyRspecの知識など様々なことを学べる機会になりました。

オブジェクト指向などの知識はRubyといった言語に囚われないため、他のチームの方とも話し合いが出来ます。 技術の向上ももちろんですが、エンジニア同士での意思疎通をしやすくするような知識を蓄えていくことも重要だなと感じました。

反省点としてはタスクの工数管理が全然できていなかったなと思います。タスクに対してどのくらいの期間で実装できるのか。自分がどのくらいできて、どのくらいできないのかを認識し、適切な工数を見積もるという力もつけていきたいなと思いました。

vivitにはやりたいと思ったことに挑戦できる機会がたくさんあると思います。今後も挑戦に対し貪欲な姿勢で頑張っていきます!!

vivitでは新卒採用、キャリア採用を募集しております。少しでも気になった方は是非お話しを聞きに来てください!

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

Shopify Themeで同系色の商品をカラーチップで絞り込む方法案

フロントエンドエンジニアの氏家です。

私は現在、アウトドア用品のセレクトショップ「hinataストア」の運用を行うストアチームで開発をしています。

hinatastore.jp

hinataストアは Shopify で構築されており、フロントエンドは Shopify Theme を使用しています。

最近、hinataストアで取り扱う商品が増えてきており、連れ各コレクションに表示する商品数が多くなってしまっています。 そのため、目当ての商品をすぐに見つけられなくなってきました。

そこで、コレクションページに商品の絞り込み機能を追加し、「カラーチップを押すとその色と同系色の商品が絞り込まれる」ためのフィルターを作成しました。

今回はこのフィルターがどのように作られ、どのように機能しているかについて簡単に説明していきたいと思います。なお、マーチャントごとに使用しているテーマが違うと思いますので、あくまで「こうすればできるよ」程度の説明と簡単なコード例のみに留めます。

実際の動き

前提

前提として以下を満たしている必要があります。

  • Shopify Themeの商品絞り込みは、Online Store 2.0 のテーマでのみ使用することができる ( hinataストアでは有料の日本語テーマ「MISEル」を使用しているが、他のテーマでも同様に実装が可能 )

  • Liquid コードの修正や JavaScript のDOM操作についてある程度の知識を有している

Shopifyにはコレクションの商品を絞り込むための機能(詳しくは後述)があります。

例えば、「カラー」というバリエーションの「RED」という名前で登録された商品を取得したい場合、クエリに filter.v.option.カラー=RED というパラメータを設定する、といった感じです。

しかし、例えば「○○RED」「△△RED」のように同じ赤色でもバリエーション名が異なる場合があります。hinataストアのようなセレクトショップだとバリエーション名は商品本来の名前で登録する必要があるため、バリエーション名を「RED」で統一することはできません。

実装案

前提を踏まえると、赤色のカラーチップで絞り込みたい場合、「○○RED」「△△RED」という同じ赤色のカラーチップをそれぞれ用意して選択する必要があります。

つまり、同系色の商品を1度に絞り込みたい場合は、該当するバリエーション名を複数同時に検索できれば良いというわけです。

上記の方法を実現するために必要な設定は以下のとおりです。

  1. 各商品へのバリエーション設定 (「カラー」などのバリエーション名で色名を登録)
  2. コレクションページに Storefront filtering のフォーム設置
  3. バリエーション名、カラーコード、カラーチップの対応付け (セクションなどを用意して、バリエーション名とそれに対応するカラーコードを対応付ける)
  4. 3の対応付けをコード側に反映 (セクションに設定された値を取得して、フィルターの処理で使用できるようにする)
  5. 絞り込み用カラーチップの挙動を修正

それぞれの設定について順番に見ていきたいと思います。

1. 各商品へのバリエーション設定

まずは絞り込み条件となるバリエーションの設定を行います。 バリエーション名は何でも良いですが全ての商品で統一する必要があるので、今回は「カラー」という名前で設定したいと思います。

カラーバリエーション

2. コレクションページに Storefront filtering のフォーム設置

絞り込み用のフォームを設置して、色ごとに商品を絞り込めるようにします。 実装方法についてはテーマごとに異なると思いますが、以下が参考になります。

Shopify Search & Discoveryアプリ を追加して管理画面上で設定するのが1番簡単な方法だと思いますが、カラーチップによる絞り込みを実装する場合はさらに追加の実装が必要です。

hinataストアの絞り込みフォームは画像のようになっています。

hinataストア絞り込みフォーム

3. バリエーション名、カラーコード、カラーチップの対応付け

2で設定したバリエーションと、そのバリエーションを絞り込むための色(カラーコード)を対応付けます。 商品のタグやメタフィールドに設定するやり方、Liquidコードにベタ書きで対応付けするやり方などがあると思いますが、今回はMISEルに標準で備わっている、セクションで対応付けするやり方を紹介します。

セクションの作成

1.まずTOPもしくはコレクションページにセクションを作成

2.スキーマにバリエーション名とカラーコードを対応付けるブロックを設定

スキーマの例

{
  "type": "text",
  "id": "property_value_1",
  "label": { "en": "Property & Value #1", "ja": "プロパティと値 #1"}
},
{
  "type": "color",
  "id": "color_1",
  "label": { "en": "Color #1", "ja": "カラー #1"}
},
{
  "type": "text",
  "id": "property_value_2",
  "label": { "en": "Property & Value #2", "ja": "プロパティと値 #2"}
},
{
  "type": "color",
  "id": "color_2",
  "label": { "en": "Color #2", "ja": "カラー #2"}
},
....(大体10個くらい)

3.スキーマにカラーチップとカラーコードを紐付けるブロックを設定 ↓スキーマの例

{
  "type": "text",
  "id": "color_name_1",
  "label": { "en": "Color Name #1", "ja": "色の名前 #1"},
},
{
  "type": "color",
  "id": "color_value_1",
  "label": { "en": "Color #1", "ja": "カラー #1"}
},
{
  "type": "text",
  "id": "color_name_2",
  "label": { "en": "Color Name #2", "ja": "色の名前 #2"},
},
{
  "type": "color",
  "id": "color_value_2",
  "label": { "en": "Color #2", "ja": "カラー #2"}
},
....(大体10個くらい)

4.テーマ編集画面でそれぞれのブロックに値を入れる

↓2のブロック

↓3のブロック

これで カラーチップが押されるそのカラーチップに紐付いたカラーコードが選択されるカラーコードに紐付いたバリエーションが選択される という対応付けが完了したことになります。

※そもそも↓画像のようにバリエーションをカラーチップで表示する必要がない場合は、2で対応付けるのはカラーコードである必要はありません。

4. 3の対応付けをコード側に反映

セクションで設定された値は setting_data.json に格納され、sectionファイル内で参照することができます。 しかし全ての処理をsection内に書くわけにはいかないので別ファイルのLiquidに値を渡したいのですが、別セクションの値を参照したり値を渡したりする機能はありません。 なので、今回はグローバル変数に格納して各ファイルが参照するかたちで設定してみます。

↓ コード例( block.type によって処理を分ける必要がありますがここでは省略しています )

// 明示的に空の配列を定義
window.customColorArray = [];
window.colorChipArray = [];
{%- for block in section.blocks -%}

    // バリエーション用のカラーチップ
    {%- for i in (1..10) -%}
        {%- assign prop_property_value_name = 'property_value_' | append: i -%}
        {%- assign prop_color_name = 'color_' | append: i -%}
        {%- if block.settings[prop_property_value_name] != blank and block.settings[prop_color_name] != blank -%}
            {%- assign property_value_split = block.settings[prop_property_value_name] | split: '|' -%}
            {%- assign value = property_value_split[1] -%}
            {%- assign color = block.settings[prop_color_name] -%}
            {%- if value != blank and color != blank  -%}
                customColorArray.push({
                    name: "{{ value }}",
                    color: "{{ color }}",
                });
            {%- endif -%}
        {%- endif -%}
    {%- endfor -%}

    // 絞り込み用のカラーチップ
    {%- for i in (1..10) -%}
        {%- assign prop_color_name_key = 'color_name_' | append: i -%}
        {%- assign prop_color_value_key = 'color_value_' | append: i -%}
        {%- if block.settings[prop_color_name_key] != blank and block.settings[prop_color_value_key] != blank -%}
            {%- assign color_name = block.settings[prop_color_name_key] -%}
            {%- assign color_value = block.settings[prop_color_value_key] -%}
            {%- if color_name != blank and color_value != blank  -%}
                colorChipArray.push({
                    "{{ color_name }}": "{{ color_value }}",
                });
            {%- endif -%}
        {%- endif -%}
    {%- endfor -%}
{%- endfor -%}

5. 絞り込み用カラーチップの挙動を修正

あとは colorChipArray をもとに絞り込み用のフォームを設置し、各フィルターが選択されたときの挙動を修正するだけです。

↓コード例 ( 各テーマの仕様に合わせて修正する必要があります )

onSubmitHandler(event) {
  const value = event.target.getAttribute("value");
  const targetColors = window.colorChipArray
    .map((colorChip) => {
      return colorChip[value];
    })
    .filter((e) => e);

  // 受け取った値が色名で、かつスキーマに登録されているものだった場合
  if (targetColors.length) {
    // 該当するカラーコードの個数分実行する
    targetColors.forEach((targetColor) => {
      if (targetColor) {
        const colorNameArray = window.customColorArray
          .map((customColor) => {
            if (targetColor === customColor.color) return customColor.name;
          })
          .filter((e) => e);

        // 非表示になっているカラーチップを全て取得するためのセレクタ
        const selectors = colorNameArray.length
          ? colorNameArray
              .map((colorName) => {
                return `input[value='${colorName}']`;
              })
              .join(", ")
          : null;

        if (event.target.checked) {
          // 該当するカラーチップ全てにチェックを入れる
          if (selectors) {
            const originalColorChipElements =
              document.querySelectorAll(selectors);
            originalColorChipElements.forEach((element) => {
              element.checked = true;
            });
          }
        } else {
          // 該当するカラーチップ全てのチェックを外す
          if (selectors) {
            const originalColorChipElements =
              document.querySelectorAll(selectors);
            originalColorChipElements.forEach((element) => {
              element.checked = false;
            });
          }
        }
      }
    });
  }
  // あとはクエリを作成して再レンダリングするかページリロードする
  ...
}

この実装例は、window.customColorArray の値で作成された非表示のフィルターを動的に選択/解除することで、あたかも1つのカラーチップが押されたことによって複数のバリエーションが選択されているように振る舞います。 (使用しているテーマに破壊的変更を加えずに実装しようとしたためこのような実装になっています。)

他にも、クエリをもとにフィルターのチェックを保持しておく処理が必要になりますが、値が出揃っている関係でそこまで複雑な実装にはならないと思います。

まとめ

Shopify は開発のためのリソースが豊富ですが、それ故に何をどのように使っていいか分からなくなる場面が多くあります。 今回のケースも、Storefront APIを叩いたり、Shopifyアプリを作ったりすることで実装することもできたと思いますが、あくまで既存テーマコードの変更のみで対応するやり方を採用してみました。

Shopify開発のナレッジが少ないためこれが良い実装なのかどうかは分かりませんが、同じような機能の実装を検討している方の参考になればと思います。

vivit では新しいことにチャレンジし、ともにプロダクトを成長させていけるエンジニアを募集中です!

少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

皆様とお話できるのを楽しみにしております!!

vivit技術開発部におけるマイクロサービスへの考え方

こんにちは!vivitの技術開発部マネジャーの井島です。 開発部ではマイクロサービスアーキテクチャを採用しており、そのアーキテクチャの考え方について話したいと思います。

この分野は個人的に好きで、一生話してられます笑

はじめに

一般的な「マイクロサービス」の知識は知っている前提で、それをどれだけ自分たちの組織向けにアレンジできるかが、肝になると私は考えています。

マイクロサービスは会社の組織構造に密接に関係します。 ビジネス側の組織構造、開発側の組織構造、プロダクト、事業方針、技術方針、採用方針、(経営方針)など。 会社として最終的にアウトプットを最大化していかなければならない状況下で、どれだけ開発効率を出せる、会社の成長に追従できるシステムアーキテクチャにできるかどうか、という視点からの話になります。

技術的な観点や教科書通りのみで導入してしまうと、事業規模に対してコスト面や事業成長にネガティブに働いてしまうことも考えられ、全体的に考えることが必要です。 マイクロサービスも導入して終わりではなく、状況に応じて戦略的に成長させていくものだと考えます。

あなただったら、どのようにシステムのアーキテクチャをデザインしていきますか?

vivitの組織について

色々部署はありますが、エンジニアは技術開発部に所属しているかたちになります。

もう少し細分化して、技術開発部とビジネス側との関係性は以下になります。

ビジネス側は事業(プロダクト)毎に部署が別れています。 それに紐づくかたちで開発部内でもチーム分割され、ビジネス側とそれぞれが密に連携しています。

使用言語, フレームワーク

エンジニア構成

  • フロントエンドエンジニア
  • バックエンドエンジニア

開発部内各チームにはフロントとバックのエンジニアが所属します。

ほか全体的な状況

  • 開発部内各チームは、基本2人で構成される。 1チームのエンジニア人数が少ない。※開発計画によって人数は増減する。
  • 事業計画によって、プロダクトにかける開発工数が上下し、兼任もあり得る。
  • 開発内容は、ビジネス側の事業部毎に管理され開発に降りてくる流れ
  • 基本的にフロントエンド、バックエンドとで職務が別れている

マイクロサービスの群雄割拠

実は、vivitにマイクロサービスが導入されたときは、2019年あたりのレンタル事業、スポット事業が立ち上がったころでした。 この頃はvivitが新規事業を立ち上げていったころで、0→1の開発で開発部も1チームでした。 この頃のシステム構成は1チームが10前後マイクロサービスを開発するかたちでBazelも使って同時ビルドしていました。

マイクロサービス分割設計が境界付けられたコンテキストになっておらず、要件を満たすために結局複数マイクロサービスを開発することになり、開発は非効率なままでした。 その問題から、マイクロサービスの統一化が進みました。

たしかに、0→1 の段階でプロダクトの業務や機能も変化が激しい時期に業務単位でマイクロサービス分割するのはかなり難しいことだということが得られました。

マイクロサービスの考え方の1つに「作りやすい壊しやすい」がありますが、新規マイクロサービス作成にコストがかかる状況も、統一化の方向性に向けていたと思います。

マイクロサービス所有者

教科書的には1チーム1マイクロサービスを聞いたことがあると思いますが、vivit開発部の場合はどうでしょう。

1チームにフロントエンドエンジニアが開発するサーバ(Next.js)、バックエンドエンジニアが開発するサーバ(Go) がすくなくとも存在します。

野良マイクロサービス(明確な所有者不在)

プロダクト共通で使っているシステムも存在しています。「会員管理システム」と読んでいるもので、顧客情報/アカウントを管理し各プロダクトからログイン廻り共通で使われる部分です。 明確にビジネス側と紐づくチームは現在ありません。

放置されたマイクロサービス

マイクロサービス群雄割拠時代に取り残され、メンテナンスがほぼ入っていないマイクロサービスも存在しています。 プロダクト共通で使われている機能となっており、プロダクト都合の変更を入れづらい状況となっています。

一般的に1マイクロサービス内に複数ドメインの都合が染み出していくことはアンチパターンで、vivitでもこの考え方に従っています。 障害ドメイン分割、リリースの独立性の面もありますが、何より所有者不在かつ継続的に開発が入らないマイクロサービスに対してこのようなことを行うことは、 各プロダクトの開発要件を常に共通システムとして設計する必要があり、現状のvivit開発部でもアンチパターンとしています。

まとめ

設計する上での前提条件や、運用経験を並べてみましたが、マイクロサービスをどう適用しようか少しでもイメージ付きましたか?

vivit でのマイクロサービス構成は、基本的なマイクロサービスの考え方にプラスし、特徴は以下になっています。

  • 1チーム複数マイクロサービスを所有
  • ソースのレポジトリはチーム毎にモノレポ
  • マイクロサービス間でも技術的統一性を保つ
  • 新規マイクロサービス作成は慎重傾向
    • 基本はプロダクト毎、フロントエンド/バックエンド毎(開発が企画される部署単位)
  • 常にメンテナンス(開発)されるマイクロサービスとしていく
  • 放置マイクロサービスは引き続き放置
    • 機能追加などしたい場合はその機能をプロダクト所有マイクロサービスに移行することを検討
  • 会員管理のマイクロサービスのように所有者不在も一部許容
    • 必要に応じて各プロダクトチームの担当者が開発を行う
  • プロダクト毎のBFF
  • Kubernetes (GKE)
  • gRPC による同期的通信
  • バッチ処理 (CronJob)
  • [構想段階] ログ出力などマイクロサービス共通で使うもののライブラリ化
    • マイクロサービス毎の品質均一化
    • 開発効率アップ
    • 放置マイクロサービスを否とし、常にメンテナンスされる状況を是としており、共用だがエンジニアの目につくところ配置でき、この方式はvivitとしてアリ

コンウェイか逆コンウェイかと言われたら、コンウェイの法則に従っていると思います。 vivit の場合、複数事業があり1チームのエンジニアが最小限なことや、継続的なプロダクト開発をしつつ、開発リソースを集中させたプロジェクト的な開発も行われるところも、システムアーキテクチャ設計を難しくしています。しかし、それが面白いところでもあります。暇しないですね笑

少しでも気になったら、話を聞きに来てください!
いつでも歓迎します!

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

Apollo で複数の GraphQL エンドポイントに接続する(+ Shopify Storefront API)

はじめに

フロントエンドエンジニアの関(@kur0buchi)です。

今回は自分の担当しているhinataレンタルで Shopify アプリケーションと連携する必要ができ Storefront API を使う事になりました。

hinata-rental.me

hinataレンタルを始めとした弊社各サービスでは元々バックエンドとの通信に GraphQL を使っており、本記事の趣旨としては新しく Storefront API の GraphQL エンドポイントに接続するために調査した結果となります。

TR;DL

ApolloLink.split メソッドを使うと、もう一つ GraphQL エンドポイントを使うことができる

www.apollographql.com

前提

GraphQL クライアントは Apollo となります。 また弊社フロントエンド環境(Next.js + TypeScript)での実装例になりますので、適宜読み替えてください。

要件としてシステム内のごく一部での連携のためこの方法を採用しておりますが、「普段から複数エンドポイントを使い分けている」「3つ以上のエンドポイントと連携する必要がある」場合には適用に際して再考の余地があるかと思われますのでご了承ください。

また Shopify Storefront API については必要最低限触れるのみで詳細な説明は省きます。参考とさせていただいたリンク先のブログ等をご参照ください。

okalog.info

準備

Shopify Storefront APIトークンを発行する

必要なアクセススコープ(権限)の設定を忘れずに行いましょう

動作確認

X-Shopify-Storefront-Access-Token ヘッダーに発行したアクセストークンを付与し、クエリを実行します。

権限に応じて欲しいデータが取得できていること、権限外のデータを取得する際はエラーになることが確認できます。

自分の作業環境では Altair GraphQL Client を愛用しています。

実装

Apollo クライアントを初期化する際に主要エンドポイントと連携用エンドポイントを定義し、 ApolloLink.split の第一引数に条件を指定します。

+  const primaryLink = new HttpLink({
+    uri: `${PRIMARY_API_URL}`,
+  });

+  const secondaryLink = new HttpLink({
+    uri: `${SECONDARY_API_URL}`,
+    headers: {
+      "X-Shopify-Storefront-Access-Token": SHOPIFY_STOREFRONT_TOKEN,
+    },
+  });

  const client = new ApolloClient({
-    link: new HttpLink({
-      uri: `${API_URL}`,
-    });
+    link: ApolloLink.split(
+      (operation) => operation.getContext().clientName === "secondary",
+      secondaryLink,
+      primaryLink
+    ),
  });

呼び出す際には初期化時に指定した clientNamecontext オプションに指定します。 いつものエンドポイントを使う際にはコードに変更が起こらない点も良いですね。

  • primaryLink
  const { data, loading, error } = useXxxQuery();
  • secondaryLink
  const { data, loading, error } = useXxxQuery({ context: { clientName: "secondary" } });

上記サンプルコードは graphql-codegen の生成物を想定しています、適宜読み替えてください。

終わりに

今回は一部分だけの適用を想定して比較的変更量の少ない実装で対応をするつもりですが、この調査を通じて ApolloLink 周りのまだまだ知らないオプションがあったりしたので深堀りしていけたらと思います。

vivit株式会社では先端技術を深堀りしていける仲間を募集しています。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

参考