ReactによるVue.js v2プロジェクトのリプレイス〜基本API からライフサイクルまで〜

React によるVue.js v2プロジェクトのリプレイス〜基本APIからライフサイクルまで〜

はじめに

この記事はPLEX Advent Calendar 2024の 13 日目の記事です。

Vue.js v2 のサポートが 2023 年 12 月 31 日に終了した事もあり、Vue.js/Nuxt v2のプロダクトをReact/Next.jsへリプレイスする作業を行いました。

その際に Vue.js v2 と React の記法の違いについて少し戸惑うところがあったため、本記事では、Vue.js v2 の基本的な APIdatamethodscomputedなど) を React ではどのように置き換えるかについて、コード例を交えて解説します。

主要な API の置き換え方法

Vue.js v2 の主要 API を React でどのように置き換えるかについて、具体的な例を見ていきましょう。

1.data

Vue.js v2 のdataはリアクティブな状態(オブジェクトのデータ)を返す関数です。 ここで定義されたデータはリアクティブなものとして扱われます。

Vue.js v2

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue!",
    };
  },
};
</script>

React

React ではuseStateを用いて置き換えます。

import React, { useState } from "react";

function App() {
  const [message, setMessage] = useState("Hello React!");

  return (
    <div>
      <p>{message}</p>
    </div>
  );
}

export default App;

2.methods

Vue.js v2 のmethodsコンポーネント内で使用されるメソッドを定義するオブジェクトです。 ユーザーのイベント(クリックなど)に対応する際や、データ操作のロジックを持つ関数などを定義します。

Vue.js v2

<template>
  <div>
    <button @click="sayHello">Click me</button>
  </div>
</template>

<script>
export default {
  methods: {
    sayHello() {
      alert("Hello Vue!");
    },
  },
};
</script>

React

React ではコンポーネント内で関数を定義します。

import React from "react";

function App() {
  const sayHello = () => {
    alert("Hello React!");
  };

  return (
    <div>
      <button onClick={sayHello}>Click me</button>
    </div>
  );
}

export default App;

3.computed

Vue.js v2 のcomputedは依存しているデータに基づいて計算されるプロパティを定義するオブジェクトです。 ここに定義したデータはキャッシュされて、依存するデータが変更された場合にのみ再計算されます。

Vue.js v2

<template>
  <div>
    <p>{{ reversedMessage }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue!",
    };
  },
  computed: {
    reversedMessage() {
      return this.message.split("").reverse().join("");
    },
  },
};
</script>

React

React ではuseMemoで返す値をメモ化します。

import React, { useState, useMemo } from "react";

function App() {
  const [message, setMessage] = useState("Hello React!");

  // messageが更新された時のみ再計算
  const reversedMessage = useMemo(() => {
    return message.split("").reverse().join("");
  }, [message]);

  return (
    <div>
      <p>{reversedMessage}</p>
    </div>
  );
}

export default App;

4.props

Vue.js v2 のpropsは親コンポーネントから子コンポーネントにデータを渡すためのプロパティを定義するオブジェクトです。 子コンポーネントpropsで宣言されたプロパティを通じて、親から渡されたデータにアクセス可能です。

Vue.js v2

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  name: "Message",
  // 親からのデータ(:message="Hello Vue!")の箇所を受け取る
  props: {
    message: {
      type: String,
      required: true,
    },
  },
};
</script>
<template>
  <div>
    <Message :message="Hello Vue!" />
    <Message :message="Hello Vue v2!" />
  </div>
</template>

<script>
import Message from "./Message.vue";

export default {
  name: "App",
  components: {
    Message
  }
};
</script>

React

React ではコンポーネントの引数にpropsを渡します。

import React from "react";

// 子コンポーネント
function Message({ message }) {
  return <p>{message}</p>;
}

// 親コンポーネント
function App() {
  return (
    <div>
      <Message message="Hello React!" />
    </div>
  );
}

export default App;

5.emit

Vue.js v2

Vue.js v2 のemitは子コンポーネントが親コンポーネントにイベントを発火させたりデータを渡すための機能です。

<template>
  <div>
    <button @click="sendMessage">Click me</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      // イベント名と親コンポーネントに送信する値
      this.$emit("onClickMessage", "Hello from child!");
    },
  },
};
</script>
<template>
  <div>
    <!-- @onClickは子のemitのイベント名 -->
    <Button @onClickMessage="getMessage" />
    {{ message }}
  </div>
</template>

<script>
import Button from "./Button.vue";

export default {
  name: "App",
  components: {
    Button,
  },
  data() {
    return {
      message: "",
    };
  },
  methods: {
    // value = 子コンポーネントから受け取る値(Hello from child!)
    getMessage(value) {
      this.message = value;
    },
  },
};
</script>

React

React では親コンポーネントから子コンポーネントpropsとしてイベントハンドラを渡すことで親にイベントを発火できます。

import React from "react";

function Button({ onClickMessage }) {
  const sendMessage = () => {
    onClickMessage("Hello from child!");
  };

  return <button onClick={sendMessage}>Click me</button>;
}

function App() {
  const handleMessage = (message) => {
    console.log(message); // "Hello from child!"
  };

  return (
    <div>
      <Button onClickMessage={handleMessage} />
    </div>
  );
}

export default App;

6.watch

Vue.js v2 のwatchは特定のデータの変更を監視し、変更時に特定の関数を実行するオブジェクトです。 データが変更されたときに別の処理を実行したい場合に役立ちます。

Vue.js v2

<template>
  <div>
    <!-- 入力値とVueインスタンスのデータを同期 -->
    <input v-model="message" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "",
    };
  },
  watch: {
    // 監視したいデータ名(message)
    message(newVal, oldVal) {
      console.log(`Message changed from ${oldVal} to ${newVal}`);
    },
  },
};
</script>

React

React ではuseEffectの引数の依存配列箇所に監視する state(ここでは message)を指定します。

import React, { useState, useEffect } from "react";

function App() {
  const [message, setMessage] = useState("");

  useEffect(() => {
    console.log(`Message changed to ${message}`);
  }, [message]);

  return (
    <div>
      <input value={message} onChange={(e) => setMessage(e.target.value)} />
    </div>
  );
}

export default App;

7.ref

Vue.js v2 のrefコンポーネント内の DOM 要素や子コンポーネントに直接アクセスするための機能です。

Vue.js v2

