RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

ダンボールワンのフロントエンドでゼロランタイムCSS in JSを採用してみた

ダンボールワンのフロントエンドでゼロランタイムCSS in JSを採用してみた

こんにちは、ラクスルの宮崎(@miyahkun)です。現在はラクスル株式会社の「ダンボールワン」という、ダンボールや梱包資材を扱うサービスの運用・開発を行っています。我々のチームではNext.jsを用いたサービス開発をしています。今回はその中で採用したvanilla-extractというツールについて紹介します。

背景と課題

我々がこのプロジェクトを始めた当初は、ラクスルでの採用実績が長いCSS Modulesの導入を検討していました。Next.jsのドキュメントで最初に記載されているのがCSS Modulesであり、当時は強く推奨されていたことを覚えています。しかし、Next.jsの環境構築を進めていく中で、他のプロジェクトで利用していたPostCSSのcustom mediaというpluginがうまく動作しないことに気づきました。PostCSSのメジャーバージョンアップ(v7 → v8)に伴い、PostCSS plugin周りに入った大きな変更が原因のようです。

いくつか代替案はありましたが、1からのプロジェクトという折角の機会なので、ラクスルでは導入していない新しいCSSツールにも視野を広げてみようという話になりました。

技術選定の要件

CSSツールの技術選定をするにあたって、求める要件は以下の通りです。

  • エディター上での開発体験:クラス名やプロパティの補完が効くこと
  • React Server Component対応:当時はまだexperimentalでしたが、Next.js App Routerの採用を見据えて、React Server Component上でも動作すること(現在はApp Routerを利用しています)
  • パフォーマンス:ブラウザランタイム上での動作が不要なもの。また、CDNによるCSSファイルのキャッシュが可能なもの
  • 未使用のスタイル定義が追える:長期運用していく上で、使われているか分からないCSSを増やさないため

これらの要件を満たす候補を探してチームで議論した結果、選ばれたのはvanilla-extractでした。

vanilla-extractとは

vanilla-extractがどういうものか簡単に紹介します。

vanilla-extract.style

一般的なCSS in JSライブラリと同様にJavaScriptを用いてスタイルを記述する一方で、ビルド時にはCSSファイルを出力します。

ファイル名は .css.ts のような命名規則に従い、パッケージから提供されているstyle 関数を利用します。JavaScriptのオブジェクトとしてCSSプロパティを記述します。

// hoge.css.ts
import { style } from '@vanilla-extract/css';

export const container = style({
  display: 'flex',
  padding: 10,
});

コンポーネント内では以下のようにスタイルを適用します。

// SomeComponent.tsx
import * as styles from './hoge.css';

export const SomeComponent = () => (
  <div className={styles.container}>
    // some contents here...
  </div>
);

ビルド時にはバンドラーによりCSSファイルとして出力されます。

満足している点

補完による実装スピードの向上

ラクスルではVue.js SFC + CSS Modulesを採用しているプロダクトが多いため、補完が効きづらく書きづらいなと個人的に感じていました。vanilla-extractでは純粋なTypeScriptとして型が付くため、IDEの拡張機能は不要です。内部的にはcsstypeが利用されておりCSSプロパティの補完も効くため、実装スピードもかなり早くなりました。また、TypeScriptのオブジェクトとして正確に参照が追えるため、定義元ジャンプを簡単にできる点も嬉しいです。

参照の追跡により安全にCSSを削除できる

私は普段VSCodeを利用していますが、”TypeScript > References Code Lens” をオンにすると参照カウントが表示されます。スタイルを書きながら、消し忘れているスタイルに気付きやすいので非常に便利です。(どこで使われているか分からない迷子CSSをビクビクしながら削除する作業とはおさらばです 👋)

未使用のCSSの削除に関してはPurgeCSSなどのライブラリでも可能ですが、TypeScriptの参照を追えた方がより確実だろうと思っています。

VSCodeの設定画面で「TypeScript, Reference Code Lens」機能にチェックを入れている

vanilla-extractを用いたスタイル定義行の上部に「1 reference」という参照カウントが表示されている

また、プロジェクト全体で参照されていないコードを見つけるために、CLIとして実行できるts-pruneも活用しています。vanilla-extractに限らず、TypeScriptファイル全体で未使用のコードがないかチェックできます。我々のチームでは気が向いたらローカル環境で実行する程度のゆるい運用にとどめています。

活用方法やTipsなど

最小限の機能のみ利用

vanilla-extractにはいくつかのパッケージが提供されています。SprinklesはTailwindCSSのようなユーティリティ形式の定義をサポートし、 Recipesはランタイム上でスタイルを切り替えるAPIを提供しています。導入した当初はこういった機能を活用した方が良いのだろうかと悩みましたが、基本的なAPIの利用だけで十分実装できています。ダンボールワンのECサイトではダークモード対応や、デザインルールが明確に定まっていないことも、シンプルな実装になっている要因かと思います。大抵のことはミニマムに始めるのが吉ですね 👍

サイト全体で利用するカスタムプロパティの定義

サイト全体で利用するカラーバリエーションやフォントサイズ、z-indexなどをカスタムプロパティとして定義することが多いと思います。vanilla-extractでは createGlobalTheme というAPIを活用するのがおすすめです。例えば、セレクター :root に対してカスタムプロパティを定義したい場合は以下のように書きます。

