Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Mock Service Workerを使ってOpenAPIに寄り添ったテストを行う。

Tech事業部プロダクトチームの仲本です。

10月から、フロントエンドチームからプロダクトチームになりました。

今回は、現在開発しているプロダクトに、テストを導入するにあたって、Mock Service Workerを導入し、OpenAPIと組みわせてMockAPIを作成しテストを作成しました。

今回の記事は導入記録/所感的なものを書いています。

テストを導入する経緯

  • フロントエンドのテストがまずできていなかったこと
  • 社内業務案件で、リリースのたびにチームで手作業でテストを行っていたこと
  • 今後規模が大きくなると、手作業でのテストが辛くなる

こういった経験のもと、テストを導入し安全性を確保したいということになりました。

jest.mockによるmock化がかなり多くなる話

実際にjestを導入して、テストを書いていくとmock化しないと通らないテストがあること気づきました。

apiを実行する際のfetchの処理などもmock化させると、実際の動きと遠くなるなるのではないかという懸念があり調べているとMock Service Workerに出会いました。

Mock Service Workerとは

Service WorkerAPIを使用して実際のリクエストをインターセプトするAPIモックライブラリです。

ユーザーログイン画面のテスト実装

今回はログイン画面のテスト実装していきます。

Login画面で表示するコンポーネントが以下になります。

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { jsx } from '@emotion/react'
import { useForm, UseFormReturn } from "react-hook-form"
import { BrowserRouter as Router, useHistory } from 'react-router-dom'
import { hot } from 'react-hot-loader'

import {
  fetchAsyncLogin,
  selectError,
} from '../../../stores/slices/authSlice'


// component
import { Button } from '../../components/atoms/Button'

// style
import { LoginBox, LoginTitle } from '../../../style/pages/Login'
import { Form, FormLabel, FormInput, FormButton, FormValidButton, FormValidTxt } from '../../../style/components/block/Form'


//type
import { AppDispatch } from '../../../stores';
import { LoginFormInput } from '../../../../types/user';