<template>
  <div>
    <!-- refの定義 -->
    <input ref="myInput" />
    <button @click="focusInput">Focus Input</button>
  </div>
</template>

<script>
export default {
  methods: {
    focusInput() {
      // inputのDOMにアクセス
      this.$refs.myInput.focus();
    },
  },
};
</script>

React

React ではuseRef を用います。

import React, { useRef } from "react";

function App() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

export default App;

ライフサイクルフックの置き換え方法

Vue.js v2 のライフサイクルフックは、Vue インスタンスの生成から破棄までの一連の間に実行される関数のことです。

React では主にuseEffectを使用することで同等の機能が実現可能です。

1.mounted

インスタンスが DOM 要素にマウント(描画)された後に呼ばれる処理です。

Vue.js v2

<script>
export default {
  mounted() {
    console.log("mounted");
  },
};
</script>

React

React ではuseEffectを使用して依存配列を空にすることで、マウント後に一度だけ実行されるようになります。

import React, { useState, useEffect } from "react";

function App() {

  useEffect(() => {
    console.log("mounted");
  }, []);

  return (
    // 略
  );
}

export default App;

2.updated

データが変更・更新され DOM が再レンダリングされた後に毎回呼ばれる処理です。

Vue.js v2

<script>
export default {
  updated() {
    console.log("updated");
  },
};
</script>

React

React ではuseEffectを使用して第二引数の依存配列を無しにすることでstateが変更された後に毎回実行されます。

import React, { useState, useEffect } from "react";

function App() {
  const [message, setMessage] = useState("Hello React!");

  useEffect(() => {
    console.log("updated");
  });

  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setMessage("Updated message!")}>
        Update Message
      </button>
    </div>
  );
}

export default App;

3.destroyed

インスタンスが破棄された後に呼び出されるフックです。

Vue.js v2

<script>
export default {
  destroyed() {
    console.log("destroyed");
  },
};
</script>

React

React ではuseEffectのクリーンアップ関数を使用します。

import React, { useState, useEffect } from "react";

function App() {
  const [message, setMessage] = useState("Hello React!");

  useEffect(() => {
    // クリーンアップ
    return () => {
      console.log("destroyed");
    };
  }, []);

  return (
    <div>
      <p>{message}</p>
    </div>
  );
}

export default App;

ディレクティブの置き換え方法

Vue.js v2 のディレクティブはv-から始まる独自の HTML 属性です。 値には JavaScript の式や data、メソッドなどを定義して、属性値の変化による副作用をリアクティブに DOM に適用します。

1. v-if, v-else-if, v-else

Vue.js v2 のv-if, v-else-if, v-elseは条件付きで要素のレンダリングを制御します。

Vue.js v2

<template>
  <div>
    <p v-if="isLoggedIn">ログイン済みです。</p>
    <p v-else-if="isGuest">ゲストとして閲覧中です。</p>
    <p v-else>ログインしてください。</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isLoggedIn: false,
      isGuest: true,
    };
  },
};
</script>

React

React では三項演算子を JSX 内で使用します。

import React from "react";

function App() {
  const isLoggedIn = false;
  const isGuest = true;

  return (
    <div>
      {isLoggedIn ? (
        <p>ログイン済みです。</p>
      ) : isGuest ? (
        <p>ゲストとして閲覧中です。</p>
      ) : (
        <p>ログインしてください。</p>
      )}
    </div>
  );
}

export default App;

2. v-show

v-showCSSdisplayプロパティで要素の表示/非表示を制御します。

Vue.js v2

<template>
  <div>
    <p v-show="isVisible">この要素は表示されています。</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isVisible: true,
    };
  },
};
</script>

React

React では、条件によってstyle属性を変更します。

import React from "react";

function App() {
  const isVisible = true;

  return (
    <div>
      <p style={{ display: isVisible ? "block" : "none" }}>
        この要素は表示されています。
      </p>
    </div>
  );
}

export default App;

3. v-for

v-forは配列やオブジェクトをループして、要素を繰り返し描画します。

Vue.js v2

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">{{ item }}</li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      items: ["リンゴ", "バナナ", "オレンジ"],
    };
  },
};
</script>

React

React では、mapメソッドを使用します。

import React from "react";