import { createGlobalTheme } from '@vanilla-extract/css';

export const color = {
  gray100: '#fafafa',
  // ...
  gray700: '#391f0d',
} as const;

export const vars = createGlobalTheme(':root', {
  color,
  // other variables...
});

ビルド結果は --color-gray100__yahcht2 のようにハッシュが付き、名前の衝突が起きない仕組みになっています。

固定文字を含んだセレクターが欲しいとき

vanilla-extract は style 関数を利用するとセレクターにハッシュが自動で付与されます。しかし、外部のライブラリなどと連携するために決められたセレクターを生成したい場合があります。具体例の一つとしてreact-transition-groupがあげられます。このライブラリはアニメーションの開始・途中・終了などのタイミングで適用されるセレクターは xxx-enterxxx-enter-active のようにsuffixが決められています。このような場面では style 関数と globalStyle 関数を組み合わせると良いです。以下はポップアップのUIでアニメーションを適用する例です。

import { globalStyle, style } from '@vanilla-extract/css';

export const popup = style({});

globalStyle(`${popup}-enter`, {
  opacity: 0,
  transform: 'translate3d(0, -6px, 1px)',
  display: 'block',
});

globalStyle(`${popup}-enter-active`, {
  opacity: 1,
  transition: `opacity 300ms ease-in-out, transform 300ms ease-in-out`,
});
// BasePopup.tsx
import { CSSTransition } from 'react-transition-group';
import * as styles from './BasePopup.css';

export const BasePopup = ({ children }) => {
  return (
    <CSSTransition classNames={styles.popup}>
      <div>{children}</div>
    </CSSTransition>
  );
};

popup という空のセレクターを作成し、globalStyle 関数の第一引数で使用している点が肝です。 popupstyle 関数によりハッシュ付きのセレクターとなっています。一方、globalStyle 関数は第一引数で指定された文字列がそのままセレクターになります。これにより、ハッシュによるスコープ化を効かせながら、固定のsuffixを持つセレクターを定義することができます。

導入してから気づいた点

半年ほどvanilla-extractを利用してきた中で気づいたことを2つ紹介します。

親コンポーネントから子コンポーネントのスタイルを上書きできないときがある

親コンポーネントから子コンポーネントのスタイルを上書きしようとしたときに、意図した通りに上書きしてくれないことがあります。そもそも、コンポーネントのスタイルを外部から上書きするようなことがあまり推奨されないかもしれませんが、時々そういった場面は訪れます。

// child.css.ts
const text = style({
  color: 'red'
});

// child.tsx
const ChildComponent = ({ className }) => (
  <div className={clsx(className, styles.text)}>This text can be red...</div>
);

// parent.css.ts
const textFromParent = style({
  color: 'blue'
});

// parent.tsx
const ParentComponent = () => (
  <ChildComponent className={styles.textFromParent} />
);

CSSの定義順がバンドラー依存のため、意図しないスタイルが適用されてしまうことがあります。私は他のCSSツールを利用してきた中でこのような問題に出会ったことはありませんでした。しかし、バンドラーを通してCSS定義の順番を決定する仕組み上、潜在的に抱えている問題のようです。

対策として、以下のようなことを心がけています。

  • CSSで上書きするようなスタイル設計を避ける
  • コンポーネントの props として外部から指定し、上書きが発生しないようにする
  • 最悪の場合、!important を指定する(今のところ出番はありません 😊)

プロパティの記述順序をソートできない

vanilla-extractではStylelintのプラグインが存在しないため、Stylelintエコシステムの恩恵を受けることができません。一番影響が大きいのはCSSプロパティの記述順をソートできなくなったことです。これに関しては、チーム内でどのような順序を良しとするかの認識がある程度揃っているため、大きな問題にはなっていません。後述するLinariaというCSS in JSライブラリでは公式プラグインが提供されているので、vanilla-extractにも提供されてほしいなと思います。いくつかのIssueではvanilla-extractの構文の関係で難しいとのコメントも見かけました。完璧ではなくとも、部分的に自動整形してくれるようになれば嬉しいですね(OSSコミットチャンス!)

採用しなかった技術

チーム内で候補に上がったものの採用されなかったCSSツールの技術を一部紹介します。どんな観点で技術選定しているかの参考になれば嬉しいです。

CSS Modules + typed-css-modules

github.com

typed-css-modulesはCSSファイルからTypeScriptの型定義を自動で生成してくれるライブラリです。純粋なCSS記法で書きつつ、型補完も効くため学習コストゼロで書き始められます。冒頭で述べたメディアクエリ周りの問題がなければこちらが最も有力な候補でした。

TailwindCSS

tailwindcss.com

“utility-first CSS framework” と呼ばれるような、事前に定義されたセレクターを組み合わせてスタイルを適用する方式です。人気のあるツールですが、以下の理由から我々には向いていないと判断しました。

ダンボールワンのECサイトはデザインルールが明確に定まっていません。例えば、marginの大きさや文字サイズはUIごとに細かく調整しており、事前に決められたサイズ(デザイントークン)の中から組み合わせるようなデザインにはなっていません。TailwindCSSを使いこなすためには、デザイントークン + それを元に定義したTailwindCSSのutilityを厳密に運用することが不可欠だと思っています。しかしながら、現状のチーム体制でそこまでやり切ることは難しいと判断しました。