const Login = () => {
  const methods: UseFormReturn<LoginFormInput> = useForm<LoginFormInput>();
  const { register, handleSubmit, formState: { errors }, reset } = methods
  const dispatch: AppDispatch = useDispatch()
  const history = useHistory()

  const onSubmit = async (data: LoginFormInput) => {
    const result = await dispatch(fetchAsyncLogin(data))
    if (fetchAsyncLogin.fulfilled.match(result)) {
      if (result.payload.status == 200) {
        history.push('/')
      }
    }
    reset()
  }

  return (
    <div>
      <div css={LoginBox}>
        <form css={Form} onSubmit={handleSubmit(onSubmit)}>
          <label css={FormLabel}>
            メールアドレス
            <input
              autoComplete="email"
              type="email"
              aria-invalid={errors.mailAddress ? "true" : "false"}
              {...register("mailAddress", { required: true, pattern: /^([a-zA-Z0-9])+([a-zA-Z0-9\._+-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/ })}
              placeholder="メールアドレスを入力してください"
              css={FormInput}
            />
          </label>
          <label css={FormLabel}>
            パスワード
            <input
              autoComplete="password"
              type="password"
              {...register("passWord", { required: true, pattern: /^[a-z\d]{1,100}$/i })} aria-invalid={errors.passWord ? "true" : "false"} placeholder="パスワードを入力してください"
              css={FormInput}
            />
          </label>
          <Button name="ログイン" cssStyle={FormButton} onClick={handleSubmit(onSubmit)} dataTestId="test-2-submit-btn"  />
        </form>
      </div>
    </div>
  )
}
export default hot(module)(Login)

要件としては

  • メールアドレスが入力できる

  • パスワードが入力できる

  • ログインボタンを押すとログイン用APIにPOSTする

流れになります。

レスポンスデータをOpenAPI定義から取得する

現在携わっているプロジェクトでは、OpenAPIを使用してAPIのやりとり/認識合わせを行なっています。 OpenAPI仕様が記載されたjsonファイルを使用し、Mock Service Workerの設定を行います。

そうすることで、より使用されているAPIを忠実に再現できることや、もし仕様の変更があった際もしっかり新しい情報の入ったOpenAPIを取り込めていたら気付けるのではないかと思い使用しました。

以下がOpenAPIで定義している内容になります。

// openapi.json

"paths": {
  "/api/login": {
// ~~省略~~
  "responses": {
    "200": {
      "description": "HTTP OK",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
// ~~省略~~
            "examples": { // <= 今回レスポンスで使用するデータ
              "default": {
                "value": {
                  "status": 200,
                  "response_time": 1.1535649299621582,
                  "message": "処理が正常終了しました。",
                    "data": {
                      "login_id": "test@hoge.co.jp",
                      "token": "123|njGYLOG9EuZuIrSv83dUvnIWzFLbo6Ri5mUOLm4q",
                    }
                 }
              }
            }

上記のjsonファイルをimportし、examplesを以下のファイルでimportします。

この設定をすることにより、jsonファイルに変更があった際に変更に対応してくれます。

import schema from '../../openapi.json'

const components = {
  LoginUser: schema.paths['/api/login'].post.responses[200].content['application/json'].examples.default.value,
}
export default components 

Mock Service Workerでhandlerを設定

次にmockを作成していきます。

mswからrestをimportしgetやpostの設定を行います。

今回はpostの設定を行います。

参考: mswjs.io

以下ファイルで定義している内容としては

  • login_idがtest@hoge.co.jpかつpasswordがtest1234@

  • 上の条件を満たしていた時ステータス200と、先ほど定義したexamplesを返す設定をしています。

import { rest } from 'msw'
import components from './components'

const handlers = [
  rest.post<Record<string, any>>('http://localhost:3000/api/login', (req, res, ctx) => {
    const { login_id, password } = req.body
    if (login_id === 'test@hoge.co.jp' && password === 'test1234@') {
      return res(
        ctx.status(200),
        ctx.json(components.LoginUser)
      )
    } else {
      return res(
        ctx.status(403),
        ctx.json({
          error: 'error: invalid username or password'
        })
      )
    }
  }),
]

export { handlers } 

jest.setup.js

jest.setup.jsというファイルで、先程設定をしたmockを動かすためのserverの設定と

node環境だと、window.fetchが使えないので node-fetchをinstallして設定を行います。

github.com

import server from './src/test/lib/msw/server'
import { cleanup } from '@testing-library/react'
import fetch from "node-fetch"

beforeAll(() => server.listen())
afterEach(async () => {
  server.resetHandlers()
})
afterAll(() => server.close())

if (!globalThis.fetch) {
  globalThis.fetch = fetch
}

実際のテストコード

import React from 'react';
import '@testing-library/jest-dom/extend-expect'
import { fireEvent, getAllByText, render, screen, waitFor } from '@testing-library/react'
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';

import store from '../tsx/stores';

import Login from '../tsx/views/pages/login/Login'


const LoginComponent =
  <Provider store={store}>
    <Login />
  </Provider>

const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    push: mockHistoryPush, // pushメソッドをダミー関数で上書きする。
  }),
}));

〜〜省略〜〜
describe('test 2 ログイン処理', () => {
  it('ログイン確認', async () => {
    const { getByTestId } = render(LoginComponent)

    await act(async () => {
      fireEvent.change(screen.getByLabelText(/メールアドレス/i), {
        target: { value: 'test@hoge.co.jp' },
      });

      fireEvent.change(screen.getByLabelText(/パスワード/i), {
        target: { value: 'test1234@' },
      })
    });

    await act(async () => {
      fireEvent.submit(getByTestId('test-2-submit-btn'))
    });

    await waitFor(() => {
      expect(mockHistoryPush).toBeCalledWith('/');
    })
  });
}) 

ログインボタンを押した時のAPI処理のみのテストを表示しています。

ログインボタンを押すと先程handlerで設定したMockを実行するようになっています。

以上がユーザーログイン画面のテスト実装でした。

今後考えていきたいこと

  • どの範囲をテストしていくべきかを明確にする
  • 別のプロジェクトに導入するために、どういった開発手法がいいかを考えていく
  • まだ導入したてなので色々実装していきながらベストを探していきたい

上記をまず、明確にできるように頑張っていきたいと思います。

参考記事

OpenAPI定義をmswに活用してお手軽モック

Mock Service Worker で jest.mock を使わず非同期リクエストのテストを書く

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

【Laravel】DDDでセッションを取り扱う際の妥協した実装

こんにちは、バックエンドエンジニアの青山です。
先日LaravelでDDDするときのセッション取り扱いに困った末、ある程度いい感じに妥協した実装に辿り着きました。今回はそちらについて書こうと思います。

はじめに

「LaravelでDDD〜」と書きましたが、本記事の内容は正確には「Laravelでレイヤードアーキテクチャを採用する際のセッションの取り扱いの一例について」です。 DDDを行う中で出会った問題に対する解決策の一例なのでタイトルは上記のようにしました。

仕様

セッションの取り扱いと一言に言っても、その用途は多岐にわたります。今回は「Webサイトの多言語化」という仕様を例に実装します↓

  • サイト上にドロップダウンメニューがあり、そこから使用言語(日本語 / 英語 / 中国語 / ベトナム語 / ポルトガル語など)を選択できる
  • 使用言語を選択したらサイト上の一部のテキストが選択した言語に翻訳されて表示される
  • サイトには管理者用ページとユーザー用ページがあり、翻訳はユーザー用ページ全体でのみ行う

実装

タイトルにある通り今回の趣旨は妥協です。困ったらLaravelの機能を利用したりして、いい感じに手を抜きます。
Laravelにもともと実装されている多言語化機能はこちらを参照してください。

前提

レイヤー

既存の部分は Domain層(Domain modelやFactoryクラス、Repositoryのinterfaceなど)
Infrastructure層(RepositoryやQueryServiceの実装クラスなど)
UseCase層(UseCaseクラス、QueryServiceのinterfaceなど)
Presentation層(ControllerやFormRequest、Middlewareなど)
の4層に分割して実装されています(その前段階で文脈ごとに大きく分割されています。詳しくは以降のディレクトリ図参照)。

今回の設計では依存の向きは所謂レイヤードアーキテクチャのようにInfrastructure層に向かうものではなく、図の矢印で示すようにDomain層へ向かうものとなっています。 多言語化のためのセッションの取り扱いも、既存の作りと同じように層を分けて実装します。

全体の流れ

1.サイトのトップページのメニューから使用言語を選択
2./lang/:localeにGETリクエスト送信。localeには"ja"や"en"など文字列のパラメーターが入る想定
3.セッションにlocale値設定
4.もとのページにリダイレクト(このときmiddlewareでlocale設定)

もう少し詳細に書くと
リクエスト -> Controllerでパラメーター取得 -> UseCaseクラスに渡して色々する(この"色々"の内部でセッションに値を保存したりする)
みたいな感じです。

ディレクトリ構成

localeはユーザー側ページ全体で取り回される共通のものなので、どこに置くべきか迷いました。
現在以下のように分割されています

│   ├── Packages
│   │   ├── AdminPage //管理者用ページ
│   │   ├── FrontPage //ユーザー用ページ
│   │   │   ├── Appointment
│   │   │   ├── Contact
│   │   │   ├── Home
│   │   │   └── StaticPages
│   │   └── Shared

考えた結果、FrontPage全体で共通のものということでSharedというディレクトリを作成しました。
もはや何も考えていないのと同じですが、いい感じに妥協していくのが今回の趣旨です。どんどん妥協しましょう。

│   ├── Packages
│   │   ├── AdminPage
│   │   ├── FrontPage
│   │   │   ├── Appointment
│   │   │   ├── Contact
│   │   │   ├── Home
│   │   │   ├── Shared //<-追加
│   │   │   │   ├── Domain
│   │   │   │   ├── Infrastructure
│   │   │   │   ├── Presentation
│   │   │   │   └── UseCase
│   │   │   └── StaticPages
│   │   └── Shared

Domain model

今回の場合localeを司るモデルを作るのがいいかなと判断しました。ということでlocale用のDomain modelを作成します。以下のようにモデリングしました。

gistc00482616b954c51babb350dc151ad9d

│── Shared
├── Domain
│   └── Models
│       └── Locale.php
├── Infrastructure
├── Presentation
└── UseCase

セッションを取り扱うクラス

このクラスをShared以下のどの層に配置するかを考えます。そもそもどういうクラスなのかというと

  • localeの値をセッションに保存する
  • セッションに保存されているlocaleの値を確認する

この2つを行うクラスです。localeの値とセッションを使ってxxをする、という役割なのでUseCase層が適切かと思います。interfaceを作成してUseCase層に置きます。

gist5eb0257e22b7ceca7a2af9df60f1d2dc

│── Shared
├── Domain
│   └── Models
│       └── Locale.php
├── Infrastructure
├── Presentation
└── UseCase
   └── LocaleSessionInterface.php

次にこのinterfaceの実装クラスを作成し、Presentation層に配置します。

gist1150419297fcfac1fdc7c5d5c1728fed

getLocaleSession()から返すDTOは以下のようになっています。

gistfbe39c56e70a119b97fb9faad28038df

├── Shared //<-追加
│   ├── Domain
│   │   └── Models
│   │       └── Locale.php
│   ├── Infrastructure
│   ├── Presentation
│   │   ├── Dto
│   │   │   └── GetLocaleSessionResponse.php
│   │   └── LocaleSession.php
│   └── UseCase
│      └── LocaleSessionInterface.php

interfaceと実装クラスはAppServiceProviderでbindするのを忘れないでください。

実装クラスをPresentation層に置く理由

最初はinterfaceと同じUseCase層に置こうと考えました。しかし実装クラスではLaravelのsession()ヘルパーを利用して「具体的にどうするか」を記述しています。UseCase層にそこまで具体的なものを置くのはよくなさそうです。
残るはDomain、Infrastructure、Presentationの三つの層ですが、そもそも何のためにセッションにlocaleの値を保存したりするのかというと、サイト上に表示されるテキストをlocaleに応じて変更するためでした。それを考えるとPresentation層に実装クラスを置くことが適切だと思われます。

locale値を変更するUseCaseクラス

Controllerから呼び出されるクラスです。リクエストがきたらControllerからこのクラスにパラメーターを渡して後続の処理を行います。 パラメーターからLocaleのインスタンスを作成し、パラメーターの値をセッションに保存します。セッション保存の具体的な処理は上記のLocaleSessionクラスで行われるので、ここには抽象的な手続きのみ記述します。

gist05956c45ab6f0dcd4531d471c5fc6d28

こちらのクラスはトップページから使用言語を選択した時のUseCaseなので、Shared以下ではなく該当するコンテキストのディレクトリ以下に配置します。

├── Home
│   ├── Domain
│   ├── Infrastructure
│   ├── Presentation
│   └── UseCase
│       └── SetLocaleUseCase.php

Controller

こちらは特筆すべき部分はありません。素直に実装します。

gist2a5bade46210839f30a9c987b32f1ff0

├── Home
│   ├── Domain
│   ├── Infrastructure
│   ├── Presentation
│   │   ├── Controllers
│   │       └── LocaleController.php
│   └── UseCase
│       └── SetLocaleUseCase.php

現在のlocaleを設定するmiddleware

Laravelのlocale設定は

app()->setLocale('locale_value')

で実現できます(Facadeを使用する方法もあります)。この部分はLaravelのmiddlewareで行い、ユーザー側の全ページに対して適用します。

gist860ff2f0cdde0dc7d3d2433b83decf6e

app/Http/Kernel.phpの$routeMiddleware配列に忘れず追加しておきます。

['set.locale' => \App\Packages\FrontPage\Home\Presentation\Middleware\SetLocaleMiddleware::class,]
├── Home
│   ├── Domain
│   ├── Infrastructure
│   ├── Presentation
│   │   ├── Controllers
│   │    │    └── LocaleController.php
│   │   ├── Middleware
│   │         └── SetLocaleMiddleware.php
│   └── UseCase
│       └── SetLocaleUseCase.php

ルーティング

gistfd3a74cc91d52ece76a6a23a2fa23c83

これで一通りの実装が完了しました。後は公式ドキュメントに従って言語ファイルを作成すれば、@langディレクティブや__()ヘルパーを使用して翻訳文字列が取得可能です。

メリット

この実装のメリットは、「ある文脈におけるセッションの取り扱い方法が限定されているので、コード内に散乱しがちなヘルパーやFacadeの乱用を抑制できたり、セッションのキーにする文字列の管理がしやすくなる」ということだと考えています。また、文脈ごとに区切ってセッションの取り扱いを限定するという実装は、セッションを取り扱う他の様々な場面でも応用できるのではと思います。粒度も場合によりけりだと思いますので、もっとスコープを広くして汎用的なセッション取り扱いクラスを作ってもいいかもしれません。

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

参考図書: ドメイン駆動設計 モデリング/実装ガイド - little-hands - BOOTH

現代を支配する究極のエンジニア像

f:id:daigo2895:20211012163521j:plain はじめまして!フロントエンドの久保です。

今回は前回の【営業部 → エンジニア】へジョブチェンジした話に続き、
「理想のエンジニア像」についてお話します。
目指すエンジニア像は人それぞれなので一つの参考になれば嬉しいです。

まずは結論から

プログラムが書けて、デザインが出来て、マーケティングも出来るエンジニアです!
・・・なんじゃそれ(笑)

実現させるには

そんな野球少年がプロ野球選手になりたい!と言ってるわけではないのです。
実はきちんとそういうポジションがあります。
それを説明させてください。

その名もグロースハッカー

Growth(成長)Hackerといい、
急激な成長を遂げた世界的なウェブサービスFacebookTwitterDropboxなどの成長を支えたと言われています。

業務内容としては、端的にいうと最低限のコストでユーザーをよりエンゲージメント(顧客・ユーザーの愛着度)の高い状態に持っていく事です。
言い換えると、トライアンドエラーを繰り返しながら新たな成果を作り出すことだと言えます。
そして、優秀なグロースハッカーほど、広告費やマーケティング費をかけずに任務を遂行するといわれています。

用語自体は2010年に出来た言葉ですが、近年多様なサービスや類似のサービスが増えてきてる中で 顧客に向き合い、商品・サービスに最も合った戦略を自ら模索していく姿が求められることから需要が急激に伸びてきています。

なぜグロースハッカーを目指すのか

私は営業部時代に様々なサービスを取り扱ってきました。
そこで見たものは、クオリティに関係なくユーザーにいい見せ方をしているサービスが数字を伸ばすという悲しい現実です。
逆に同業他社と比べて優位性のあるものも何故か競り負けている状態です。

それを改善するにはマーケティングの見直し、そして実装をするエンジニアが必要なことがわかりました。
しかし現状自社にはその2つが出来る人材はおらず、他業種とのコミュニケーションにおいてもなかなかもどかしい経験をしました。

その経験から私は、
おぼろげながらもその課題を解決出来るような人材になりたいと思うようになり、
そこから現在、グロースハッカーという明確な目標が生まれました。

目指すのは一つ、
「本当に良いサービスが正しくユーザーに届くようにしたい」です。

必要なスキル

グロースハッカーとは具体的に何が出来る人なのかざっくり2つに分けて説明します。

① データ解析能力

サービスを成長させるためにアクセス分析は勿論、カスタマー分析、事業分析、マーケティング分析など総合的に行います。
また、グロースハッカーが打ち出す施策は、勘やこれまでの経験、定性的な情報では行わず、
定量的(数値化出来るもの)なデータを分析した結果に基づいて行われます。
そのため、分析ツールを使いこなす知識や統計学に関する知識も求められます。

② 問題点を把握し、仮設を立て、解決する能力

①で提案したものを自らが実装出来るのがグロースハッカーの強みだと考えます。
これによって従来よりもより細やかなトライアンドエラーが実行出来ることとなり、サービスとしての力強さが実現するのです。

これはシンプルな仕事に思えますが、辛抱強く膨大なデータを検証し、徹底したA/Bテストを実施することが非常に重要なのです。
グロースハックのために打ち出す施策の75%〜90%は失敗に終わると言われています。
その中で地道に素早いサイクルで実行し続ける「不屈の精神」も必要となってきます。

5段階のフレームワーク「AARRR」

グロースハックには「AARRR(アー)」というフレームワークがあります。
具体的には、下記となります。

① Acquisition(ユーザー獲得)
② Activation(活性化)
③ Retention(継続)
④ Referral(紹介)
⑤ Revenue(収益化)

これが実行出来る人材になれば市場での価値は勿論のこと、
自分が思い描くエンジニア像を実現出来ると考えます。

WEBでの集客、交流が主流をなっている今、
1人でも実行出来る人材が増えたらもっともっとエンジニア市場の価値が上がり、報酬も上がっていくことでしょう。

求められる資質「ABCDE」

グロースハッカーに求められる資質を紹介します。

① Analyticity(分析力に長けている)
② Broad interest(好奇心に富んでいる)
③ Creative(創造性が豊かである)
④ Discipline(自らを律し、地道に取り組める)
⑤ Empathy(ユーザーの気持ちに共感できる)

これを見ると、いかにユーザーを想い地道に分析出来るかが重要となってくることが わかります。
ちなみに現時点では「B,D,E」を持ち合わせていると思っています!
色々な経験をし、分析力や創造性を高めていけるように努力したいと思います。

参考

グロースハッカーとは?シリコンバレーでも注目される最先端のWEBマーケティング職 | 株式会社EXIDEA

グロースハックとは?〜サービスを急成長させる方法と実践のための6ステップ〜 | ProSharing Consulting(プロシェアリングコンサルティング)

最後に

いかがだったでしょうか。 この記事でグロースハッカーを知ったという人もいるのではないでしょうか。 まだまだ書ききれなかったことも多くありますので少しでも興味を持った方は一度ググってみるのも良いかもしれません。

営業からエンジニアにジョブチェンジし半年が経過しようとしています。 まだまだ出来ることは少ないですが今までの経験を生かし、やるからには希少なエンジニアになるべく努力をしていきます。

そして…

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

EMトライアングルから見るWizのエンジニアリングマネージャーの現役割と、さらなる成長に向けて組織再編した話

こんにちは、株式会社Wizでエンジニアリングマネージャー(以下EM)をやっている上野です。

バックエンドエンジニアとして活動したのちEMとなり、数年経過しました。

今回、WizのEMがどんな活動を行っているのかを振り返った上で、 さらなるエンジニア組織の前進のために、10月から実施した組織再編について紹介します。

(前提)そもそもエンジニア組織はなにやっているの?

前提として、Wizのエンジニア組織は大きく2つの役割を担っており、すべてが自社開発となります。

  • 各種Webメディアを開発し集客する広告的役割
  • 社内外を業務改善したり、営業価値を最大化するようなWebプロダクト開発

WizのEMの現役割

現役割の整理には、「エンジニアリングマネジメントトライアングル」を利用させていただきます。 先人の方々の知見が詰まった非常にありがたいモデルです。

「テクノロジー」「プロダクト」「チーム」の軸の間をそれぞれ埋める空白領域が、EMの役割だという考え方です。

弊社において、役割として担っている部分に色をつけたものが以下です。

f:id:uehi1206:20211008162609j:plain

弊社のEMとしては、赤部分を担うことが主な役割です。

一方で、青部分のような特定のEMのみ担っているものもあり、 これは、各EMのやりたい領域や得意領域に合わせて遂行している部分です。 弊社の「言ったもの勝ち」「やったもの勝ち」の文化が出ている部分ではないでしょうか。

また、全体として「チーム」に寄っていると思いますが、これも文化として「人の成長」「何をやるかより誰とやるか」を大切にしている結果かもしれません。

具体的には主に以下のような活動を行っています。

  • アサインやメンバーのリソース/進捗マネジメント
    • メンバーのやりたいこと、できることを考慮したアサインと、完遂までの支援、負荷調整など。
  • メンバーのキャリア開発・成長支援
    • 1on1、コミュニケーションをベースとした関係構築
  • 組織としてのビジョン・ミッションの共有と評価・フィードバック
    • 組織のやりたいと個人のやりたいをマッチングさせ目標を定め、取り組みをフィードバック。
  • 採用活動、面接

さらなる成長に向けての組織再編

まずは結論から。このような Before → After にしました。

f:id:uehi1206:20211008162907j:plain

f:id:uehi1206:20211008162921j:plain

なぜこのような再編をやったかについては、以下のような課題解決の考えからです。

「ミッション」に、よりフォーカスする

EMトライアングルの現状でも出ていますが、「プロダクト」寄りの役割がまだまだ弱いと感じていました。 これは、ミッションが弱いということにもつながり、ここをもっと担っていきたいという課題がありました。

そこで、Beforeのような「フロントエンド」「バックエンド」といった職能別組織をやめ、 「メディア」「プロダクト」といったミッションの大枠で組織を定義しました。 そこには、各人が「フロントエンドだけ」「バックエンドだけ」を学習・業務遂行すればよいわけではなく、 ミッション達成のために得意分野を重ね合わせ、協力していってほしいというメッセージです。

曖昧な部分を、役割として明確化し、整備したい。

エンジニアのメイン業務は開発することですが、それだけでは強いエンジニア組織になりません。 例えば本ブログのような社外向けの発信といったものや、採用にまつわるイベント・カジュアル面談といったもの、 教育体制を作ったり、コミュニケーション改善のための施策などなど、開発体制をつくるための周辺業務があります。

もちろん全社的には採用や教育の事業部は存在しますが、エンジニア組織に適用するにはマッチしない部分もあるため、 これまでエンジニア組織内の有志のメンバーやEMが推進してくれていました。 これが、上記EMトライアングルの青部分「特定のEMのみ担っているもの」であったり、 赤部分「EM全員が担うもの」でもEMによって得意領域の違いから取り組みに濃淡がありました。

そのため今回、「本部」を組織構造として明言した、ということになります。

以上が、組織再編の思いです。

おわりに

このように、エンジニア組織は、EMとメンバーがともに 様々な試行錯誤を繰り返しながら、日々成長していっています。

一緒に切磋琢磨したい方、エンジニアもEMも募集していますので、 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

知ってると得するかもしれないCSS(Sass)のニッチなテクニック集

f:id:wiz_sasaki:20210927144411j:plain

皆様こんにちは、フロントエンドエンジニアの佐々木です。

今回は私が実際に実務で使用しているCSS(Sass)のちょっとしたテクニック集を紹介しようと思います。

否定疑似クラス「:not()」を利用した余白のとり方

:not():first-childを指定することで、最初の要素以外という指定ができます。

<ul class="list">
  <li class="list-item"></li>
  <li class="list-item"></li>
  <li class="list-item"></li>
  <li class="list-item"></li>
</ul>
.list-item {
  &:not(:first-child) {
    margin-top: 16px;
  }
}

これの何が良いのかというと、リストのようなレイアウトは最初もしくは最後の余白を0にしたいことがほとんどだと思います(他の要素への影響を最小限にするため)。 .list-itemに直接margin-topをとってしまうと.list-item:first-childmargin-topを打ち消す記述をしないといけません。:not()を使用することで不要な記述を減らすことができます。

「&」を利用した連続する同じ要素間の余白

ネスト内で& + & とすることで次の要素が同じだったらという指定ができます(ここでは.l-section)。

<section class="l-section">
  <h2>セクション1</h2>
</section>

<section class="l-section">
  <h2>セクション2</h2>
</section>

<section class="l-section">
  <h2>セクション3</h2>
</section>
.l-section {
  & + & {
    margin-top: 50px;
  }
}

先程の&:not(:first-child)に似ていますが、要素の間に別の要素が入って来た時の挙動が微妙に違います。

注意点としてこの方法はネストの親でのみ使用可能です。

ちなみに以下と同じ意味です。

.l-section {
  + .l-section {
    margin-top: 50px;
  }
}

詳細度を無理やり上げる

CSSには詳細度というのもが存在します。上書きするにはその詳細度を上回る必要があります。 !importantを使えば簡単に上書き可能ですが、宗教上の理由で!importantを使えない方がほとんどだと思います。以下はその代替方法です。

CSS詳細度計算サイト : https://specificity.keegan.st/

<div class="content">
  <p class="text">この文字の色は赤です。</p>
</div>
//詳細度 : 1, 1, 0
.text {
  &:not(#_) {
    color: red;
  }
}

//詳細度 : 0, 1, 0
.text {
  color: blue;
}

//詳細度 : 0, 2, 0
.content {
  .text {
    color: blue;
  }
}

上記のように&:not(#_)とすることで、.text#_ではないという指定になります。こうすることでIDを指定した時と同等の詳細度を得ることができます。

この方法のメリットは!importantを使用せずに高い詳細度を得ることにあります。AMPのような!importantが使用するとエラーが出る環境だと使う可能性があります。

可読性はあまり良くないので、そもそもこれを使わなくて済むようなCSS設計を心がけたほうが良いです。

他に有名な詳細度を上げる方法として:not(:root)という方法も存在します。

画像の比率を固定する

様々なデバイスに対応しないといけない現在のWEB制作において画像の比率を維持するということは非常に重要なことです。以下はpaddingの仕様を利用した画像の比率維持方法です。

ここでの画像は1920px * 1080pxを想定しています。

<div class="image-wrapper">
    <img src="/path/to/image.jpg" width="1920" height="1080" alt="比率固定したい画像">
</div>
.image-wrapper {
  position: relative;
  &::before {
    content: '';
    display: block;
    padding-top: calc((1080 / 1920) * 100%);
  }
  img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}

上記のCSSで1920px * 1080pxの比率は画面の幅が変わっても維持されます。

ここで重要なのは疑似要素::beforepadding-topです。padding-topは%で指定するとその値は親要素の幅に対しての割合となります。

以下の計算式に当てはめると幅に対する高さの比率を求めることができます

(高さ / 幅) * 100%

これを利用して疑似要素で高さをとり、imgをabsoluteで浮かせて配置することで画像の比率の維持が可能です。

しかし、非常に可読性が悪くなるのでこういった面倒な計算をするものはmixin化すると使い勝手がよくなります。

@mixin aspect-ratio($width, $height, $first: true) {
  position: relative;
 
  &::before {
    content: '';
    display: block;
    padding-top: ($height / $width) * 100%;
  }

  @if $first == true {
    & > :first-child {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  }
}
@include aspect-ratio(1920, 1080);

親要素を突き破って横幅100%

背景のみ横幅100%にして色をつけたいときなどに役に立ちます。

margin-right: calc(50% - 50vw);
margin-left: calc(50% - 50vw);

padding-right: calc(50vw - 50%);
padding-left: calc(50vw - 50%);

ポイントはpaddingで内要素の幅を保っていることです。ですのでpadding削除すると完全に横幅100%になります。

状況に応じて使い分けていくと良いでしょう。

否定形のメディアクエリ

not演算子を使うとメディアクエリの内容を反転することができます。

例えば、767px以下768以上のスタイルを書きたい場合があるとします。

.text { 
  @media screen and (min-width: 768px) { 
    color: red; 
  } 
}
.text { 
  @media screen and (max-width: 767px) { 
    color: blue; 
  } 
}

このように書きたくなりますが、これは特定の条件でスタイルが当たりません。

特定の条件とは768px〜767pxの間である画面幅が767.5pxのなどの場合はスタイルが当たりません。これはブラウザの拡大率や画面の解像度の組み合わせによって起こる可能があります。

.text { 
  @media screen and (min-width: 768px) { 
    color: red; 
  } 
}
.text { 
  @media not screen and (min-width: 768px) { 
    color: blue; 
  } 
}

上記のようにメディアクエリにnot演算子を組み合わせることによって画面幅が768pxより小さい場合という指定が可能です。

つまり画面幅が767.5pxの場合でもスタイルが当たります。

メディアクエリについての詳しい解説はコチラ

ユーザビリティを考慮したコンテンツの非表示方法

以下のように実装するとテキストは見えないけどスクリーンリーダーは読み上げ可能を実現できます。

<p class="visually-hidden">このテキストは見えないけどスクリーンリーダーは読み上げます。</p>
.visually-hidden {
  position: absolute;
  white-space: nowrap;
  width: 1px;
  height: 1px;
  overflow: hidden;
  border: 0;
  padding: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%); 
  margin: -1px;
}

詳しくは下記サイトのHiding content visuallyの箇所をご確認ください。

https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939

他のメリットとしてはdisplay: noneではないのでフォーカスが可能な点が挙げられます。

最後に

今回は「CSSのニッチなテクニック集」という内容でパッと思いついたものを紹介してみました。知っている人は「なんだそんなことか」と思われる内容だったかもしれません。

CSSはかなり自由に書けてしまうのでオレオレコーディングになりがちです。特に:not()を利用したものは便利な反面、知らない人は「ん?」ってなることが多いです。

他の人もCSSを触ることを考慮して複雑な内容ほどコメントを残しておくと良いでしょう。

それでは今回は以上となります。この記事が少しでも誰かの役の立てればいいなと思います。

また、

Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

リモートで社会人スタートした新卒エンジニアの本音

アイキャッチ画像 こんにちは!フロントエンドエンジニアの島田です。
前回インターン研修について記事を書きましたが、今回はリモートワークについてお話しします。

研修・実務のほとんどをリモートで行ってきた新卒エンジニアが、リモートワークに対する本音を語っていきたいと思います。

1.リモートワークのメリット

早速良かった点からお話ししていきます!^^

その1 通勤ストレスがゼロ

まずこちらが真っ先に思い浮かびました。笑
通勤時間って、合計するとかなり時間がかかると思いませんか?
会社まで片道30分だとしても一日で60分、一週間で5時間も移動に時間を使っていることになりますよね…😓

しかし!これがリモートになると出勤や退勤で使う時間が0になる上に、満員電車のストレスからも逃れることができちゃいます!(大学へ行くのに電車で40分、片道1時間10分かけていた私からしましたら、本当に天国でした…!)

そして最も有り難かったのが、有効活用できる時間が大幅に増えたことです!
本来通勤に使う時間で机に向かって勉強したり、趣味に使ったりと…浮いた時間を自由に使っています。

その2 リラックスした環境で仕事できる

自分の家や部屋って落ち着きますよね。
リモートワークは出社する必要がないため、自分の部屋で仕事することができます!

落ち着いた環境で黙々と作業できるのは、集中できて良いと感じています。
時々の出社も、気分転換のような感じで新鮮で楽しいです!

その3 コミュニケーション量が増えた

私がWizに入社するまでは、

「リモートで仕事するって聞いたけど、会社の人とのコミュニケーションはどうなるのかな…?」
「リモートじゃ話す機会も少ないだろうし、先輩方や同期と仲良くなれるか心配だ…」

という、リモートワークに対する不安がいくつかありました。
が!実際に入社してから感じたことは、

「あれっ?全然問題なくコミュニケーション取れてる」

でした。笑

WizではoViceというツールを使って、業務に取り組んでいます。
有り難いことにここでは雑談会等、コミュニケーションの活性化を図るイベントが沢山行われています。そういうイベントに参加することで、色々な職種の方とお話しする機会があります。

それ以外でも、業務終わりにoViceに残って雑談することもあり、コミュニケーションが不足していると感じることはあまり無かったです!

2.リモートワークのデメリット

何かと便利なリモートワークですが、もちろん良い点ばかりではありません😰
ここでは、リモートで作業している際に「ここはちょっと大変だな…」と感じたことについてお話ししていきます。

その1 相手に何かを伝える手段が限られている

リモートワークでは、相手の顔を見ながら説明する機会が少ないです。基本的に文章や音声のみ、必要に応じて画面共有しながらやりとりをしています。

最初の頃は自分の伝えたい内容が上手く相手に伝わらなかったり、逆に相手の説明をなかなか理解できないといったことがありました。
慣れてしまえば問題ないのですが、慣れるまでが大変でした。

その2 慣れるまでは話しかけづらい

リモートで作業している時は相手の様子がわかりません。そのため、先輩に質問をしたい時に

「今話しかけても大丈夫だろうか?作業の邪魔にならないだろうか…」

と、必要以上にタイミングを気にしてしまい、対面の時よりも話しかけづらいと感じてしまうことが多々ありました。

こちらも慣れてしまえば問題ないのですが、最初はリモートで他の人に話しかけることのハードルが高いと感じました。

その3 環境によっては集中できない

先程「リラックスした環境で仕事できる」と記述しましたが、自宅付近の環境によってはそれが難しい場合もあります。

在宅で仕事をしている際に気づいたことですが、外の音って意外と聞こえてくるんですよね😓
近所で工事していたり外で子供が騒いでいたりすると、集中力が切れやすくなって逆に仕事が捗らないことが多いです…(体験談)

こればっかりは仕方のないことですが、しっかり集中できる環境で仕事したいのなら、自室の環境に加え外の環境も良いところを選ばなければと強く感じました。

余談ですが、仕事机や椅子も自分の身長やスタイルに合ったものを使わないと、腰が痛くなったり疲れやすくなります。
多少高くても、自分に合ったものの利用をオススメします…!

3.まとめ

リモートワークには便利な点が多い反面、快適なリモート生活を得るには、慣れることが必要だったり周囲の環境が大きく影響します。

この記事を通して、リモートワークについて少しでも参考になりましたら幸いです!^^

最後に…

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

ServiceWorkerをサクッと導入してみた

        f:id:wiz-akakura:20210922142424p:plain

こんにちは、インフラエンジニアの赤倉です。

今回は「ServiceWorker」のオフラインキャッシュを使ってWebページの読み込み高速化を実現してみたいとおもいます。

ServiceWorkerとは

ServiceWorker とは、ブラウザが Web ページとは別にバックグラウンドで実行するJavaScript環境のことです。ページ内のコンテンツをブラウザ側にキャッシュしてオフライン状態でも利用することができます。

対応ブラウザ

また、ServiceWorkerの稼働条件としてWebページのHTTPS化が必須となっております。(またはlocalhostでも実行可)

ServiceWorkerのライフサイクル

register
  • ブラウザにServiceWorkerのJavaScriptファイルのパスを知らせます。
  • ServiceWorkerが操作できるスコープ(後述)を指定します。
install
  • ブラウザにキャッシュが生成されます。
  • キャッシュ名(=バージョン)の定義やキャッシュ対象を指定します。
activate
  • ServiceWorkerのJavaScriptファイルを更新します。 ServiceWorkerはサーバ上のJavaScriptファイルとバイト単位で差が生じた場合に新しいファイルと認識します。
  • ユーザがブラウザを閉じたり、ページを更新したことを契機にServiceWorkerが更新されます。

スコープとは

ServiceWorker が管理するURLの範囲を示します。
スコープはServiceWorkerのスクリプトファイルが設置されているパス以下の階層を指定する必要があります。なお、このパス制限はService-Worker-Allowed: ヘッダで解除可能です。

ServiceWorker 導入

前置きが長くなってしまいましたが、ここからが導入手順になります。

準備するファイル
DocumentRoot
├── main.js
├── serviceworker.js

今回はどちらのファイルもDocumentRootに配置するものとします。

ファイル名に特に制約はありません。

1. ファイル作成
main.js

registerを行うファイルです。serviceworker.jsのパスを示します。

if ("serviceWorker" in navigator) {
  window.addEventListener("load", function () {
    //今回はDocRoot以下をServiceWorkerのスコープとします
    navigator.serviceWorker.register("/serviceworker.js", { scope: "./" }).then(
      function (registration) {
        // 登録成功
        console.log(
          "ServiceWorker registration successful with scope: ",
          registration.scope
        );
      },
      function (err) {
        // 登録失敗
        console.log("ServiceWorker registration failed: ", err);
      }
    );
  });
}
serviceworker.js

ServiceWorkerのコアファイルです。※コードの細かい説明は省略します。

//キャッシュ名(=バージョン)を指定する
var
CACHE_NAME = "cache-v1";
//キャッシュするファイル or ディレクトリを指定する
var urlsToCache = [ "/", ]; // install self.addEventListener("install", function (event) { event.waitUntil( caches.open(CACHE_NAME).then(function (cache) { console.log("Opened cache"); return cache.addAll(urlsToCache); }) ); }); // activate self.addEventListener("activate", function (event) { var cacheWhitelist = [CACHE_NAME]; event.waitUntil( caches.keys().then(function (cacheNames) { return Promise.all( cacheNames.map(function (cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); // fetch self.addEventListener("fetch", function (event) { event.respondWith( caches.match(event.request).then(function (response) { if (response) { return response; } var fetchRequest = event.request.clone(); return fetch(fetchRequest).then(function (response) { if (!response || response.status !== 200 || response.type !== "basic") { return response; } var responseToCache = response.clone(); caches.open(CACHE_NAME).then(function (cache) { cache.put(event.request, responseToCache); }); return response; }); }) ); });
2. main.js読み込み

ページの共通ヘッダなどに記載します。

<script src="/main.js"></script>

導入後の動作チェック

ServiceWorkerが動作していることはブラウザから確認可能です。

 

下記はGoogleChromeの検証モードの画面です。

[Status Code]に"(from service worker)"と記載されていればオフラインキャッシュ化成功です。

f:id:wiz-akakura:20210922133406p:plain

 

また、[Application]の[Service Woerkers]でも動作していることが確認できます。

f:id:wiz-akakura:20210922141334p:plain

効果

今回の検証では100MBの画像を読み込んだページでオフラインキャッシュの効果を試してみました。

 

ServiceWorkerなしの状態では画像の読み込みに852msかかっていますが・・

f:id:wiz-akakura:20210922122154p:plain

これがServiceWorkerのオフラインキャッシュを利用すると249msまで短縮されていることがわかります。

f:id:wiz-akakura:20210922122158p:plain

この検証はローカルのMAMP環境で実施したので読み込み時間にそこまで大きな差はありませんが、本番サーバであればもっと顕著に効果が出るとおもいます。

さいごに

ServiceWorkerを使ってオフラインキャッシュを作ることによって、画像やcss、 jsファイルなどの静的コンテンツの読み込みをコストゼロで高速化することができるので、非常に便利ですね。コストや技術的な理由でキャッシュサーバを用意できない場合などに導入を検討してみては如何でしょうか?

 

株式会社Wizではエンジニアを募集しています!

↓↓↓興味ある方はぜひご覧ください!↓↓↓