function App() {
  const items = ["リンゴ", "バナナ", "オレンジ"];

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

export default App;

4. v-model

v-modelはフォーム要素とデータの双方向バインディングを実現します。 下記では入力に連動して data の message に入力内容が追加されます。

Vue.js v2

<template>
  <input v-model="message" />
</template>

<script>
export default {
  data() {
    return {
      message: "",
    };
  },
};
</script>

React

React ではuseStateを使い、value属性とonChangeイベントを連携させます。

import React, { useState } from "react";

function App() {
  const [message, setMessage] = useState("");

  return <input value={message} onChange={(e) => setMessage(e.target.value)} />;
}

export default App;

5. v-bind

v-bindは様々な HTML の属性やコンポーネントのプロパティにデータを動的にバインド出来ます。

Vue.js v2

<template>
  <img v-bind:src="imageSrc" />
  <img :src="imageSrc" /><!-- 「:属性名」で省略記法となる -->
</template>

<script>
export default {
  data() {
    return {
      imageSrc: "https://picsum.photos/id/237/200/300",
    };
  },
};
</script>

React

React では、プロパティをそのまま JSX で指定します。

import React from "react";

function App() {
  const imageSrc = "https://picsum.photos/id/237/200/300";

  return <img src={imageSrc} />;
}

export default App;

6. v-on

v-on はイベントリスナーを登録する際に使用されます。

Vue.js v2

<template>
  <button v-on:click="handleClick">クリック</button>
  <button @click="handleClick">クリック</button
  ><!-- 「@イベント名」で省略記法となる -->
</template>

<script>
export default {
  methods: {
    handleClick() {
      alert("クリックされました");
    },
  },
};
</script>

React

React では、イベントハンドラーを直接指定します。

import React from "react";

function App() {
  const handleClick = () => {
    alert("クリックされました");
  };

  return <button onClick={handleClick}>クリック</button>;
}

export default App;

おわりに

今回はVue.js v2 の基礎機能をReactで書き直す方法についてご紹介しました。

実際にはリプレイスする規模感によって両者の記法を参考にしただけでは事足りなくなるかと思いますが、 同じような状況に立つ方の第一歩として参考にしていただければ幸いです。

dev.plex.co.jp

参考文献

v2.vuejs.org

ja.react.dev

ISUCON14で入賞した初出場の新卒チームが取り組んだこと

この記事は、 PLEX Advent Calendar 2024の11日目の記事です。

はじめに

2024年に株式会社プレックスにエンジニアとして新卒入社した佐藤祐飛(@yuhi_junior)と申します。

ISUCON14にチーム「黒酢唐揚げサン丼」として出場し、初出場で30位入賞することができました。

順位:     30位 / 834チーム
得点:     24464点

得点推移

全チームスコア

ISUCONはつよつよエンジニアが多く参加するので準備なしに好成績を収めることがとても難しいコンテストです。しかし、私たちはチームで入念に準備することで初出場の新卒チームであっても入賞することができました。

本番のチューニング記録は他の上位のチームに任せて、本ブログでは、ISUCON本番に向けてチームで取り組んだことについて紹介したいと思います。ISUCONを勉強し始めた方や、入賞を狙っている方に読んでいただけたら嬉しいです!

自分やチームについて

2023年の8月からインターンを開始しました。webエンジニアとしての経験年数は1年4ヶ月になります。

ISUCON13にテックリード@naro143さんが参加していたことをきっかけにISUCONの存在を知りました。当時、ISUCON本を買って読んでみましたが何も理解できなかったことを鮮明に覚えています笑。

新卒入社した後、日本CTO協会主催のISUCON研修に参加させていただくことが決まりました。ISUCONに興味があったのと本気で勝ちたかったので入念に準備をしました。その結果、見事優勝することができました。この時にISUCONの基礎体力をつけることができました。

product.plex.co.jp

今回のチームは、ISUCON研修で1位と2位だったメンバーで出場しました。しかし、全員がISUCON初出場の新卒1年目のチームでした。

チームで取り組んだこと

ISUCONに参加する上でチーム方針としてやりたいことがざっくり3つありました。

  1. 高い基準値(継続的にモチベーションを保つため)
  2. 知見の共有(学習コスト軽減 & 学んだ知識をISUCON以外にも活かすため)
  3. コミュニケーションを積極的に取る

以下に具体的に取り組んだことを紹介します。

現在地点の把握と目標設定

この研修で自信がついていたのでチーム全体に「100位くらいならいけるっしょ」というムードが漂っていました。しかし、本番2ヶ月前にISUCON13の問題を解いたところまさかの本番377位相当のスコアで「このままではヤバイ」という危機感を感じました。ここから僕たちのISUCONが始まります。

初めに100位以内を目標(本番1週間前に目標を50位に上方修正しました)として設定し、100位以内を目指す上で自分たちに足りないものを入念に話し合いました。目標が明確化されているからこそ、チーム全体で勉強するべき内容や本番で取り組むチューニングに優先順位をつけることができました。

例えば、以下の勉強するべき内容があるとします。

  • 複数リクエストを跨いだバッチ処理の実装(ISUONC11予選 postIsuCondition関数より)
  • N+1を改善する実装

前者の実装を行うと点数が伸びるのですが、ISUCON11予選のブログを読む限りTOP10位レベルのチームのみが実装しています。逆に後者は必ずと言って良いほど出題される実装です。100位を達成するためには前者を練習するべきかと言えばNoで、後者の実装を正確に短くできるように反復練習するべきと言えます。ISUCONにおいて入賞したいのであれば、目標から逆算して練習することが何よりも大事です。

余談ですが、前者の実装はISUCON14のPOST /api/chair/coordinateにおいてもできたはずなのですが、チームメンバーで過去問の実装共有が出来てなかったことにより改善ができなかったです泣。目標設定からの逆算は突き抜けた上振れを引けなくなるというデメリットもあるのだと痛感しました。

github.com

ISUCON練習問題の作成

ISUCON過去問から部分的に練習問題を抽出するという形で練習問題をいくつか作成しました。これを行った理由は3点あります。

  • 練習のサイクルを素早く回すことができる
  • 改善を思いつく思考の再現性をもたせる
  • チームメンバーが実装することができる基準値を作る

本番中は基本的に私がチューニング箇所を割り振っていく形で進めていたのですが、3つ目の基準値があったおかげで実装の成功確率を大まかに見積もることができました。

では、実際に利用した問題を紹介したいと思います。

ISUCON9予選 練習問題

getTransactions関数にて5N+1が発生している。この関数のチューニングを行いたい。以下の問の改善を入れて、ベンチが通るか検証してください。 https://github.com/isucon/isucon9-qualify/blob/00ee4c7f793ace5f4615cf9fb5570d2156c7e6a1/webapp/go/main.go#L853

問1 getUserSimpleByID, getCategoryByIDをキャッシュによって負荷を減らしてください

問2 transactionEvidenceとshippingのクエリをjoinで一つにまとめて、itemsループの外にてIN句でフェッチしてください。

問1解答例

var (
    // 各リクエスト(スレッド)にまたがるデータ競合が発生するのでsync.Mapを利用
    userSimpleMapByID sync.Map
)

func getUserSimpleByID(q sqlx.Queryer, userID int64) (userSimple UserSimple, err error) {
    user := User{}
    if v, ok := userSimpleMapByID.Load(userID); ok { 
        // キャッシュがあれば利用する
        user = v.(User)
    } else {
        // キャッシュがなければクエリを叩く
        err = sqlx.Get(q, &user, "SELECT * FROM `users` WHERE `id` = ?", userID)
        if err != nil {
            return userSimple, err
        }

        // キャッシュをセットする
        userSimpleMapByID.Store(userID, user)
    }

    userSimple.ID = user.ID
    userSimple.AccountName = user.AccountName
    userSimple.NumSellItems = user.NumSellItems
    return userSimple, err
}

// userについて挿入、更新、削除している箇所全てでキャッシュを削除する処理を入れる
userSimpleMapByID.Delete(userID)

var (
    // categoryについて挿入、更新、削除が存在しないのでデータ競合は発生しない
    categoryByID map[int64]*Category
)

// categoryについて挿入、更新、削除が存在しないので初期データを全て反映させるだけで良い
func postInitialize(w http.ResponseWriter, r *http.Request) {
    ...
    categories := []Category{}
    err = dbx.Select(&categories, "SELECT * FROM `categories`")
    if err != nil {
        log.Print(err)
        outputErrorMsg(w, http.StatusInternalServerError, "db error")
        return
    }
    for _, category := range categories {
        categoryMapByID[category.ID] = &category
    }

    for _, category := range categories {
        if category.ParentID != 0 {
            categoryMapByID[category.ID].ParentCategoryName = categoryMapByID[category.ParentID].CategoryName
        }
    }
    ...
}

// キャッシュの値を返す
func getCategoryByID(q sqlx.Queryer, categoryID int) (category Category, err error) {
    return *categoryMapByID[categoryID], err
}

問2解答例

// transactionEvidenceとshippingのjoin用に構造体を定義
type TransactionEvidenceShipping struct {
    ItemID                    int64  `db:"item_id"`
    TransactionEvidenceID     int64  `db:"transaction_evidence_id"`
    TransactionEvidenceStatus string `db:"transaction_evidence_status"`
    ShippingStatus            string `db:"shipping_status"`
}

func getTransactions(w http.ResponseWriter, r *http.Request) {
    ...
    // IN句用にitemIDのスライスを作成
    itemIDs := make([]int64, len(items))
  for _, item := range items {
      itemIDs = append(itemIDs, item.ID)
  }

    // transaction_evidencesの各レコードの存在は保証したいのでLEFT JOINにしている
  q, params, err := sqlx.In("SELECT te.item_id AS item_id, te.id AS transaction_evidence_id, te.status AS transaction_evidence_status, s.status AS shipping_status FROM transaction_evidences te LEFT JOIN shippings s ON te.id = s.transaction_evidence_id WHERE te.item_id IN (?)", itemIDs)
  q := dbx.Rebind(q)
  transactionEvidenceShippings := []TransactionEvidenceShipping{}
  err = tx.Select(&transactionEvidenceShippings, q, params...)
  if err != nil {
      log.Print(err)
      outputErrorMsg(w, http.StatusInternalServerError, "db error")
      return
  }
 
    // itemのループで対応するtransactionEvidenceShippingを取得するためにmapを作成
  transactionEvidenceShippingMapByItemID := make(map[int64]TransactionEvidenceShipping)
  for _, transactionEvidenceShipping := range transactionEvidenceShippings {
      transactionEvidenceShippingMapByItemID[transactionEvidenceShipping.ItemID] = transactionEvidenceShipping
  }

    itemDetails := []ItemDetail{}
  for _, item := range items {
        ...
      transactionEvidenceShipping := transactionEvidenceShippingMapByItemID[item.ID]
 
      if transactionEvidenceShipping.ItemID > 0 {
          itemDetail.TransactionEvidenceID = transactionEvidenceShipping.TransactionEvidenceID
          itemDetail.TransactionEvidenceStatus = transactionEvidenceShipping.TransactionEvidenceStatus
          itemDetail.ShippingStatus = transactionEvidenceShipping.ShippingStatus
      }
 
      itemDetails = append(itemDetails, itemDetail)
  }
  ...
}

ISUCON11予選 練習問題

GET /api/condition/:jia_isu_uuid エンドポイントがalpの結果より重いことがわかった。 またスロークエリとして 以下のクエリが上位に来ていた。以上の情報をもとにgetIsuConditions関数のチューニングを行なってください。https://github.com/isucon/isucon11-qualify/blob/1011682c2d5afcc563f4ebf0e4c88a5124f63614/webapp/go/main.go#L941

SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'aaaf8165-11eb-4903-b371-77b4d4512c66' AND `timestamp` < '2021-08-24 09:42:20'  ORDER BY `timestamp` DESC

解答例 getIsuConditions内で呼ばれているgetIsuConditionsFromDBのコードを読むと、クエリパラメータとして受け取ったcondition_levelに対応するISUのコンディションを取得していることがわかる。

ここで以下の箇所に注目すると、limit分だけのコンディションを取得すれば良く、スロークエリは無駄なレコードを取得していることがわかる。

if len(conditionsResponse) > limit {
    conditionsResponse = conditionsResponse[:limit]
}

しかし、condition_levelがDBテーブルに存在しないため、スロークエリにlimit句をつけることができない。

そこで、isu_conditionsテーブルにcondition_levelカラムを追加し、スロークエリ時点で必要なコンディションでWHERE条件をかけてlimitすることを考える。

コード例 github.com

知見をドキュメント化

各メンバーが学んだ知見をCosense(旧ScrapBox)にまとめました。CosenseはNotionなどの階層ベースのドキュメントツールとは異なり、相互リンクで繋がるので知識が定着しやすいです。チーム全体の学習時間を減らすことができたのと、本番中でも何度も参照したのでとても良かったです。

また、普段の業務でもISUCON練習で作成したページを振り返ることがあります。ISUCONの知識は業務でもよく役立ちます。例えば、実際にISUCON14で出題されたLAG関数、OVER ~ PARTITION BYを用いたSQLクエリを業務で書く機会がありました。

もくもく会

週3日でISUCONの勉強をするもくもく会を実施していました。勉強する時間を確保できたこと以外にも、チームで話し合いをする時間を多く取れたことがとても良かったです。

実装の境界についてチームで合意を取ることができたり、各メンバーが何を得意不得意としているのかを把握することができました。

もくもく会で勉強したことについてチームメンバーがブログを書いているのでこちらも参考にしてみてください

tech.every.tv

ペアプログラミング

本番では優先度が高い改善については部分的に僕とメンバー一人でペアプログラミングを導入していました。理由は僕の弱点として凡ミスをしやすいということがあるので、それを補うためです。

実際に本番中でも僕のミスによって沼っていた改善があるのですが、一度落ち着いてメンバーとペアプログラミングを始めることで改善することができました。本当にメンバーには感謝しかないです。

timesで思考を垂れ流す

他のチームのレポジトリやブログで学んだことをtimesにアウトプットしていました。メンバーに「知らないということ」を知ってもらい、モチベーションを刺激できたかなと思います。準備期間が長いとチーム全体で中弛みしがちなのでモチベーション維持する施策は大事だと思います。

NaruseJunのレポジトリを見た反応

輪読会を実施

日本CTO協会の新卒メンバーで輪読会を開催しているのですが、ISUCONに向けて@soudai1025さんの「RDBの正しい歩き方」を輪読しました。本の内容をさらに深ぼりしてチーム全体のDBへの理解を深めました。

インデックスの知識やトランザクション分離レベルについて普段の業務に生きる知識を身につけることができました。

輪読会についてのブログはこちら

最後に

ISUCON14に向けてチームで取り組んだことを紹介しました。

社会人の中で一番時間を使って練習したという自負があります(誕生日でさえもチームで一日練習して、ケーキすら食べてません笑笑)。エンジニア歴1年4ヶ月で入賞できたのでとても自信がつきました。

本番はチームメンバーにとても助けられたので本当に感謝の気持ちで一杯です。チームで掴み取った入賞だと思います。

順位発表直後の様子

最後になりますが、ISUCON14の運営の皆様、素晴らしいISUCON体験をありがとうございました。こんなにも熱くなれることは久しぶりでとても良い経験になりました。この結果に満足することなく打倒takonomura、NaruseJunを目指して日々精進していきたいと思います!

神田〜三越前周辺のおすすめランチ10選

はじめに

こんにちは、プレックスの石塚です。

この記事は、 PLEX Advent Calendar 2024 の10日目の記事です。 ようやくアドベントカレンダーのできる組織の規模になってきて嬉しい限りです!

ここまで9件の記事が公開されており、ほとんどが技術的なテーマの記事だったため、このあたりで技術の全く関係のないネタをぶち込んで行きたいと思います。

神田〜三越前周辺のおすすめランチ10選

今回の記事ではプレックスのオフィス近辺である神田〜三越前のエリアでランチ営業を行っているお店の中から、一芸に秀でているお店を紹介していきます。

記事を見てくれた方が、どれか1つでも行ってみたいという気持ちになって、実際に訪れていただけたら幸いです。

1. あげづき コレド室町テラス店

近辺でのとんかつNo. 1のお店です。社内では「とんかつ檍 日本橋店」の方が知名度が高いのですが、連れて行くとこっちの方がうまいじゃん、となります。

立地とランチでも最低2000円弱という価格帯からかいつもお店は空いているのですが、味は対象的にめちゃくちゃ美味いです。 肉質もいいのですが、衣がなんとも言えないサクサク感で最高です。

価格帯を考えると給料日後や今月は1ヶ月やりきったなというシーンで活用するのがオススメです。

tabelog.com

2. 雲林坊 秋葉原

神田周辺には汁なし担々麺のお店が多数あり、近辺でのランキングをつけようと食べ歩いていたのですが、その中でNo. 1に輝いたのがこのお店です。 かつては日本橋室町店という会社近くの店舗があったのですが、残念ながら閉店してしまいました。 オフィスからは徒歩10分強掛かるので頻繁には行けませんが、自分は通勤で秋葉原駅を使っているため、たまに帰り道で寄ることがあります。

辛さと痺れが5段階で選べるので、辛いものが得意でない方にもオススメです。

tabelog.com

3. もつ焼き煮込み鶴田

店内はこじんまりとした大衆居酒屋といった雰囲気ですが、ランチも人気です。 ランチメニューは生姜焼き、牛もつ煮込み丼、もつ鍋の3種類となっており、価格も¥1,000前後とリーズナブルです。

特に1人前からランチでも、もつ鍋が食べられるというのは中々貴重なお店になっていると思います。

tabelog.com

4. 欧風カレー ボンディ 神田小川町

カレー激戦区の神田でカレー屋さんを選ぶということで、何を選ぶかは非常に迷ったのですが、欧風カレーの「ボンディ」さんをチョイスしました。 洗練された味わいとスパイスのバランスが絶妙なカレーはここでしか食べられないのでは?と思ったのが選んだ理由です。 (個人的に欧風カレー推しというのもありますが...笑)

他にもインド系やスパイス系などいろいろな種類のカレーを提供するカレー屋さんのあるエリアなので、非常に開拓しがいがあります。 今回は紹介できなかったですが、スパイス系では「葡萄舎」さんがイチオシです!

tabelog.com

5. 香川 一福 神田店

食べログ100名店にも常連のさぬきうどんのお店です。コシのあるうどんを冷でいただくのが個人的には好きなのですが、この寒い季節には釜玉バターも捨てがたいですね。

人気店なので12時〜13時のピーク時には列ができていることが多いですが、回転が早いのでそこまで待たずに入れます。

tabelog.com

6. 串エ門

ランチでケンタッキーのフライドチキンくらいのどデカい唐揚げが6個まで同一料金というお店です。 前職が六本木で唐揚げ食べ放題のランチがあったのですが、それと同じような店を探し求めて行き着きました。

大食いしたい時にオススメです。

tabelog.com

7. 日本橋 墨之栄

コレド室町の2Fにある魚料理のお店です。 にしんや丸さばを炭火で焼いた原始焼きが名物で、価格は税込み¥1,200くらいからとなっています。

ランチ帯は混んでいる事が多いですが、席数も多いため、そこまで待ち時間は長くない印象です。

tabelog.com

8. PASTA FRESCA

プレックス社内では、ランチでパスタが食べたくなったらここと言われているお店です。 ランチメニューは税込み¥1,200でサラダとパスタのセットとなっています(以前はスープの提供もあったのですが、価格高騰の波に飲まれてなくなってしまいました)。

パスタはもちもちとした生パスタのような食感で満足感があり、LINEの友だち登録で大盛り or ドリンク1杯無料のサービスがあります。

tabelog.com

9. トラットリア・ジュゲム

神田駅すぐ近くのイタリアンです。 地下1階の非常にわかりにくい場所にあるためか知名度が低いのですが、こちらのお店でも美味しいパスタが食べられます。

パスタで2つお店選ぶのかよと突っ込まれそうですが、こちらのパスタは細麺で味付けが上手く、方向性の違うパスタになっています。 また、¥1,600でランチのショートコースがあり、ウェルカムランチでもオススメです(弊社だと1人¥1,500まで補助が出ます)。

tabelog.com

10. ラーメン 盛太郎

オフィス近辺で二郎系を食べたいとなった時に第1の選択肢になってくるのがこのお店です。 東京駅まで足を伸ばせば「豚山」があったり、神保町までいければ「ラーメン二郎 神田神保町店」があるのですが、ランチだと難しい距離感です。

その他にもオフィス周辺にいくつか二郎インスパイア系のお店はあるのですが、こちらが乳化〜微乳化のスープで1番パンチのある味だと思います。

tabelog.com

おわりに

お店の紹介は以上です。

実はプレックスは神田、三越前日本橋というエリアで3つのオフィスを構えています。しかしながら自分が普段働いているオフィスは神田や三越前寄りのため、まだまだ日本橋のエリアは開拓できていません。

来年は日本橋エリアのランチも開拓して、ブログで発信していきたいと思っているので、ご協力いただける方はぜひご応募よろしくお願いします。

dev.plex.co.jp

Semantic LoggerのログをDatadog用にカスタマイズしてみた

こんにちは、Plex Job開発チームの種井です。

この記事は、 PLEX Advent Calendar 2024の9日目の記事です。

DBクライアントツールはDataGripを使っています! よく使う機能はダイアグラム機能です。

背景・課題

過去の記事「Railsアプリケーションのログを構造化し、Datadogで活用するまで」の中で、Datadog Logs機能の仕様に合わせてログをフォーマットする方法として、Remapperを使用する方法を紹介しました。

ただ、前回紹介させていただいたRemapperを使用する方法だと、ログのフォーマットに関しての設定がアプリケーションコード(Formatter)とDatadog上の機能(Remapper)にまたがってしまうことになります。 そのため、Formatterの責務がアプリケーションコードと監視ツールに分散してしまい、二重管理による管理コストが懸念点としてありました。

Semantic Loggerには、ログ構造についてのドキュメントがあります。 当時、Semantic Loggerのログ構造にカスタム属性を含めたい場合はnamed_tagspayloadなどの属性にカスタム属性を含める必要があると考え、Remapperを使用して実装することにしました。 改めて調査したところ、Semantic LoggerのFormatterをカスタマイズする方法でも実現することが可能とわかったため、切り替えることにしました。

今回は、前回記事に続いて、Semantic LoggerのカスタムFormatterを作成し、ログをDatadog Logsのエラートラッキング機能で活用できる形にフォーマットするまでの流れについて書いていきたいと思います。

前提の確認

環境

  • Ruby: 3.3.6
  • Rails: 7.1
  • Semantic Logger
    • semantic_logger: 4.16
    • rails_semantic_logger: 4.17

Semantic Loggerとログのフォーマット

Semantic LoggerではAppenderという、ログを異なる出力先に送信するための機能があります。 Appenderには、ログの出力先やログのフォーマットを指定することができます。

例えば、ログをJSON形式でdevelopment.logに書き出したい場合は以下のようになります。

SemanticLogger.add_appender(file_name: "development.log", formatter: :json)

また、ログのフォーマットを独自のものにカスタマイズしたい場合は以下のように書くことができます。

class MyFormatter < SemanticLogger::Formatters::Color
  # Return the complete log level name in uppercase
  def level
    "#{color}log.level.upcase#{clear}"
  end
end

SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new)