Linaria

linaria.dev

タグ関数を用いたテンプレートリテラルでスタイル定義します。テンプレート内は素のCSSと同じ記法のため、既存のCSS実装を移行するのは簡単そうです。一方で、テンプレート記法のため、シンタックスハイライトがエディターの拡張機能に依存します。vanilla-extractとは立ち位置がかなり似ていると思います。当時はありませんでしたが、現在ではStylelintのプラグインが公式で提供されているのは地味に嬉しいポイントです。

まとめ

今回はラクスルの「ダンボールワン」開発チームで導入したvanilla-extractについて紹介しました。ダンボールワンではモダンな技術を取り入れたプロダクト開発を行っています。直近ではNext.js App RouterやChromaticなどの知見を共有していく予定です。ぜひ楽しみにしていてください!

HonoとDenoで社内ツールを作ってみた

こんにちは!ラクスルの灰原です! 軽量かつ高速なWebフレームワークであるHonoと、新進気鋭のJSランタイムであるDenoを使って、社内ツールを作ってみましたので紹介します。

作ったツール

テックブログ向けのアイキャッチ画像ジェネレータを作りました。

タイトルを入力して、 背景画像と文字色を選んで、 文字の位置と大きさを調整して、 後は「Download」ボタンを押せば画像が手に入ります。

これは以前、弊社デザイン組織で作られた「Zoom背景ジェネレータ」に多分に影響されています。 こちらのデザイナーブログも是非ご覧ください! note.com

技術スタック

このツールは利用頻度がそこまで多くないと思われることもあり、お手軽にローカルで動くアプリとして作ることにしました。 またプロダクションではなく社内ツールということもあり、モダンな技術を取り入れて作ってみました。 最終的な技術スタックは以下の通りです。

  • Hono
    • 軽量・高速で各種JSランタイムに対応したWebフレームワーク
  • Deno
    • 次世代JSランタイム
  • esbuild
    • Goで実装された高速なバンドラー
  • Twind
    • tailwind-in-js を実現するJSライブラリ
  • Alpine.js
    • 動きのあるページを手軽に作れるJSフレームワーク

Honoのコンセプトについて知りたい方は、作者のyusukebeさんのブログもご覧ください。 zenn.dev

ファイル構造はこのようになっています。

.
├── Dockerfile
├── README.md
├── public
│   └── assets
│       └── images # 背景画像
│           ├── ... 
│           ├── ...
│           └── ...
└── src
    ├── components # JSXコンポーネント
    │   ├── footer.tsx
    │   ├── header.tsx
    │   ├── layout.tsx
    │   └── main.tsx
    ├── scripts # ブラウザ上で動かすスクリプト
    │   ├── app.js
    │   └── canvas.js
    ├── index.tsx # アプリのエントリーポイント
    ├── twind.ts # twindを使うためのスクリプト
    └── esbuild.ts # esbuildを使うためのスクリプト

全体の行数カウントは429行です。なかなかコンパクトに作ることができました。

goclocの実行結果

index.tsx の内容

アプリのエントリーポイントである index.tsx に関してもコンパクトに書くことができました。 index.tsx ではHonoで配信する内容を設定して、最後に Deno.serve をしています。各行を見ていきましょう。

まずはHono本体と関連のコンポーネントをimportします。

/** @jsx jsx */
import { Hono } from "https://deno.land/x/hono@v3.9.2/mod.ts";
import {
  jsx,
  logger,
  poweredBy,
  serveStatic,
} from "https://deno.land/x/hono@v3.9.2/middleware.ts";

Twindやesbuildを使うためのスクリプトをimportします。これらはHonoのMiddlewareという形で実装されています。こちらについては後述します。 Middleware - Hono

import { tailwindStyleTagInjector } from "./twind.ts";
import { withForms } from "https://esm.sh/@twind/forms@0.1.4";
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";

import { esbuildBundler } from "./esbuild.ts";

次に各種JSXコンポーネントをimportします。HonoはJSXを使うことができます! JSX - Hono

import { Layout } from "./components/layout.tsx";
import { Header } from "./components/header.tsx";
import { Footer } from "./components/footer.tsx";
import { Main } from "./components/main.tsx";

importが終わり、アプリの設定をしていきます。ここではアプリの初期化、各種Middlewareの設定をしています。

const app = new Hono();

app.use("*", logger(), poweredBy());
app.use("/public/assets/*", serveStatic({ root: "./" }));
app.use(
  "*",
  tailwindStyleTagInjector({
    presets: [
      presetTailwind(),
      {
        theme: {
          preflight: withForms(),
        },
      },
    ],
  })
);

app.get(
  "/scripts/*",
  await esbuildBundler({
    entryPoints: ["./src/scripts/app.js"],
    outdir: "/scripts/",
  })
);

最後に先ほどimportしたJSXコンポーネントを組み合わせて、ルートパスで返すHTMLを指定し、サーバを立ち上げます。

app.get("/", (c) => {
  return c.html(
    <Layout>
      <Header />
      <Main />
      <Footer />
    </Layout>
  );
});