いずれも公式ドキュメントから引用

SemanticLogger::Formatters モジュールのクラスを継承した独自のFormatterを実装し、add_appender の引数として渡すことで、期待する形にログをフォーマットすることができます。

やったこと

  1. Datadog Logs用のFormatterクラスを作成する
  2. Appenderに作成したFormatterクラスを設定する

1. Datadog Logs用のFormatterクラスを作成する

今回の目的は任意のカスタム属性(エラートラッキング用途)をSemantic Loggerのログ構造に追加することです。 Semantic loggerには、デフォルトで複数のFormatterが用意されていますが、Datadog用に対応するものはありませんでした。 ただ、同じ監視ツールであるNewRelicのFomatterがあったので、実装を見てみることにしました。

module SemanticLogger
  module Formatters
    class NewRelicLogs < Raw
      def call(log, logger)
        ...
        if hash[:exception]
          result.merge!(
            "error.message": hash[:exception][:message],
            "error.class":   hash[:exception][:name],
            "error.stack":   hash[:exception][:stack_trace].join("\n")
          )
        end
        ...
      end
    end
  end
end

https://github.com/reidmorrison/semantic_logger/blob/05a00e186c958ddd474285a37aa7e910aa5e9841/lib/semantic_logger/formatters/new_relic_logs.rb#L79

例外時にカスタム属性であるerror.xxx 属性を定義していることがわかります。

同じようにcallメソッドをオーバーライドし、任意の属性を定義していきます。 今回は、例としてapp/lib/log/formatters/datadog_formatter.rb というファイルを作成し、Datadog Logs用に以下のようなカスタムFormatterを作成しました。

※ エラートラッキング機能で要求されるデータ構造については、前回記事を参照ください

require 'rails_semantic_logger'

class DatadogFormatter < SemanticLogger::Formatters::Raw
  def call(log, logger)
    hash = super(log, logger)

    if log.level == :error
      hash = {
        **hash,
        error: {
          kind: log.name,
          message: log.exception&.message,
          stack: log.backtrace.join('\n')
        }
      }
    end

    hash.to_json
  end
end

2. Appenderに作成したFormatterクラスを設定する

カスタムFormatterができたので、設定ファイル(config/environments/任意の環境.rb)にAppenderとして設定していきます。

...
require_relative '../../app/lib/datadog_formatter'

Rails.application.configure do
  # 作成したDatadogFormatterを引数にして、Appenderを設定する
  config.semantic_logger.add_appender(io: $stdout, formatter: DatadogFormatter.new)
end

上記のような実装を行うことにより、無事にRemapperで再構成していたログを、カスタムFormatterでフォーマットすることができました。