Deno.serve(app.fetch);

以上が index.tsx の内容です。シンプルですよね!

Tips

HonoやDenoを使ってみて、いくつかTipsを得られたので紹介します。

HonoでTwindを使う

TwindをSSRで使う際、返却されるHTMLを読み込ませて、そこから適切なstyleタグが生成され、それを元のHTMLに埋め込んで配信するというような動きになります。 Use With: React • Twind.style

つまり、Honoがレスポンスを返す直前で処理をフックして、Twindを使ってレスポンスのbodyを書き換える必要があります。 これはHonoのMifflewareを作ることで実現しました。

HonoのContextからbodyの内容を取得し、Twindの inline メソッドでstyleタグを挿入済みのHTMLを生成しています。

  • twind.ts
import { createMiddleware } from "https://deno.land/x/hono@v3.9.2/helper.ts";
import { inline, install } from "https://esm.sh/@twind/core@1.1.3";

// response bodyにTailwindを使うために必要なstyleタグを挿入する
// see: https://twind.style/packages/@twind/core#inline
export const tailwindStyleTagInjector = (config: any) => {
  install(config);

  return createMiddleware(async (c, next) => {
    await next();

    if (!c.res.body) {
      return;
    }

    const stream = c.res.body.pipeThrough(new TextDecoderStream());
    const buffer: string[] = [];

    for await (const chunk of stream) {
      buffer.push(chunk);
    }

    const html = buffer.join();
    const inserted_html = inline(html);

    c.res = new Response(inserted_html, c.res);
  });
};

HonoのJSXでAlpine.jsを使う

HonoのJSXの中で、Alpine.jsを使った :src="thumbnail" のような動的なアトリビュートの割り当てを記述すると、下記のようなエラーになってしまいます。

error: The module's source code could not be parsed: Unexpected token `button`. Expected jsx identifier at xxx

これはHonoのJSXとhtmlヘルパーを組み合わせることで解決できます。以下のようにJSXのなかにhtmlヘルパーの記述を埋め込むことで、エラーにならずにレンダリングすることができます。

const Component = () => (
  <button>
    {html`
      <img :src="thumbnail" />
    `}
  </button>
);

今回Honoを使ってみて、このJSXとhtmlヘルパーの組み合わせがとても体験が良いと感じました。HonoのJSXについてはyusukebeさんのこちらのブログでも紹介されています。 zenn.dev

Hono+Denoでesbuildを使う

このアプリはブラウザ上で動くスクリプトがAlpine.jsに依存していますが、実はCDNもNodeも使っていません。

実際のスクリプトをご覧ください。そうです、Alpine.jsのインポートにDenoのnpm:specifierを使っています!

import Alpine from "npm:alpinejs";
import { setupFunctions } from "./canvas.js";

window.Alpine = Alpine;
Alpine.start();

setupFunctions();

これはDeno公式のWebフレームワークであるFreshの機能を真似たものです。

Fresh 1.2 – welcoming a full-time maintainer, sharing state between islands, limited npm support, and more

npm: specifireによるimportはもともとDenoの機能で、Denoでネイティブにnpmパッケージを扱うためのものです。 これを使うことで node_modules が生成されず、Denoのキャッシュにパッケージが保持されます。

npm: specifiers | Deno Docs

さて、これはDenoランタイム上の挙動、つまりサーバーサイドでの挙動です。 Freshではそれをブラウザ上で動かすフロントエンドのコードでも使えるようにしています。

esbuild_deno_loaderというesbuildのプラグインで、それが実現されています。esbuildによるバンドルをする際、npm: specifierの記述があれば、node_modulesではなくDenoのキャッシュを参照するようになっているようです。

feat: support for `npm:` specifiers by lucacasonato · Pull Request #67 · lucacasonato/esbuild_deno_loader · GitHub

このesbuild_deno_loaderとesbuildを使ったバンドルを、HonoのMiddlewareとして実装することで、npm: specifierで依存先を記述したフロントエンドのコードを動かしています。

  • esbuild.ts
import { createMiddleware } from "https://deno.land/x/hono@v3.9.2/helper.ts";
import * as esbuild from "https://deno.land/x/esbuild@v0.19.2/mod.js";
import { denoPlugins } from "https://deno.land/x/esbuild_deno_loader@0.8.2/mod.ts";

export type EsbuildBundlerOptions = {
  entryPoints: string[];
  outdir: string;
};

export const esbuildBundler = async (options: EsbuildBundlerOptions) => {
  const bundle = await esbuild.build({
    plugins: [...denoPlugins()],
    entryPoints: options.entryPoints,
    outdir: options.outdir,
    bundle: true,
    format: "esm",
    write: false,
  });
  esbuild.stop();

  return createMiddleware(async (c, next) => {
    const url = new URL(c.req.url);
    const output = bundle.outputFiles.find((v) => v.path == url.pathname);
    if (!output) {
      await next();
      return;
    }

    c.res = c.body(output.text);
  });
};

おわりに

HonoやDenoなどモダンな技術スタックを使った社内ツールについて紹介しました。 新しいフレームワークや言語をいきなりプロダクションで使うのは、安定性や社員の教育の面から難しい面も多々ありますが、社内ツールをはじめとした限定的なシーンからじわじわと使っていきたいものです。

「ラクスル課題解決型インターン」データサイエンスチームとして参戦!

はじめに

この夏「ラクスル 課題解決型インターン」に参加しました、インターン生の澤木です。僕はノバセルのデータサイエンスチームとして参戦したので、この記事ではその感想や、学びをまとめていこうと思います!

読んで欲しい人

  • ラクスルに興味がある人
  • 志望企業はまだ決まってないが夏インターンを成長機会にしたい人
  • DS/ML職でインターンや就職を考えている人

インターンシップ概要

  • 参加コース:ラクスル課題解決型インターン
  • 配属:ノバセル
  • チームの職種:データサイエンス・機械学習チーム
  • 参加日程:9/11 - 9/15
  • 報酬:10万円

2023夏のラクスルのインターンシップは「ハッカソン型」と「課題解決型」の2種類があり、僕は後者で参加しました。課題解決型インターンは、「ラクスルの開発組織が実際に直面している課題やタスクにアサインされ、エンジニアと共に業務を進める(募集ページより引用)」インターンシップです。

タスクごとにチームが分けられ、フロントエンドチームやサーバーサイドチームがラクスル事業部とノバセル事業部で1チームずつ、そして僕らが属するデータサイエンスチームがノバセル事業部で1チーム、合計5チーム編成でした。各チームは3〜4人( + メンター社員1人)で編成されており、課題解決型インターン全体の参加者は20人弱でした。

応募・選考について

このインターンとの出会いは、サポーターズが主催する逆求人イベントでした。そこで人事の方と面談をするまではラクスルは企業名は知っている程度で、そもそもエンジニア募集やインターンがあることすら知りませんでしたが、事業内容・インターン概要(そして報酬)どれにも魅力を感じオファーを受けることにしました。

選考に進む前に、後のメンターとなるデータサイエンティストの松村さんとの面談も設けていただき、ノバセルにおけるDS職の解像度が高まったことも参加への大きな動機になりました。

選考は技術課題と面接が1度ずつありましたが、専門性や高い技術力を求めているというよりかは、最低限のスキルチェックと、カルチャーフィットを見ているという印象でした。特に技術課題は、コーディングテストとしてはかなり丁寧な作りで、自分で手を動かしてデータの分析やモデリングをしたことのある学生なら十分に解ききれる実用的な良問でした。

データサイエンスチームの開発について

取り組んだ課題

実際にノバセル内部のお仕事にチャレンジさせていただいたため、その多くがNDA的に技術ブログに書くことができません…。ですが、伝えられる限りの情報で5日間の取り組みについて説明できたらと思っています。

それすなわち、僕らデータサイエンスチームの取り組みは、

「テレビCMの分析に必要な情報を画像から半自動でとって来れるようにしよう」

ということでした。ノバセルは広告代理店などからデータを受託し、効率の良いテレビCMの放映プランニングや効果分析を行なっています。そのためには分析できる形式のデータが必要不可欠なわけですが、テレビ業界のDX化はまだまだ道半ばで、綺麗なデジタル形式のデータが手に入らないことも多いようです。現状は手作業でデータ化やダブルチェックを行なっており、その現状を画像処理やデータサイエンス技術を使って脱しようというのが今回のインターンシップで取り組む課題でした。

補足:「ノバセル」について https://novasell.com/

分析・開発スケジュール

5日間(最終日は資料作成や発表のため実質4日間)という限られた時間で、どのように取り組むべきか、チームでまず作戦会議を行いました。しかし作業を進めていく中での軌道修正も多く、結論から言うと、5日間の開発は以下のような形で進みました。

【1日目@オンライン】

  • データの基礎分析・手法選定
  • 開発の役割分担

【2日目@オンライン】

  • 1日目で決めた主法の実装・課題の洗い出し
  • Bizの方々とのミーティング①
    • 現状の課題の聞き込み
    • 機能要件のすり合わせ

【3日目@オフィス】

  • それぞれの役割の実装を完了
  • 開発の繋ぎこみ

【4日目@オフィス】

  • streamlitを使った簡易的なアプリ開発
  • Bizの方々とのミーティング②
    • ここまでの開発の進捗共有
  • デザイナーの方からのFB会
    • アプリデザイン、ユーザビリティについてのフィードバック

【5日目@オフィス】

  • バグのFix
  • 発表資料作り
  • 発表会

初めは「機能だけ完成できればそれで良いのでは?」と考えていたチームメンバーでしたが、実際に実装をしてみると、精度100%の完璧なオートメーションは現実的でないことがわかってきました。さらに、ノバセルのBizの方々とお話をして、僕らは大切な視点を欠いていることに気付かされました。

Bizの方々と話した気づき
  • データサイエンス技術は”銀の弾丸”にはならない
    • 「絶対に精度100%」のモデルは実現できない
    • 仮に精度99%なら、その1%の修正ために結局人の手が介入しなくてはならない
  • データサイエンスの技術の「オペレーションとの融合」という視点
    • 業務フローの効率化を目指して技術を導入する
    • 完全な自動化を目的に掲げる必要はない
  • 機能や方法論だけでは実用には不十分
    • オペレーションに組み込むためには、「使える」ようにしなくてはならない
    • 半自動化した部分が誤りを生じうるなら、それを簡単に確認・修正できる必要がある