まとめ

今回の取り組みにより、ログのフォーマットをアプリケーションコードに一元化し、設定漏れやTerraformのコードとの二重管理など、運用上コストを幾分か削減することができました。 Semantic Loggerには、標準で多様なAppenderが用意されています。 基本的には用途に合うものが見つかりそうですが、今回のようにないものもあるため、その場合は独自のFormatterを作成する必要があります。 その場合でも、用意されているものを参考にすることで、特につまづくこともなくカスタマイズできました。

おわりに

最後にはなりますが、Plex Jobだけでも今回のような監視や可観測性をはじめコスト最適化などの課題があったり、他事業部のプロダクトもGCPへのインフラ移行を控えていたりします。

このような課題にチャレンジしていただけるSREをはじめ、ソフトウェアエンジニアフロントエンドエンジニアを募集しています。

dev.plex.co.jp

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

Heroku で Cloud SQL を利用する方法

Heroku で Cloud SQL を利用する方法

この記事は、 PLEX Advent Calendar 2024の8日目の記事です。

はじめに

こんにちは。コーポレートチームの山崎です。
とある社内向けプロジェクトの要件として Heroku 上で Rails を動かしつつ、DB は Cloud SQL を利用する必要がありました。
設定に手こずったので、備忘録がてら手順をまとめようと思います。