手元のデータセットで研究や解析をしたり、Kaggleなどのコンペで精度勝負ばかり取り組んでいる学生が見落としがちな視点だと思います。便利な学習モデルやツールが次々現れようと、ビジネス場面ではあくまで「道具としての技術」の1つであり、それを扱うエンジニアは常に業務フローの中でそれをどう組み込んで活かすかを考えなくてはなりません。機能だけではダメだと気づき、開発スケジュールを組み直して、オペレーションに載せられるようなWebアプリの作成まで視野に入れることにしました。

開発の進め方

1~3日目:画像処理・画像認識の実装

データサイエンスや機械学習周りの開発は、基礎分析と手法選定ができれば、比較的役割分担がしやすい領域だと思います。実際に、1日目の時点で大雑把な技術選定の目処が立ったため、与えられたタスクを実現するためのコーディングを大きく3つの工程に分け、三人で分担して取り組みました。それぞれの工程で各人が実験や実装を行い、何か進捗や疑問があればSlack・Github・オフラインで逐次共有を行い、議論しながら進めました。

4日目:Webアプリ実装

滑り出し好調であった開発ですが、途中でアプリまで作り切ろうと決まったことで、その開発を行った4日目はうまく役割分担ができず、急に開発が混乱しました。 実装が容易なstreamlitを使って開発を行いましたが、いかんせん3人とも初めましての技術だったので、全員が手探りで進めました。 この上なく贅沢なことに、ラクスル全体のデザイン統括をしている社員さんからユーザビリティやデザインのフィードバックをいただき、試行錯誤しながらなんとか動くものを完成させることができました。

5日目:スライド作成・発表準備

ほとんどが完成しました。細かなバグのFixを分担しつつ、発表資料を急いで作り、発表練習を行いました。発表時間は3分と非常に短かったため、背景から開発概要・技術詳細まで組み込むことに苦労しました。 結果としてはオーディエンス投票で1位となり、User Awardを取ることができたので、5日間の頑張りが報われてとても嬉しかったです。

開発以外のイベント

5日間のインターンはひたすら開発をしていたわけではなく、オフィスツアーやランチ、懇親会など様々なイベントが用意されていました。どれも楽しかったので、それぞれの学びや気づきをまとめます。

オフィスツアー

初出社日だった3日目の朝にありました。 何より驚いたのはオフィスのおしゃれさです。オフィスといえば整然とデスクが立ち並ぶ無彩色なイメージがありますが、ラクスルのオフィスは日経ニューオフィス賞を受賞しているだけあって、開放感があり、緑が多く、とても気分が晴れるデザインでした。エンジニアの出社は週1以上ですが、このオフィスならむしろ毎日出社したいと思える雰囲気でした。

ラクスルHPより引用

ランチ・懇親会

3日目の終業後の懇親会や、4日目のランチでは、インターン生だけでなく新卒社員の方や人事の方、他のグループのメンターの方などと広く交流することができました。畏まった場では聞きづらい採用や待遇についての話や、ラクスルの魅力など、お酒を飲みながらたくさん聞くことができたので、ただインターンに参加する以上に、カルチャーやビジョンの解像度が上がりました。僕らのチームメンバーは全員お調子者だったので、失礼なことをたくさん聞いてしまったような気がしますが、それでもNG無しと言わんばかりに答えていただけました。結果として、昔からの友達のように社員さんと仲良くなっているインターン生もいました笑

まとめ

全体として、ラクスルそしてノバセルの雰囲気を知りながら、楽しく課題に取り組める非常に有意義なインターンシップでした。メンターや社員さんのサポートも手厚く、懇親会を通じてNGなしで色々な話を聞くことができたので、自分自身非常に解像度が高まり良かったなと感じています。エンジニアインターン、特にデータサイエンス領域のものは、インターン用に作られた模擬的なタスクが与えられることもしばしばですが、ラクスルの課題解決インターンはインターン生を新入社員同等に扱い社内のリアルなタスクに取り組ませてもらえる点で非常に学びのあるものだったと思います。Tech側だけでなく、Biz側の視点も持ちながら取り組むことのできるリアリティに富んだインターンであるという点で、とても濃い5日間でした。僕らのチームはとにかく仲がよく、それも相まって笑いの絶えない5日間でもありました。改めて、最高のインターンシップをありがとうございました! この記事を読んでラクスルのインターンに少しでも興味を持つ方が増えれば嬉しいです。

ラクスル株式会社のインターンに参加しました

はじめに

先日ラクスルのインターンに参加した、藤堂と申します。 参加体験記を書きますので、ラクスルで実際に働いてみた雰囲気が伝われば幸いです。

インターン概要

内容は、ラクスルの事業で実際に抱えている課題を解決するため、プロダクトを作り、プレゼンするというものです。 実施期間は5日間、2023/09/11(月)〜2023/09/15(金)の11:00〜19:00でした。朝弱い私に嬉しい11時始業。

また、5日間のうち、前半2日間はリモート、後半3日間は出社でした。 ラクスルでは週1出社が多いとのことで、リモートも織り交ぜた実際の働き方が体験できる形になっていました。

私たちの開発について

何のために、何を作るのか

ラクスルの事業の一つである「ノバセル」で、Webの統計データを自社に蓄積したいという思いがありました。 その解決の第一歩として、顧客のWebサイトに埋め込むだけでPV数を計測できる、JSタグとそのシステムを作成することになりました。

どう進めたか

自己紹介などチームビルディングを行い、私がリーダーとして進めていく形になりました。 初日はどう実装していくか、1日かけて話し合いました。 個々人の技術スタックがバラバラな中で、何を根拠にどの技術を選ぶのか、まとめていくのが大変でした。 最終的に、全員若干は経験があって実装コストが低そうなPythonを主軸に、フレームワークなどを選定していきました。

2日目からは実装を進めました。使用技術のイメージは共有できていましたが、チームメンバーそれぞれ未経験の領域も多かったので、適宜ドキュメントやチュートリアルを触って進めていました。 なお、私たちのチームでは具体的な分担を決めることは少なかったと思います。4人それぞれ、やりたいこととやる必要のあることでバランスをとって、自然と仕事やチームが分かれていったのが印象に残っています。 むしろ、分担を明確にしなかったおかげで、「他の人の仕事だから」と放置せず、全員が共有しあって助け合える環境でした。

1〜2日目はリモートで、相手の顔が見えない中でしたが、チームメンバーが積極的で、特に苦労は感じませんでした。バーチャルオフィスツールのGather Townを使っているのもあり、他のチームを覗いたり、たまたま同じインターンに参加した知り合いに絡んだり、チームメンバーはゴーカートレースしてたりと、オンラインの良さがあって面白かったです。

3〜5日目は対面で、やっぱりディスカッションは顔が見えたほうがやりやすいと思いました。リモートで仲良くなった後に初対面だったので、謎に緊張して新鮮な体験でした。

実際にできたものの概要

大きく分けて、自動計測システムと、管理者向けPV数確認サイトの2つができました。 自動計測システムでは、Webページを訪問するとJavascriptが実行され、PV情報をLambda上のFastAPIに送り、DynamoDBに保存します。 管理者サイトでは、EC2上のDjangoがDynamoDBから情報取得し、Chart.jsでグラフ形式で表示します。 また、追加要件として、PV以外のイベント(例えばボタンクリックなど)も計測できるようにしました。

工夫したところ

チームメンバーのおかげで、技術的にも、プロダクト的にも、色々な工夫を加えられました。

  • 適したフレームワークの選定
  • 計測したいイベントをカスタマイズ
  • 複数サイトを一括管理
  • CSVダウンロード

などなど。

リーダーとしての学び

リーダーとして開発したのは初めてでした。マネジメント職種はこれまでイメージが湧かなかったのですが、今回のインターンで、大変さも面白さも凝縮して体験できたと思います。

大変だったことは、チーム全体を見渡しつつ個々の技術もキャッチアップしなければならなかったことです。全体最適に寄りすぎて個別技術がナニモワカランときが多々あり、私が手を動かすタイミングでよく困りました。

逆に面白かったことは、たった一度方針を決めただけで、私が何もせずともプロダクトができていったことです。チームメンバー一人ひとりが強みを発揮しながら、みんなで同じ方を向いて開発していました。もちろん、ひとえにチームメンバーのおかげでしかないです。ただ、その作られていく過程を見ていて、「これがマネジメントのやりがいなのかも」と思いました。

全体の感想

まず、ラクスル社内の雰囲気が良すぎて驚きました。toBでかっこいいビジョンを掲げているだけあって、キビキビ仕事しているイメージでしたが、実際には立ち話で気軽に交流したり、ソファでお茶していたり、ゆったりとした人間関係で自由に仕事ができる環境だと感じました。

しかも、チームメンバーの雰囲気も最高でした。初日から雑談ができるような関係になり、開発フェーズでも積極的に声を掛け合っていました。遠方の方もいましたが、できればまた集まりたいです。 また、他のチームの方もレベルの高い人が多く、非常に刺激になりました。

おわりに

今回のインターンは、「ラクスルで働くとはどういうことか」を体験できるまたとない機会でした。 チームメンバーのみなさん、メンターさん、ラクスルの皆さん、本当にありがとうございました!

ご興味を持たれた方は、ぜひ次回応募してみてください!

ラクスルのインターン体験!ハッカソン型と課題解決型、どちらを選ぶべきか?

修士1年生の平尾です。情報セキュリティを専攻しています。 今回、ラクスルの課題解決型インターンに参加してきました。これまでラクスルでは、ハッカソン型のインターンのみを実施していたため、課題解決型の実務に近いインターンは今年が初の試みになります。「ラクスルのインターンに行きたい!”ハッカソン型””課題解決型”があるけどどっちにしよう?」と迷っている方などに届くと嬉しいです!

目次

課題解決型インターンって?

今年は、前述の通り”ハッカソン型””課題解決型”の2つのコースでインターンの募集がされていました。前者は8/21〜25、後者は9/11〜15の各5日間で開催されました。待遇は双方とも100,000円/5日間の交通費・宿泊費支給で、対面実施の際にはお昼ご飯まで提供してくださいました。ハッカソン型インターンはテーマの自由度が広く、最新の技術を試して新しいシステムを作るなど普段の業務にすぐに使うことがなくとも中長期的な視野で技術的チャレンジをするテーマに取り組めるものです。一方今年から実施された課題解決型インターンは、業務で出てきた課題をインターン生がチームになって開発をすることで解決する、といったより実務に近い経験を積むことができる内容でした。今回私はどちらを志望するか考えた上で、ラクスルの社風や「課題解決」といった理念をより身近に体験できるインターンに取り組みたいと考え、課題解決型のインターンを選びました。