Heroku で Cloud SQL を利用するには

Google Cloud SQL Proxy buildpack」というビルドパックを利用して実現できます。
仕組みとしては、Google が提供する Cloud SQL Auth Proxy をビルド時にダウンロードし、プロキシ経由で Cloud SQL との通信が可能になるという感じです。
あくまで、有志によって作成されたビルドパックなので、調査・検証した上で利用することをお勧めします。

elements.heroku.com

cloud.google.com

設定方法

README に手順が書かれていますが、若干の補足を加えて説明します。 github.com

BuildPacks の設定

  • Settings の Buildpacks に https://github.com/DanielZambelli/heroku-buildpack-cloud-sql-proxy を追加

環境変数の設定

  • GCLOUD_INSTANCE を追加し、Cloud SQL インスタンスの接続名を設定
  • GCLOUD_CREDENTIALS を追加し、base64 エンコードしたサービスアカウントキーを設定
    • サービスアカウントに必要な権限は下記ドキュメントを参照してください
# base64 エンコードの例
base64 -i path/to/key-file.json | pbcopy
  • DB指定用の環境変数(DATABASE_URL等)として postgres://<username>:<password>@localhost:5432/<database-name> を設定

cloud.google.com

起動時の設定

Web サーバー起動前にプロキシを起動させる必要があります。
起動コマンドの前に bin/run_cloud_sql_proxy &>null && を追加すれば OK です。

# Procfile の例
web: bin/run_cloud_sql_proxy &>null && bin/rails server

注意点

heroku run実行時

heroku run コマンドやスケジューラは、アプリケーションの Web Worker とは異なる dyno 上で実行されます。
そのため、起動コマンドと同様にプロキシを実行させる必要があります。

# 例
heroku run 'bin/run_cloud_sql_proxy & rails db:seed' -a app-name

既存DBとのリプレイス

Heroku Postgres を設定した場合、DATABASE_URL という環境変数が追加され、データベースのURLが設定されます。 
アプリケーションに Heroku Postgres が存在する限り、DATABASE_URL を書き換えることができないため、削除するか、他の環境変数を指定して利用する必要があります。
削除する場合は、必ずバックアップを取得しましょう!!!

The DATABASE_URL config var designates the URL of an app’s primary Heroku Postgres database. For apps with a single database, its URL is automatically assigned to this config var.

devcenter.heroku.com

リージョン

Heroku では通常のCommon Runtimeの場合、USリージョンもしくはEUリージョンのみが利用可能です。
東京リージョンにするにはPrivate Spaces Runtimeを利用する必要があります。
Cloud SQL インスタンスを地理的距離が遠いリージョンに配置していると、許容できないほどの遅延が発生する可能性があるので注意しましょう。

devcenter.heroku.com

cloud.google.com

まとめ

特殊な事情や要件がない限り、Heroku から Cloud SQL を利用するというパターンは珍しいかもしれませんが、同じ境遇の方々の参考になればと思います。
速度的な不満は特にありません。(管理は若干面倒)

さいごに

現在プレックスではソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーを募集しています。

とても働きやすい環境なので、一緒に働いてみたいと思った方がいましたら、是非ご連絡をお待ちしています!

dev.plex.co.jp

RailsのDelegated Typesで実現する柔軟なモデル設計

この記事は、 PLEX Advent Calendar 2024 の7日目の記事です。

はじめに

こんにちは、株式会社プレックスのコーポレートチームの金山です。

この記事では、Railsアプリケーションの設計に役立つ「Delegated Types」について解説したいと思います。

「Delegated Types」がどのような課題を解決するのか、使い方やメリット・デメリット、実際に使ってみた感想をシェアしていきます。

背景と課題

とあるプロジェクトの開発で、通話履歴を管理する機能を実装することになりました。

通話には「企業への通話」と「個人への通話」の2つがあります。

これら2つの異なるモデルを画面にリスト表示し、ページネーション、ソート、検索機能を提供する必要がありました。

Railsでは異なるモデルを一緒に扱うことが難しいため、この課題を解決する方法として「単一テーブル継承」と「Delegated Types」を検討しました。

単一テーブル継承(STI)とは

STIは、複数のモデルを単一のテーブルで管理する方法です。

モデルの種類を識別するための型情報「type」カラムを持たせます。

今回の通話履歴で例えると、企業への通話履歴ならtypeが「CompanyCall」、個人への通話履歴ならtypeが「UserCall」となります。

STIを利用すると、異なるモデルのデータを1つのテーブルで管理できるため、検索や操作が簡単になります。

考慮すべき点

ひとつのテーブルにサブクラス固有の属性も含まれるため、未使用カラムが多くなりテーブルが肥大化する可能性があります。

また、モデルごとに異なるカラムにNOT NULL制約を適用できないため、データ整合性のチェックが難しくなります。

Delegated Typesとは

Delegated Typesは、STIの問題を解決するためのアプローチです。

共通の属性を親テーブルに保存し、固有の属性は別の子テーブルに分けて管理します。

例えば、今回の通話履歴の場合、通話日時などの共有属性はCallテーブルで持ち、モデルごとに異なる属性はサブテーブルで保持します。

親子の関連付けは◯◯able_typeと◯◯able_idで紐付けます。

これにより、単一のテーブルで、すべてのサブクラス間で不必要に共有される属性を定義する必要がなくなります。

考慮すべき点

テーブルを分けることで、複数テーブル間の結合が必要になりクエリのパフォーマンスが低下する場合があります。

また、レコード作成や削除処理では親子テーブルで同時に行う必要があり、パフォーマンスに影響します。

テーブルが分かれることから実装の複雑になるため、各テーブルの構造や関連付けを明確に設計する必要があります。

Delegated Typesの使い方

まずはマイグレーションファイルを作成します。

callableというポリモーフィック関連を定義します。

ポリモーフィック関連を定義する場合、◯◯ableが慣例的な命名規則です。

これを書くことでcallable_typeとcallable_idカラムが自動的に追加されます。

class CreateCalls < ActiveRecord::Migration[7.1]
  def change
    create_table(:calls, comment: '通話テーブル') do |t|
      t.references(:callable, polymorphic: true, null: false)
      t.datetime(:called_at, null: false, comment: '通話日時')
      t.integer(:duration, comment: '通話時間')

      t.timestamps
    end
  end
end

次にモデルの設定です。