開発チームについて

今回はそれぞれフロントエンドチームバックエンドチームデータ活用チームの3種類のチームに分かれ、合計5チームでの開発でした。1チームは約3〜4人のインターン生、1人のメンターで構成されています。私はバックエンドチームとして参加し、チームは計5人でした。私自身今年の3月からモバイルアプリ開発のアルバイトをしているのですが、周りのインターン生の方々はその期間以上に開発経験がある方ばかりでとても恐縮でした。インターンの選考時に、「制限時間こそ決められていないが、精度や整った設計や可読性を求められるコーディング試験」が課されたからこその、技術力が高いメンバーだったのかなと思います。余談ですが、インターンの正式なオファーを受けた際にコーディング試験のフィードバックを頂けたため、かなり自身のコードを見直す参考になりました。インターン選考を受ける方は仕様を満たすことができたからとすぐに出すのではなく、せっかくフィードバックをいただけるので、「妥協せず全力を出しきった」「これが一番綺麗だ」と思えるコードを出すのが良いと思います(あまり選考時に妥協する人はいないと思いますが...)。もちろん選考期間にインターンの枠が埋まってしまう可能性もあるので、参考程度に頭の片隅に置いていただけると嬉しいです。

使用技術

実務課題に取り組んだ関係上、開発テーマについて具体的な話は書けないですが、使用技術については言及が可能なので、「どういう技術に触れられるか」が気になっている方に向けて書かせていただきます。 使用した技術は以下の6つです(直接的にテーマ推測が出来るものについては省略)

  • AWS Lambda
  • Amazon EventBridge
  • Amazon ECR
  • Amazon S3
  • Snowflake
  • OpenAI

特にAWSに関してかなり自由な権限が与えられ、「実装したいけど権限周りで申請などに時間がかかる」ことはほぼありませんでした。そのためインターン期間が5日と短い中でかなり有難く、のびのびと実装を進めることが出来ました。また、ラクスルはクラウドベースのデータ活用に際してSnowflakeを使用しています。私自身Snowflakeに触るのは初めてで、近年注目されているサービスを自由に使用できるのは良い経験になりました。ちなみに去年ラクスルでは、Snowflakeの社内勉強会も開催されていたらしいです。 インターン期間中は社内のリポジトリやNotion、Slackを自由に見ることが出来たため、過去の実装事例を参考にしたり設計図ややり取りを元にヒントを得たりと、実務により近い経験が出来たのでは無いかと思います。

苦労したことや工夫点

AWSに触るのはほぼ初めてだったので、まずは仕様書を読む段階から苦労しました。読めていたつもりでも上手く動かなかったり、環境依存のバグが発生したり......。そのたびにメンターの方への質問を繰り返すことで、段々詰まることが減り、作業の効率が上がるのを感じました。 私たちのチームでは「10分ルール」というWorking Agreementを設定しており、10分悩んでもわからなかったことは周りの方やメンターに即相談するようにしていました。そのおかげで、「1人だけ詰まっている疎外感」を感じることがなく、チームとして楽しく開発をすることができました。また、毎朝詳細な設計や優先順位について1時間チームのみんなで話し合ったおかけで、かなり工数のかかる開発テーマの中、無事にやり遂げることができたのでは無いかと思います。

業務外の話

開発テーマに触れられない関係上、写真とかを貼ることができず少し寂しいので美味しかったお昼ご飯や素敵なオフィスの写真を貼ります。

最終日に1チームに1プレートいただいたつきじ海賓のお寿司

この世の全ての美味しさを凝縮したお味でした。最終発表前だったので本当に原動力になりました。ありがとうございます。

ラクスルのオフィス

天井がとにかく高い。ミーティングルーム上に本物の木が植えてあり、オフィスとは思えないほど広々とした環境でチーム開発をすることができました。一般的なオフィスのような部屋ももちろんあり、適宜集中できる自分にあった場所をえらぶことができます。 また、無料のウォーターサーバーが設置されてあり、ノベルティでラクスルロゴの水筒をプレゼントしていただいたこともあり、かなりお世話になりました。他にも社内に珈琲を100円で1杯飲むことが出来るサーバーがあったり、オフィスグリコが設置されていたりと、かなり働きやすい環境だなぁと感じました。

感想

今回はラクスルの課題解決型インターンに参加させていただきました。インターンの業務を通して、ラクスルの実務のイメージがかなり明確に掴めたのではないか、と思います。また、情報セキュリティを専攻していることもあり、その関係の方ともお話させていただく機会をいただき、かなりモチベーションになりました。「技術スキルを伸ばしたい」「ラクスルが何をしているか知りたい」の双方を叶えることができる、とても良いインターンだったと思います。「ハッカソン型と課題解決型、どちらにしよう?」と悩んでいる方の一助になると有難いです。読んでいただきありがとうございました!