class Call < ApplicationRecord
  delegated_type :callable, types: ['UserCall', 'CompanyCall'], dependent: :destroy
  accepts_nested_attributes_for :callable
end
class UserCall < ApplicationRecord
  has_one :call, as: :callable, touch: true, dependent: :destroy
  belongs_to :user
end
class CompanyCall < ApplicationRecord
  has_one :call, as: :callable, touch: true, dependent: :destroy
  belongs_to :company
end

マイグレーションとモデルを設定したら準備完了です。

ここからはどのように使用するのかを見ていきます。

作成方法

新しいCallオブジェクトを作成する際に、Callableサブクラスを同時に指定できます。

以下のようにするとCallsとUserCallsテーブルにそれぞれレコードが作成されます。

  Call.create!(
    assignee_user_id: User.ids.sample,
    called_at: Faker::Time.backward(days: 30),
    duration: Faker::Number.between(from: 1, to: 100),
    callable: UserCall.new(user_id: User.ids.sample)
  )

取得方法

Callモデルを使用して通話一覧を取得できます。

Call.user_callsとすれば、UserCallのCallのみ取得可能です。

注意点として親から子の属性を参照することはできません。

委任先の情報を取得するにはcallableを呼び出す必要があります。

Call.order(called_at: :desc).limit(10)

Call.user_calls

call = Call.find(1)
callable = call.callable

更新方法

Callモデルにaccepts_nested_attributes_for :callableを定義することで、Callableサブクラスを同時に更新できます。

親と子のレコードが同じトランザクションで更新されます。

class Call < ApplicationRecord
  delegated_type :callable, types: ['UserCall', 'CompanyCall'], dependent: :destroy
  accepts_nested_attributes_for :callable
end
call = Call.find(1)

call.update(
  called_at: Time.now,
  duration: Faker::Number.between(from: 1, to: 100),
  callable_attributes: { user_id: User.ids.sample }
)

削除方法

削除はシンプルにdestroyを呼び出します。

親と子のレコードが同じトランザクションで削除されます。

call = Call.find(1)
call.destroy

結果と感想

今回のプロジェクトでは、各通話ごとに異なる属性を持つことを考慮し、Delegated Typesを採用しました。

その結果、共通部分を明確にしつつ、通話ごとに異なる外部キーにNOT NULL制約をつけることで、データの整合性を保つことができました。

しかし一方で、ActiveRecordでの操作がやや煩雑になり、STIと比べてシンプルな操作がしづらい場面がありました。

どちらの方法を採用するかはプロジェクトの要件次第です。それぞれのメリットとデメリットを考慮した上で選択することが大切だと感じました。

プレックス入社!コーポレートチームで頑張ります!

はじめに

はじめまして、プレックスの前川と申します。

2024年11月に株式会社プレックス(以下、プレックス)にエンジニアとして入社しました。

入社して2週間と少しですが、自己紹介を兼ねて入社経緯や入社してからの感想などをまとめておきたいと思います。

一人でも多くの方にプレックスに興味を持っていただけると嬉しいです。

目次

  • 自己紹介
  • プレックスに入社した理由
  • 入社してからの感想
  • 最後に

自己紹介

私の経歴は以下の通りです。

年月 経歴
2016年5月 某個別指導塾企業へ入社。 教室長と教室売却をしていました。
2022年2月 ポテパンキャンプでRuby on Railsを主に学習
2022年8月 永産システム開発株式会社に入社
2024年11月 株式会社プレックスに入社

教室長時代は赤字を出している上手くいってない直営の教室に配属されて立て直しをしました。 黒字化したら学習塾を経営したいと考えている企業、個人の方に売却する事業です。

新規事業で前例がなかったのですが、立て直し自体に苦戦しているメンバーの中で、 唯一2教室売却する成果を出してましたので、向いていました。

そんな中、エンジニアに転向したきっかけは、教室にプログラミング授業が導入された事でした。 プログラミング自体の楽しさ、エンジニアの働き方などに憧れがあり、退職してからポテパンキャンプに通い始めました。

永産システム開発株式会社では、フロントエンド、バックエンド問わず幅広く開発に携わりました。 PMをする機会もあり、受託・請負の案件を上流〜下流まで経験できたことは大変勉強になりました。 2年弱ほどお世話になった後、プレックスに転職しました。

プレックスでは開発本部のコーポレートチームに所属し、社内で使用するシステムの開発・カスタマイズに従事しています。

プレックスに入社した理由

コーポレートエンジニアをやりたい!

コーポレートエンジニアって何?という方は下記の記事を参照ください!

product.plex.co.jp

プレックスにおけるコーポレートチームは 「オペレーションの効率化によって事業成長に貢献する」 をミッションとしています。 赤字の教室運営で、「日常の業務フローが非効率かつ、再現性がない」が共通の課題としてあったので、オペレーションの効率化 = 事業成長という価値観が一致していました。 また、人とコミュニケーションを取りながら実装するのが好きなので、社内間での仕様の確認・調整も向いているなと考えました。

入社してからの感想

kintoneプラグイン・カスタマイズの開発環境が整っている

入社2週間で7つのプラグイン・カスタマイズを修正・実装しました。 今までkintoneのplugin・カスタマイズの実装をしたことはなかったので、タスクに取り組む前は不安もありました。実際やってみると、ほぼ躓くことなく入社2日目には1つ目のタスクが完了しました。 どんな開発環境なんだろう?って気になる方は是非カジュアル面談などで質問してください!

ビジネスサイドの方ともコミュニケーションを取りやすい

オペレーションの効率化が事業成長につながるという共通認識と結果があるので、開発環境に実装したものを現場の方が試してフィードバックを頂けたり、改善に向けてフラットに話せます!コミュニケーションコストが低いというのはとてもありがたいです。

今のコーポレートチームはいい感じに担当領域を分担できそうなので、 自分はkintone周りを中心に理解を深めて、チームに貢献して行ければいいなと考えています!

最後に

入社して日が浅いので業務の一連の流れを理解している段階ですが、シャッフルランチやポケポケ大会など事業部を跨いだエンジニア間の交流もあり、今まで経験のなかった業務にも携わり、実りのあるオンボーディング期間を過ごせています。

最後にはなりますが、現在プレックスではソフトウェアエンジニア、フロントエンドエンジニアの募集もあります。 本記事を読み少しでも共感する点があれば、是非ご連絡ください!

dev.plex.co.jp